Skip to content

Commit

Permalink
Load history up to the last read event when triggered
Browse files Browse the repository at this point in the history
The main work is done in QuaternionRoom::ensureHistory(), which runs
a chain of Room::getPreviousContent() calls until the event with
the passed id (in this case - last read event id) is obtained.

Fixes #799.
  • Loading branch information
KitsuneRal committed Jan 1, 2025
1 parent 3176555 commit ce19b02
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 3 deletions.
10 changes: 8 additions & 2 deletions client/qml/Timeline.qml
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ Page {
function onViewPositionRequested(index) {
scrollFinisher.scrollViewTo(index, ListView.Contain)
}
function onHistoryRequestChanged() {
scrollToReadMarkerButton.checked = controller.isHistoryRequestRunning
}
}

Connections {
Expand Down Expand Up @@ -686,8 +689,11 @@ Page {
if (messageModel.readMarkerVisualIndex < chatView.count)
scrollFinisher.scrollViewTo(messageModel.readMarkerVisualIndex,
ListView.Center)
else
room.getPreviousContent(chatView.count / 2) // FIXME, #799
else {
checkable = true
controller.ensureLastReadEvent()
}
}
onCheckedChanged: { if (!checked) checkable = false }
}
}
81 changes: 81 additions & 0 deletions client/quaternionroom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <Quotient/events/roommessageevent.h>
#include <QtCore/QRegularExpression>

#include <ranges>

using namespace Quotient;

QuaternionRoom::QuaternionRoom(Connection* connection, QString roomId, JoinState joinState)
Expand Down Expand Up @@ -96,6 +98,7 @@ void QuaternionRoom::onAddHistoricalTimelineEvents(rev_iter_t from)
{
std::for_each(from, messageEvents().crend(),
std::bind_front(&QuaternionRoom::checkForHighlights, this));
checkForRequestedEvents(from);
}

void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti)
Expand Down Expand Up @@ -132,3 +135,81 @@ void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti)
highlights.insert(e);
}
}

QuaternionRoom::EventFuture QuaternionRoom::ensureHistory(const QString& upToEventId,
quint16 maxWaitSeconds)
{
if (auto eventIt = findInTimeline(upToEventId); eventIt != historyEdge())
return makeReadyVoidFuture();

if (allHistoryLoaded())
return {};
// Request a small number of events (or whatever the ongoing request says, if there's any),
// to make sure checkForRequestedEvents() gets executed
getPreviousContent();
HistoryRequest r{ upToEventId,
QDeadlineTimer{ std::chrono::seconds(maxWaitSeconds), Qt::VeryCoarseTimer } };
auto future = r.promise.future();
r.promise.start();
historyRequests.push_back(std::move(r));
return future;
}

namespace {
using namespace std::ranges;
template <typename RangeT>
requires(std::convertible_to<range_reference_t<RangeT>, QString>)
inline auto dumpJoined(const RangeT& range, const QString& separator = u","_s)
{
return
#if defined(__cpp_lib_ranges_join_with) && defined(__cpp_lib_ranges_to_container)
to<QString>(join_with_view(range, separator));
#else
QStringList(begin(range), end(range)).join(separator);
#endif
}
}

void QuaternionRoom::checkForRequestedEvents(const rev_iter_t& from)
{
using namespace std::ranges;
const auto addedRange = subrange(from, historyEdge());
for (const auto& evtId : transform_view(addedRange, &RoomEvent::id)) {
cachedEvents.erase(evtId);
onGettingSingleEvent(evtId);
}
std::erase_if(historyRequests, [this, addedRange](HistoryRequest& request) {
auto& [upToEventId, deadline, promise] = request;
if (promise.isCanceled()) {
qCInfo(MAIN) << "The request to ensure event" << upToEventId << "has been cancelled";
return true;
}
if (auto it = find(addedRange, upToEventId, &RoomEvent::id); it != historyEdge()) {
promise.finish();
return true;
}
if (deadline.hasExpired()) {
qCWarning(MAIN) << "Timeout - giving up on obtaining event" << upToEventId;
promise.future().cancel();
return true;
}
return false;
});
if (!historyRequests.empty()) {
auto requestedIds =
dumpJoined(transform_view(historyRequests, &HistoryRequest::upToEventId));
if (allHistoryLoaded()) {
qCDebug(MAIN).noquote() << "Could not find in the whole room history:" << requestedIds;
for_each(historyRequests, [](auto& r) { r.promise.future().cancel(); });
historyRequests.clear();
}
static constexpr auto EventsProgression = std::array{ 50, 100, 200, 500, 1000 };
static_assert(is_sorted(EventsProgression));
const auto thisMany = requestedHistorySize() >= EventsProgression.back()
? EventsProgression.back()
: *upper_bound(EventsProgression, requestedHistorySize());
qCDebug(MAIN).noquote() << "Requesting" << thisMany << "events, looking for"
<< requestedIds;
getPreviousContent(thisMany);
}
}
31 changes: 30 additions & 1 deletion client/quaternionroom.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

#include <Quotient/room.h>

#include <QtCore/QDeadlineTimer>

class QuaternionRoom: public Quotient::Room
{
Q_OBJECT
Expand All @@ -28,7 +30,33 @@ class QuaternionRoom: public Quotient::Room

bool canRedact(const Quotient::EventId& eventId) const;

private:
using EventFuture = QFuture<void>;

//! \brief Loads the message history until the specified event id is found
//!
//! This is potentially heavy; use it sparingly. One intended use case is loading the timeline
//! until the last read event, assuming that the last read event is not too far back and that
//! the user will read or at least scroll through the just loaded events anyway. This will not
//! be necessary once we move to sliding sync but sliding sync support is still a bit away in
//! the future.
//!
//! Because the process is heavy (particularly on the homeserver), ensureHistory() will cancel
//! after \p maxWaitSeconds.
//! \return the future that resolves to the event with \p eventId, or self-cancels if the event
//! is not found
Q_INVOKABLE EventFuture ensureHistory(const QString& upToEventId, quint16 maxWaitSeconds = 20);

private:
using EventPromise = QPromise<void>;
using EventId = Quotient::EventId;

struct HistoryRequest {
EventId upToEventId;
QDeadlineTimer deadline;
EventPromise promise{};
};
std::vector<HistoryRequest> historyRequests;

QSet<const Quotient::RoomEvent*> highlights;
QString m_cachedUserFilter;
int m_requestedEventsCount = 0;
Expand All @@ -37,4 +65,5 @@ class QuaternionRoom: public Quotient::Room
void onAddHistoricalTimelineEvents(rev_iter_t from) override;

void checkForHighlights(const Quotient::TimelineItem& ti);
void checkForRequestedEvents(const rev_iter_t& from);
};
19 changes: 19 additions & 0 deletions client/timelinewidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,25 @@ void TimelineWidget::setGlobalSelectionBuffer(const QString& text)
m_selectedText = text;
}

void TimelineWidget::ensureLastReadEvent()
{
auto r = currentRoom();
if (!r)
return;
if (!historyRequest.isCanceled()) { // Second click cancels the request
historyRequest.cancel();
return;
}
// Store the future as is, without continuations, so that it could be cancelled
historyRequest = r->ensureHistory(r->lastFullyReadEventId());
historyRequest.then([this](auto) {
qCDebug(TIMELINE, "Loaded enough history to get the last fully read event, now scrolling");
emit viewPositionRequested(m_messageModel->findRow(currentRoom()->lastFullyReadEventId()));
});
}

bool TimelineWidget::isHistoryRequestRunning() const { return historyRequest.isRunning(); }

void TimelineWidget::reStartShownTimer()
{
if (!readMarkerOnScreen || indicesOnScreen.empty()
Expand Down
4 changes: 4 additions & 0 deletions client/timelinewidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class TimelineWidget : public QQuickWidget {
QString selectedText() const;
QuaternionRoom* currentRoom() const;
Q_INVOKABLE Qt::KeyboardModifiers getModifierKeys() const;
Q_INVOKABLE bool isHistoryRequestRunning() const;

signals:
void resourceRequested(const QString& idOrUri, const QString& action = {});
Expand All @@ -32,6 +33,7 @@ class TimelineWidget : public QQuickWidget {
void showDetails(int currentIndex);
void viewPositionRequested(int index);
void animateMessage(int currentIndex);
void historyRequestChanged();

public slots:
void setRoom(QuaternionRoom* room);
Expand All @@ -44,6 +46,7 @@ public slots:
const QString& selectedText, bool showingDetails);
void reactionButtonClicked(const QString& eventId, const QString& key);
void setGlobalSelectionBuffer(const QString& text);
void ensureLastReadEvent();

private:
MessageEventModel* m_messageModel;
Expand All @@ -55,6 +58,7 @@ public slots:
QBasicTimer maybeReadTimer;
bool readMarkerOnScreen;
ActivityDetector activityDetector;
QFuture<void> historyRequest;

class NamFactory : public QQmlNetworkAccessManagerFactory {
public:
Expand Down

0 comments on commit ce19b02

Please sign in to comment.