From ded6a35ec7dccb784e66e03e233c3607209ac562 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 13 Dec 2024 21:23:48 +0100 Subject: [PATCH] Room::getEvent() You can now request an arbitrary event in the room, and save it for future use (within a single running session, same as timeline). --- Quotient/room.cpp | 71 ++++++++++++++++++++++++++++++++++++++++----- Quotient/room.h | 9 ++++++ quotest/quotest.cpp | 15 ++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 261f5c62..b8a561e5 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -80,6 +80,12 @@ using std::llround; namespace { enum EventsPlacement : int { Older = -1, Newer = 1 }; +struct SingleEventRequest { + EventId eventId; + JobHandle requestHandle; + std::vector eventPromises{}; +}; + struct HistoryRequest { EventId upToEventId; QDeadlineTimer deadline; @@ -118,6 +124,8 @@ class Q_DECL_HIDDEN Room::Private { Timeline timeline; PendingEvents unsyncedEvents; + std::unordered_map> cachedEvents; + std::vector singleEventRequests; QHash eventsIndex; // A map from event id/relation type pairs to a vector of event pointers. Not using QMultiHash, // because we want to quickly return a number of relations for a given event without enumerating @@ -256,12 +264,30 @@ class Q_DECL_HIDDEN Room::Private { } return changes; } + SingleEventRequest makeSingleEventRequest(const QString& eventId); void addRelation(const ReactionEvent& reactionEvt); - void addRelations(auto from, auto to) + void finishEventPromises(const RoomEvent& evt) + { + if (auto it = std::ranges::find(singleEventRequests, evt.id(), &SingleEventRequest::eventId); + it != cend(singleEventRequests)) { + for (auto& p : it->eventPromises) + if (!p.isCanceled()) { + p.addResult(std::cref(evt)); + p.finish(); + } + if (!it->requestHandle.isFinished()) + it->requestHandle.abandon(); + singleEventRequests.erase(it); + } + } + void afterAddedMessages(auto from, auto to) { - for (auto it = from; it != to; ++it) - if (const auto* reaction = it->template viewAs()) - addRelation(*reaction); + for (const auto& ti : std::ranges::subrange(from, to)) { + // NB: switchOnType() and std::bind_front() don't go well together + ti->switchOnType([this](const ReactionEvent& e) { addRelation(e); }); + finishEventPromises(*ti); + cachedEvents.erase(ti->id()); + } } Changes addNewMessageEvents(RoomEvents&& events); @@ -1765,7 +1791,8 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, }, [](QUrl&&) {} }, fileContent->commonInfo().source); - + // TODO: check if it has relations to another event and call getEvent() for it, _if_ + // we are in the displayed area if (auto n = q->checkForNotifications(ti); n.type != Notification::None) notifications.insert(eId, n); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); @@ -2382,6 +2409,36 @@ JobHandle Room::getPreviousContent(int limit, const QString& f return d->getPreviousContent(limit, filter); } +inline SingleEventRequest Room::Private::makeSingleEventRequest(const QString& eventId) +{ + return { eventId, + connection->callApi(id, eventId).then([this](RoomEventPtr&& pEvt) { + const auto [it, cachedEventInserted] = + cachedEvents.insert_or_assign(pEvt->id(), std::move(pEvt)); + finishEventPromises(*(it->second)); + if (QUO_ALARM(!cachedEventInserted)) + emit q->updatedEvent(it->first); // At least notify clients... + }) }; +} + +EventFuture Room::getEvent(const QString& eventId) +{ + using namespace std::ranges; + if (auto timelineIt = findInTimeline(eventId); timelineIt != historyEdge()) + return makeReadyValueFuture(std::cref(**timelineIt)); + auto baseStateObjects = d->baseState | views::values; + if (auto stateIt = find(baseStateObjects, eventId, &RoomEvent::id); + stateIt != end(baseStateObjects)) + return makeReadyValueFuture(std::cref(**stateIt)); + if (auto cachedIt = d->cachedEvents.find(eventId); cachedIt != d->cachedEvents.end()) + return makeReadyValueFuture(std::cref(*cachedIt->second)); + auto alreadyRequestedIt = find(d->singleEventRequests, eventId, &SingleEventRequest::eventId); + if (alreadyRequestedIt == end(d->singleEventRequests)) + alreadyRequestedIt = d->singleEventRequests.insert(d->singleEventRequests.cend(), + d->makeSingleEventRequest(eventId)); + return alreadyRequestedIt->eventPromises.emplace_back().future(); +} + EventFuture Room::ensureEvent(const QString& eventId, quint16 maxWaitSeconds) { if (auto eventIt = findInTimeline(eventId); eventIt != historyEdge()) @@ -3061,7 +3118,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } if (totalInserted > 0) { - addRelations(from, syncEdge()); + afterAddedMessages(from, syncEdge()); qCDebug(MESSAGES) << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" @@ -3120,7 +3177,7 @@ std::pair Room::Private::addHistoricalMessageEv q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); - addRelations(from, historyEdge()); + afterAddedMessages(from, historyEdge()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= ProfilerMinNsecs) qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to" << q->objectName() diff --git a/Quotient/room.h b/Quotient/room.h index d5a5898b..0b0ab8f9 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -750,6 +750,15 @@ class QUOTIENT_API Room : public QObject { QJsonArray exportMegolmSessions(); + //! \brief Obtain an arbitrary room event by its id + //! + //! Looks through the timeline, state events that arrived out of the timeline, and finally + //! cached individual events; if the event is not found locally, requests this one event from + //! the homeserver. + //! \return a ready future with the event reference if an event with \p eventId is found locally; + //! otherwise, a running future connected to the homeserver request + EventFuture getEvent(const QString& eventId); + //! \brief Loads the message history until the specified event id is found //! //! This is potentially heavy; clients should use this sparingly. One intended use case is diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index a739cff9..2e048a04 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -104,6 +104,7 @@ private slots: TEST_DECL(sendReaction) TEST_DECL(sendFile) TEST_DECL(sendCustomEvent) + TEST_DECL(getEvent) TEST_DECL(setTopic) TEST_DECL(redactEvent) TEST_DECL(changeName) @@ -547,6 +548,20 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return true; } +TEST_IMPL(getEvent) +{ + QUO_CHECK(targetRoom->maxTimelineIndex() - targetRoom->minTimelineIndex() > 5); + const auto& timelineEventIt = targetRoom->findInTimeline(targetRoom->maxTimelineIndex() - 5); + const auto timelineEventFuture = targetRoom->getEvent((*timelineEventIt)->id()); + FAIL_TEST_IF(!timelineEventFuture.isFinished()); + FINISH_TEST_IF(&timelineEventFuture.result().get() == timelineEventIt->event()); + const auto& stateEvent = targetRoom->creation(); + const auto stateEventFuture = targetRoom->getEvent(stateEvent->id()); + FAIL_TEST_IF(!stateEventFuture.isFinished()); + FINISH_TEST_IF(&stateEventFuture.result().get() == stateEvent); + FINISH_TEST(true); // TODO: Test a request to an event that is not loaded yet +} + DEFINE_SIMPLE_EVENT(CustomEvent, RoomEvent, "quotest.custom", int, testValue, "test_value")