aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/room.cpp330
1 files changed, 177 insertions, 153 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index 85eadd67..92768aff 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -31,6 +31,7 @@
#include "csapi/kicking.h"
#include "csapi/leaving.h"
#include "csapi/receipts.h"
+#include "csapi/read_markers.h"
#include "csapi/redaction.h"
#include "csapi/room_send.h"
#include "csapi/room_state.h"
@@ -55,7 +56,6 @@
#include "events/roompowerlevelsevent.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
-#include "jobs/postreadmarkersjob.h"
#include "events/roomcanonicalaliasevent.h"
#include <QtCore/QDir>
@@ -135,7 +135,7 @@ public:
QString firstDisplayedEventId;
QString lastDisplayedEventId;
QHash<const User*, QString> lastReadEventIds;
- QString serverReadMarker;
+ QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
@@ -311,11 +311,12 @@ public:
*/
void dropDuplicateEvents(RoomEvents& events) const;
- Changes setLastReadEvent(User* u, QString eventId);
- void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
- Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false);
-
- Changes markMessagesAsRead(rev_iter_t upToMarker);
+ void setLastReadReceipt(User* u, rev_iter_t newMarker,
+ QString newEvtId = {});
+ Changes setFullyReadMarker(const QString &eventId);
+ Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
+ Changes recalculateUnreadCount();
+ void markMessagesAsRead(const rev_iter_t &upToMarker);
void getAllMembers();
@@ -637,139 +638,162 @@ void Room::setJoinState(JoinState state)
emit joinStateChanged(oldState, state);
}
-Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
+void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker,
+ QString newEvtId)
{
+ if (!u) {
+ Q_ASSERT(u != nullptr); // For Debug builds
+ qCCritical(MAIN) << "Empty user, skipping read receipt registration";
+ return; // For Release builds
+ }
+ if (q->memberJoinState(u) != JoinState::Join) {
+ qCWarning(MAIN) << "Won't record read receipt for non-member" << u->id();
+ return;
+ }
+
+ if (newMarker == timeline.crend() && !newEvtId.isEmpty())
+ newMarker = q->findInTimeline(newEvtId);
+ if (newMarker != timeline.crend()) {
+ // NB: with reverse iterators, timeline history >= sync edge
+ if (newMarker >= q->readMarker(u))
+ return;
+
+ // Try to auto-promote the read marker over the user's own messages
+ // (switch to direct iterators for that).
+ const auto eagerMarker = find_if(newMarker.base(), timeline.cend(),
+ [=](const TimelineItem& ti) {
+ return ti->senderId() != u->id();
+ })
+ - 1;
+ newEvtId = (*eagerMarker)->id();
+ if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt
+ qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id()
+ << "to" << newEvtId;
+ }
+
auto& storedId = lastReadEventIds[u];
- if (storedId == eventId)
- return Change::NoChange;
+ if (storedId == newEvtId)
+ return;
+ // Finally make the change
eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(eventId, u);
- swap(storedId, eventId);
+ eventIdReadUsers.insert(newEvtId, u);
+ swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId
emit q->lastReadEventChanged(u);
- emit q->readMarkerForUserMoved(u, eventId, storedId);
- if (isLocalUser(u)) {
- if (storedId != serverReadMarker)
- connection->callApi<PostReadMarkersJob>(BackgroundRequest, id,
- storedId);
- emit q->readMarkerMoved(eventId, storedId);
- return Change::ReadMarkerChange;
- }
- return Change::NoChange;
+ if (!isLocalUser(u))
+ emit q->readMarkerForUserMoved(u, newEvtId, storedId);
}
-void Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from,
+ const rev_iter_t& to)
{
Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
Q_ASSERT(to >= from && to <= timeline.crend());
- // Catch a special case when the last read event id refers to an event
- // that has just arrived. In this case we should recalculate
- // unreadMessages and might need to promote the read marker further
- // over local-origin messages.
- auto readMarker = q->readMarker();
- if (readMarker == timeline.crend() && q->allHistoryLoaded())
- --readMarker; // Read marker not found in the timeline, initialise it
- if (readMarker >= from && readMarker < to) {
- promoteReadMarker(q->localUser(), readMarker, true);
- return;
- }
-
- Q_ASSERT(to <= readMarker);
+ auto fullyReadMarker = q->readMarker();
+ if (fullyReadMarker <= from)
+ return NoChange; // What's arrived is already fully read
+
+ if (fullyReadMarker == timeline.crend() && q->allHistoryLoaded())
+ --fullyReadMarker; // No read marker in the whole room, initialise it
+ if (fullyReadMarker < to) {
+ // Catch a special case when the last fully read event id refers to an
+ // event that has just arrived. In this case we should recalculate
+ // unreadMessages to get an exact number instead of an estimation
+ // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
+ // For the same reason (switching from the estimation to the exact
+ // number) this branch always returns UnreadNotifsChange, even if
+ // the estimation luckily matched the exact result.
+ recalculateUnreadCount();
+ return UnreadNotifsChange;
+ }
+
+ // Fully read marker is somewhere beyond the most historical message from
+ // the arrived batch - add up newly arrived messages to the current counter,
+ // instead of a complete recalculation.
+ Q_ASSERT(to <= fullyReadMarker);
QElapsedTimer et;
et.start();
const auto newUnreadMessages =
- count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1));
+ count_if(from, to,
+ std::bind(&Room::Private::isEventNotable, this, _1));
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Counting gained unread messages took" << et;
- if (newUnreadMessages > 0) {
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages < 0)
- unreadMessages = 0;
-
- unreadMessages += newUnreadMessages;
- qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
- << newUnreadMessages << "unread message(s),"
- << (q->readMarker() == timeline.crend()
- ? "in total at least"
- : "in total")
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- }
+ if (newUnreadMessages == 0)
+ return NoChange;
+
+ // See https://github.com/quotient-im/libQuotient/wiki/unread_count
+ if (unreadMessages < 0)
+ unreadMessages = 0;
+
+ unreadMessages += newUnreadMessages;
+ qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
+ << newUnreadMessages << "unread message(s),"
+ << (q->readMarker() == timeline.crend()
+ ? "in total at least"
+ : "in total")
+ << unreadMessages << "unread message(s)";
+ emit q->unreadMessagesChanged(q);
+ return UnreadNotifsChange;
}
-Room::Changes Room::Private::promoteReadMarker(User* u,
- const rev_iter_t& newMarker,
- bool force)
+Room::Changes Room::Private::recalculateUnreadCount()
{
- Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
- Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
+ // Recalculate unread messages
+ const auto oldUnreadCount = unreadMessages;
+ QElapsedTimer et;
+ et.start();
+ unreadMessages =
+ int(count_if(timeline.crbegin(), q->readMarker(),
+ [this](const auto& ti) { return isEventNotable(ti); }));
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(PROFILER) << "Recounting unread messages took" << et;
- const auto prevMarker = q->readMarker(u);
- if (!force && prevMarker <= newMarker) // Remember, we deal with reverse
- // iterators
- return Change::NoChange;
+ // See https://github.com/quotient-im/libQuotient/wiki/unread_count
+ if (unreadMessages == 0)
+ unreadMessages = -1;
- Q_ASSERT(newMarker < timeline.crend());
+ if (unreadMessages == oldUnreadCount)
+ return NoChange;
- // Try to auto-promote the read marker over the user's own messages
- // (switch to direct iterators for that).
- auto eagerMarker =
- find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) {
- return ti->senderId() != u->id();
- });
+ if (unreadMessages == -1)
+ qCDebug(MESSAGES)
+ << "Room" << displayname << "has no more unread messages";
+ else
+ qCDebug(MESSAGES) << "Room" << displayname << "still has"
+ << unreadMessages << "unread message(s)";
+ emit q->unreadMessagesChanged(q);
+ return UnreadNotifsChange;
+}
- auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
- if (isLocalUser(u)) {
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(eagerMarker, timeline.cend(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages took" << et;
-
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
-
- if (force || unreadMessages != oldUnreadCount) {
- if (unreadMessages == -1) {
- qCDebug(MESSAGES)
- << "Room" << displayname << "has no more unread messages";
- } else
- qCDebug(MESSAGES) << "Room" << displayname << "still has"
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- changes |= Change::UnreadNotifsChange;
- }
+Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
+{
+ if (fullyReadUntilEventId == eventId)
+ return NoChange;
+
+ const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
+ qCDebug(MESSAGES) << "Fully read marker moved to" << fullyReadUntilEventId;
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
+
+ Changes changes = ReadMarkerChange;
+ if (const auto rm = q->readMarker(); rm != timeline.crend()) {
+ // Pull read receipt if it's behind
+ if (auto rr = q->readMarker(q->localUser()); rr > rm)
+ setLastReadReceipt(q->localUser(), rm);
+
+ changes |= recalculateUnreadCount();
}
return changes;
}
-Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
{
- const auto prevMarker = q->readMarker();
- auto changes = promoteReadMarker(q->localUser(), upToMarker);
- if (prevMarker != upToMarker)
- qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker();
-
- // We shouldn't send read receipts for the local user's own messages - so
- // search earlier messages for the latest message not from the local user
- // until the previous last-read message, whichever comes first.
- for (; upToMarker < prevMarker; ++upToMarker) {
- if ((*upToMarker)->senderId() != q->localUser()->id()) {
- connection->callApi<PostReceiptJob>(BackgroundRequest,
- id, QStringLiteral("m.read"),
- QUrl::toPercentEncoding(
- (*upToMarker)->id()));
- break;
- }
+ if (upToMarker < q->readMarker()) {
+ setFullyReadMarker((*upToMarker)->id());
+ connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
+ fullyReadUntilEventId);
}
- return changes;
}
void Room::markMessagesAsRead(QString uptoEventId)
@@ -924,6 +948,11 @@ void Room::setFirstDisplayedEventId(const QString& eventId)
if (d->firstDisplayedEventId == eventId)
return;
+ if (findInTimeline(eventId) == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as first displayed but doesn't seem to be loaded";
+
d->firstDisplayedEventId = eventId;
emit firstDisplayedEventChanged();
}
@@ -946,8 +975,18 @@ void Room::setLastDisplayedEventId(const QString& eventId)
if (d->lastDisplayedEventId == eventId)
return;
+ const auto marker = findInTimeline(eventId);
+ if (marker == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as last displayed but doesn't seem to be loaded";
+
d->lastDisplayedEventId = eventId;
emit lastDisplayedEventChanged();
+ if (d->displayed && marker < readMarker(localUser()))
+ connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
+ QStringLiteral("m.read"),
+ QUrl::toPercentEncoding(eventId));
}
void Room::setLastDisplayedEvent(TimelineItem::index_t index)
@@ -962,11 +1001,14 @@ Room::rev_iter_t Room::readMarker(const User* user) const
return findInTimeline(d->lastReadEventIds.value(user));
}
-Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); }
+Room::rev_iter_t Room::readMarker() const
+{
+ return findInTimeline(d->fullyReadUntilEventId);
+}
QString Room::readMarkerEventId() const
{
- return d->lastReadEventIds.value(localUser());
+ return d->fullyReadUntilEventId;
}
QList<User*> Room::usersAtEventId(const QString& eventId)
@@ -2347,22 +2389,15 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// The first event in the just-added batch (referred to by `from`)
// defines whose read marker can possibly be promoted any further over
// the same author's events newly arrived. Others will need explicit
- // read receipts from the server (or, for the local user,
- // markMessagesAsRead() invocation) to promote their read markers over
+ // read receipts from the server - or, for the local user, calling
+ // setLastDisplayedEventId() - to promote their read receipts over
// the new message events.
- if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) {
- auto* const firstWriter = q->user(senderId);
- if (firstWriter && q->readMarker(firstWriter) != timeline.crend()) {
- roomChanges |=
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
- qCDebug(MESSAGES)
- << "Auto-promoted read marker for" << senderId
- << "to" << *q->readMarker(firstWriter);
- }
+ if (auto* const firstWriter = q->user((*from)->senderId())) {
+ const auto firstEventId = (*from)->id();
+ if (lastReadEventIds.value(firstWriter) == firstEventId)
+ setLastReadReceipt(firstWriter, rev_iter_t(from + 1));
}
-
- updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
- roomChanges |= Change::UnreadNotifsChange;
+ roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
@@ -2407,8 +2442,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
emit q->updatedEvent(relation.eventId);
}
}
- if (from <= q->readMarker())
- updateUnreadCount(from, timeline.crend());
+ updateUnreadCount(from, timeline.crend());
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
@@ -2612,7 +2646,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
d->usersTyping.clear();
for (const QString& userId : qAsConst(evt->users())) {
auto* const u = user(userId);
- if (u && memberJoinState(u) == JoinState::Join)
+ if (memberJoinState(u) == JoinState::Join)
d->usersTyping.append(u);
}
if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
@@ -2633,25 +2667,21 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
<< p.receipts.size() << "users";
}
const auto newMarker = findInTimeline(p.evtId);
- if (newMarker != timelineEdge())
- qCDebug(EPHEMERAL) << "Event" << p.evtId
- << "not found; saving read receipts anyway";
- for (const Receipt& r : p.receipts) {
- auto* const u = user(r.userId);
- if (u == localUser())
- continue; // The local user has m.fully_read but FIXME: #464
- if (u && memberJoinState(u) == JoinState::Join) {
+ if (newMarker == historyEdge())
+ qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not "
+ "found; saving them anyway";
+ for (const Receipt& r : p.receipts)
+ if (auto* const u = user(r.userId);
+ memberJoinState(u) == JoinState::Join) {
// If the event is not found (most likely, because it's
// too old and hasn't been fetched from the server yet)
// but there is a previous marker for a user, keep
- // the previous marker (XXX: why??). Otherwise, blindly
- // store the event id for this user.
- if (newMarker != timelineEdge())
- changes |= d->promoteReadMarker(u, newMarker);
- else if (readMarker(u) == timelineEdge())
- changes |= d->setLastReadEvent(u, p.evtId);
+ // the previous marker because read receipts are not
+ // supposed to move backwards. Otherwise, blindly
+ // store the event id for this user and update the read
+ // marker when/if the event is fetched later on.
+ d->setLastReadReceipt(u, newMarker, p.evtId);
}
- }
}
if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10
|| et.nsecsElapsed() >= profilerMinNsecs())
@@ -2671,15 +2701,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
changes |= Change::TagsChange;
}
- if (auto* evt = eventCast<ReadMarkerEvent>(event)) {
- auto readEventId = evt->event_id();
- qCDebug(STATE) << "Server-side read marker at" << readEventId;
- d->serverReadMarker = readEventId;
- const auto newMarker = findInTimeline(readEventId);
- changes |= newMarker != timelineEdge()
- ? d->markMessagesAsRead(newMarker)
- : d->setLastReadEvent(localUser(), readEventId);
- }
+ if (auto* evt = eventCast<const ReadMarkerEvent>(event))
+ changes |= d->setFullyReadMarker(evt->event_id());
+
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
// A polymorphic event-specific comparison might be a bit more