Skip to content

Commit

Permalink
Merge #825(nvrwhere): Create ThreadView to track threads in a room
Browse files Browse the repository at this point in the history
  • Loading branch information
KitsuneRal authored Dec 10, 2024
2 parents ab1fde2 + 3084338 commit 8d9f044
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ target_sources(${QUOTIENT_LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS .
Quotient/events/keyverificationevent.h
Quotient/keyimport.h
Quotient/qt_connection_util.h
Quotient/thread.h
PRIVATE
Quotient/function_traits.cpp
Quotient/networkaccessmanager.cpp
Expand Down Expand Up @@ -247,6 +248,7 @@ target_sources(${QUOTIENT_LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS .
Quotient/e2ee/cryptoutils.cpp
Quotient/e2ee/sssshandler.cpp
Quotient/keyimport.cpp
Quotient/thread.cpp
libquotientemojis.qrc
)

Expand Down
5 changes: 3 additions & 2 deletions Quotient/events/roommessageevent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,10 @@ QString RoomMessageEvent::threadRootEventId() const
const auto relation = relatesTo();
if (relation && relation.value().type == EventRelation::ThreadType) {
return relation.value().eventId;
} else {
return unsignedPart<QJsonObject>("m.relations"_ls)[EventRelation::ThreadType].toString();
} else if (unsignedPart<QJsonObject>("m.relations"_L1).contains(EventRelation::ThreadType)) {
return id();
}
return {};
}

namespace {
Expand Down
4 changes: 4 additions & 0 deletions Quotient/events/roommessageevent.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ class QUOTIENT_API RoomMessageEvent : public RoomEvent {
//!
//! \note This will return the ID of the event if it is the thread root.
//!
//! \note If the event is the thread root event and has not been updated with the server-side
//! the function will return an empty string as we can't tell if the message
//! is threaded.
//!
//! \return The event ID of the thread root if threaded, an empty string otherwise.
QString threadRootEventId()const;

Expand Down
42 changes: 42 additions & 0 deletions Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "roommember.h"
#include "roomstateview.h"
#include "syncdata.h"
#include "thread.h"
#include "user.h"

#include "csapi/account-data.h"
Expand Down Expand Up @@ -123,6 +124,8 @@ class Q_DECL_HIDDEN Room::Private {
// about the timeline.
EventStats partiallyReadStats {}, unreadStats {};

ThreadView threads;

// For storing a list of current member names for the purpose of disambiguation.
QMultiHash<QString, QString> memberNameMap;
QStringList membersInvited;
Expand Down Expand Up @@ -270,6 +273,8 @@ class Q_DECL_HIDDEN Room::Private {
Timeline::size_type moveEventsToTimeline(RoomEventsRange events,
EventsPlacement placement);

void updateThread(const RoomEvent* event);

/**
* Remove events from the passed container that are already in the timeline
*/
Expand Down Expand Up @@ -555,6 +560,8 @@ const Room::PendingEvents& Room::pendingEvents() const
return d->unsyncedEvents;
}

const Room::ThreadView& Room::threads() const { return d->threads; }

int Room::requestedHistorySize() const
{
return eventsHistoryJob() != nullptr ? d->lastRequestedHistorySize : 0;
Expand Down Expand Up @@ -1752,12 +1759,47 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
if (auto n = q->checkForNotifications(ti); n.type != Notification::None)
notifications.insert(eId, n);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
updateThread(ti.event());
}
const auto insertedSize = (index - baseIndex) * placement;
Q_ASSERT(insertedSize == int(events.size()));
return Timeline::size_type(insertedSize);
}

void Room::Private::updateThread(const RoomEvent* event)
{
const auto rme = eventCast<const RoomMessageEvent>(event);
if (rme == nullptr) {
return;
}
if (!rme->isThreaded()) {
return;
}

auto& thread = threads[rme->threadRootEventId()];
if (thread.threadRootId.isEmpty()) {
thread.threadRootId = rme->threadRootEventId();
// If we can't find the root we assume it's a historical event and will be loaded later.
if (auto rootIt = q->findInTimeline(thread.threadRootId); rootIt != historyEdge()) {
thread.addEvent(rootIt->viewAs<RoomMessageEvent>(), true,
(*rootIt)->senderId() == connection->userId());
}
}

const auto threadLatestIndex = eventsIndex.constFind(thread.latestEventId);
const auto eventIndexIt = eventsIndex.constFind(rme->id());
if (QUO_ALARM_X(
eventIndexIt == eventsIndex.cend(),
rme->id()
+ u"not in the timeline. Update a thread after moving the event to timeline."_s)) {
return;
}

thread.addEvent(rme,
(threadLatestIndex == eventsIndex.cend() || *eventIndexIt > *threadLatestIndex),
rme->senderId() == connection->userId());
}

const Avatar& Room::memberAvatarObject(const QString& memberId) const
{
return connection()->userAvatar(member(memberId).avatarUrl());
Expand Down
5 changes: 5 additions & 0 deletions Quotient/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct MemberSorter;
class LeaveRoomJob;
class SetRoomStateWithKeyJob;
class RedactEventJob;
class Thread;

/** The data structure used to expose file transfer information to views
*
Expand Down Expand Up @@ -176,6 +177,7 @@ class QUOTIENT_API Room : public QObject {
using RelatedEvents = QVector<const RoomEvent*>;
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
using ThreadView = QHash<QString, Thread>;

//! \brief Room changes that can be tracked using Room::changed() signal
//!
Expand Down Expand Up @@ -363,6 +365,9 @@ class QUOTIENT_API Room : public QObject {
//!
//! Same as messageEvents().crend()
rev_iter_t historyEdge() const;

const ThreadView& threads() const;

//! \brief Get an iterator for the position beyond the latest arrived event
//!
//! Same as messageEvents().cend()
Expand Down
24 changes: 24 additions & 0 deletions Quotient/thread.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 James Graham <[email protected]>
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "thread.h"

#include "events/roommessageevent.h"

using namespace Quotient;

bool Thread::addEvent(const RoomMessageEvent* event, bool isLatest, bool isLocalUser)
{
// Note: the root event may not have the thread aggregation in its unsigned on creation
// hence checking the event id.
if (event->threadRootEventId() != threadRootId && event->id() != threadRootId) {
return false;
}
if (isLatest || latestEventId.isEmpty()) {
latestEventId = event->id();
}
++size;
localUserParticipated |= isLocalUser;

return true;
}
24 changes: 24 additions & 0 deletions Quotient/thread.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 James Graham <[email protected]>
// SPDX-License-Identifier: LGPL-2.1-or-later

#pragma once

#include <QString>

#include "quotient_export.h"

namespace Quotient {

class RoomMessageEvent;

class QUOTIENT_API Thread {
public:
QString threadRootId = {};
QString latestEventId = {};
int size = 0;
bool localUserParticipated = {};

bool addEvent(const RoomMessageEvent* event, bool isLatest, bool isLocalUser);
};

} // namespace Quotient
3 changes: 3 additions & 0 deletions autotests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ include(CMakeParseArguments)

add_custom_target(autotests ALL)

add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )

function(QUOTIENT_ADD_TEST)
cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
add_executable(${ARG_NAME} ${ARG_NAME}.cpp testutils.h testutils.cpp)
Expand All @@ -27,3 +29,4 @@ quotient_add_test(NAME testcryptoutils)
quotient_add_test(NAME testkeyverification)
quotient_add_test(NAME testcrosssigning)
quotient_add_test(NAME testkeyimport)
quotient_add_test(NAME testthread)
18 changes: 18 additions & 0 deletions autotests/data/test-thread1-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"content": {
"body": "Thread reply 1 event",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org"
}
},
"event_id": "$thread1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}
18 changes: 18 additions & 0 deletions autotests/data/test-thread2-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"content": {
"body": "Thread reply 2 event",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org"
}
},
"event_id": "$thread2:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}
14 changes: 14 additions & 0 deletions autotests/data/test-threadroot-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"content": {
"body": "Thread root event",
"msgtype": "m.text"
},
"event_id": "$threadroot:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}
88 changes: 88 additions & 0 deletions autotests/testthread.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2024 James Graham <[email protected]>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include <QTest>

#include <Quotient/events/roommessageevent.h>
#include <Quotient/thread.h>

#include "testutils.h"

using namespace Quotient;

class TestThread : public QObject
{
Q_OBJECT

private Q_SLOTS:
void newThread();
void historicalThread();
};

void TestThread::newThread()
{
auto testThread = Thread();
const auto rootEvent = loadEventFromFile<RoomMessageEvent>("test-threadroot-event.json"_L1);

testThread.threadRootId = rootEvent->id();
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, QString());
QCOMPARE(testThread.size, 0);
QCOMPARE(testThread.localUserParticipated, false);

testThread.addEvent(rootEvent.get(), true, false);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.size, 1);
QCOMPARE(testThread.localUserParticipated, false);

const auto replyEvent1 = loadEventFromFile<RoomMessageEvent>("test-thread1-event.json"_L1);
testThread.addEvent(replyEvent1.get(), true, true);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$thread1:example.org"_L1);
QCOMPARE(testThread.size, 2);
QCOMPARE(testThread.localUserParticipated, true);

const auto replyEvent2 = loadEventFromFile<RoomMessageEvent>("test-thread2-event.json"_L1);
testThread.addEvent(replyEvent2.get(), true, false);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$thread2:example.org"_L1);
QCOMPARE(testThread.size, 3);
QCOMPARE(testThread.localUserParticipated, true);
}

void TestThread::historicalThread()
{
auto testThread = Thread();
const auto replyEvent2 = loadEventFromFile<RoomMessageEvent>("test-thread2-event.json"_L1);

testThread.threadRootId = replyEvent2->threadRootEventId();
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, QString());
QCOMPARE(testThread.size, 0);
QCOMPARE(testThread.localUserParticipated, false);

testThread.addEvent(replyEvent2.get(), true, false);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$thread2:example.org"_L1);
QCOMPARE(testThread.size, 1);
QCOMPARE(testThread.localUserParticipated, false);

const auto replyEvent1 = loadEventFromFile<RoomMessageEvent>("test-thread1-event.json"_L1);
testThread.addEvent(replyEvent1.get(), false, true);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$thread2:example.org"_L1);
QCOMPARE(testThread.size, 2);
QCOMPARE(testThread.localUserParticipated, true);

const auto rootEvent = loadEventFromFile<RoomMessageEvent>("test-threadroot-event.json"_L1);
testThread.addEvent(rootEvent.get(), false, true);
QCOMPARE(testThread.threadRootId, "$threadroot:example.org"_L1);
QCOMPARE(testThread.latestEventId, "$thread2:example.org"_L1);
QCOMPARE(testThread.size, 3);
QCOMPARE(testThread.localUserParticipated, true);
}

QTEST_GUILESS_MAIN(TestThread)
#include "testthread.moc"
15 changes: 15 additions & 0 deletions autotests/testutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#include <memory>

#include <Quotient/events/event.h>

namespace Quotient {

class Connection;
Expand All @@ -17,6 +19,19 @@ class JobHandle;
std::shared_ptr<Connection> createTestConnection(QLatin1StringView localUserName,
QLatin1StringView secret,
QLatin1StringView deviceName);


template<EventClass EventT>
inline event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
{
if (!eventFileName.isEmpty()) {
QFile testEventFile;
testEventFile.setFileName(QLatin1StringView(DATA_DIR) + u'/' + eventFileName);
testEventFile.open(QIODevice::ReadOnly);
return loadEvent<EventT>(QJsonDocument::fromJson(testEventFile.readAll()).object());
}
return nullptr;
}
}

#define CREATE_CONNECTION(VAR, USERNAME, SECRET, DEVICE_NAME) \
Expand Down
Loading

0 comments on commit 8d9f044

Please sign in to comment.