aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp859
1 files changed, 547 insertions, 312 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index 7e0806a7..fac24e5e 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -15,6 +15,7 @@
#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
+#include "eventstats.h"
// NB: since Qt 6, moc_room.cpp needs User fully defined
#include "moc_room.cpp"
@@ -54,7 +55,6 @@
#include <QtCore/QDir>
#include <QtCore/QHash>
-#include <QtCore/QMimeDatabase>
#include <QtCore/QPointer>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
@@ -117,19 +117,21 @@ public:
QHash<QPair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
- int highlightCount = 0;
- int notificationCount = 0;
+ QHash<QString, Notification> notifications;
+ qsizetype serverHighlightCount = 0;
+ // Starting up with estimate event statistics as there's zero knowledge
+ // about the timeline.
+ EventStats partiallyReadStats {}, unreadStats {};
members_map_t membersMap;
QList<User*> usersTyping;
- QMultiHash<QString, User*> eventIdReadUsers;
+ QHash<QString, QSet<QString>> eventIdReadUsers;
QList<User*> usersInvited;
QList<User*> membersLeft;
- int unreadMessages = 0;
bool displayed = false;
QString firstDisplayedEventId;
QString lastDisplayedEventId;
- QHash<const User*, QString> lastReadEventIds;
- QString serverReadMarker;
+ QHash<QString, ReadReceipt> lastReadReceipts;
+ QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
@@ -253,39 +255,34 @@ public:
// return EventT::content_type()
// }
- bool isEventNotable(const TimelineItem& ti) const
- {
- return !ti->isRedacted() && ti->senderId() != connection->userId()
- && is<RoomMessageEvent>(*ti)
- && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty();
- }
-
template <typename EventArrayT>
Changes updateStateFrom(EventArrayT&& events)
{
- Changes changes = NoChange;
+ Changes changes {};
if (!events.empty()) {
QElapsedTimer et;
et.start();
for (auto&& eptr : events) {
const auto& evt = *eptr;
Q_ASSERT(evt.isStateEvent());
- auto change = q->processStateEvent(evt);
- if (change != NoChange) {
+ if (auto change = q->processStateEvent(evt); change) {
changes |= change;
baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
}
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
- << "*** Room::Private::updateStateFrom():" << events.size()
- << "event(s)," << et;
+ << "Updated" << q->objectName() << "room state from"
+ << events.size() << "event(s) in" << et;
}
return changes;
}
Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
+ Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache);
+ void postprocessChanges(Changes changes, bool saveState = true);
+
/** Move events into the timeline
*
* Insert events into the timeline, either new or historical.
@@ -303,11 +300,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);
+ Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
+ ReadReceipt newReceipt = {},
+ bool deferStatsUpdate = false);
+ Changes setFullyReadMarker(const QString &eventId);
+ Changes updateStats(const rev_iter_t& from, const rev_iter_t& to);
+ bool markMessagesAsRead(const rev_iter_t& upToMarker);
void getAllMembers();
@@ -473,7 +471,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
- qCDebug(STATE) << "New" << initialJoinState << "Room:" << id;
+ qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -584,13 +582,13 @@ QImage Room::avatar(int width, int height)
{
if (!d->avatar.url().isEmpty())
return d->avatar.get(connection(), width, height,
- [=] { emit avatarChanged(); });
+ [this] { emit avatarChanged(); });
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (auto* u : dcUsers)
if (u != localUser())
- return u->avatar(width, height, this, [=] { emit avatarChanged(); });
+ return u->avatar(width, height, this, [this] { emit avatarChanged(); });
return {};
}
@@ -624,156 +622,261 @@ void Room::setJoinState(JoinState state)
if (state == oldState)
return;
d->joinState = state;
- qCDebug(STATE) << "Room" << id() << "changed state: " << oldState
+ qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState
<< "->" << state;
- emit changed(Change::JoinStateChange);
emit joinStateChanged(oldState, state);
}
-Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
-{
- auto& storedId = lastReadEventIds[u];
- if (storedId == eventId)
- return Change::NoChange;
- eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(eventId, u);
- swap(storedId, eventId);
- emit q->lastReadEventChanged(u);
- emit q->readMarkerForUserMoved(u, eventId, storedId);
- if (isLocalUser(u)) {
- if (storedId != serverReadMarker)
- connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
- storedId);
- emit q->readMarkerMoved(eventId, storedId);
- return Change::ReadMarkerChange;
+Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt,
+ bool deferStatsUpdate)
+{
+ if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
+ newMarker = q->findInTimeline(newReceipt.eventId);
+ if (newMarker != historyEdge()) {
+ // 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(), syncEdge(),
+ [=](const TimelineItem& ti) {
+ return ti->senderId() != userId;
+ });
+ // eagerMarker is now just after the desired event for newMarker
+ if (eagerMarker != newMarker.base()) {
+ newMarker = rev_iter_t(eagerMarker);
+ qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
+ << "to" << *newMarker;
+ }
+ // Fill newReceipt with the event (and, if needed, timestamp) from
+ // eagerMarker
+ newReceipt.eventId = (eagerMarker - 1)->event()->id();
+ if (newReceipt.timestamp.isNull())
+ newReceipt.timestamp = QDateTime::currentDateTime();
+ }
+ auto& storedReceipt =
+ lastReadReceipts[userId]; // clazy:exclude=detaching-member
+ const auto prevEventId = storedReceipt.eventId;
+ // Check that either the new marker is actually "newer" than the current one
+ // or, if both markers are at historyEdge(), event ids are different.
+ // NB: with reverse iterators, timeline history edge >= sync edge
+ if (prevEventId == newReceipt.eventId
+ || newMarker > q->findInTimeline(prevEventId))
+ return Change::None;
+
+ // Finally make the change
+
+ Changes changes = Change::Other;
+ auto oldEventReadUsersIt =
+ eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member
+ if (oldEventReadUsersIt != eventIdReadUsers.end()) {
+ oldEventReadUsersIt->remove(userId);
+ if (oldEventReadUsersIt->isEmpty())
+ eventIdReadUsers.erase(oldEventReadUsersIt);
+ }
+ eventIdReadUsers[newReceipt.eventId].insert(userId);
+ storedReceipt = move(newReceipt);
+
+ {
+ auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug
+ dbg << "The new read receipt for" << userId << "is now at";
+ if (newMarker == historyEdge())
+ dbg << storedReceipt.eventId;
+ else
+ dbg << *newMarker;
}
- return Change::NoChange;
+
+ // TODO: use Room::member() when it becomes a thing and only emit signals
+ // for actual members, not just any user
+ const auto member = q->user(userId);
+ Q_ASSERT(member != nullptr);
+ if (isLocalUser(member) && !deferStatsUpdate) {
+ if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId),
+ newMarker)) {
+ qCDebug(MESSAGES)
+ << "Updated unread event statistics in" << q->objectName()
+ << "after moving the local read receipt:" << unreadStats;
+ changes |= Change::UnreadStats;
+ }
+ Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check
+ }
+ emit q->lastReadEventChanged(member);
+ // TODO: remove in 0.8
+ if (!isLocalUser(member))
+ emit q->readMarkerForUserMoved(member, prevEventId,
+ storedReceipt.eventId);
+ return changes;
}
-void Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateStats(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 == historyEdge() && q->allHistoryLoaded())
- --readMarker; // Read marker not found in the timeline, initialise it
- if (readMarker >= from && readMarker < to) {
- promoteReadMarker(q->localUser(), readMarker, true);
- return;
+ const auto fullyReadMarker = q->fullyReadMarker();
+ auto readReceiptMarker = q->localReadReceiptMarker();
+ Changes changes = Change::None;
+ // Correct the read receipt to never be behind the fully read marker
+ if (readReceiptMarker > fullyReadMarker
+ && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) {
+ changes |= Change::Other;
+ readReceiptMarker = q->localReadReceiptMarker();
+ qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read "
+ "marker - it's now corrected to be at index"
+ << readReceiptMarker->index();
+ }
+
+ if (fullyReadMarker < from)
+ return Change::None; // What's arrived is already fully read
+
+ // If there's no read marker in the whole room, initialise it
+ if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
+ return setFullyReadMarker(timeline.front()->id());
+
+ // Catch a case when the id in the last fully read marker or the local read
+ // receipt refers to an event that has just arrived. In this case either
+ // one (unreadStats) or both statistics should be recalculated to get
+ // an exact number instead of an estimation (see documentation on
+ // EventStats::isEstimate). For the same reason (switching from the
+ // estimate to the exact number) this branch forces returning
+ // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if
+ // the estimation luckily matched the exact result.
+ if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) {
+ unreadStats = EventStats::fromMarker(q, readReceiptMarker);
+ Q_ASSERT(!unreadStats.isEstimate);
+ qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in"
+ << q->objectName() << ": " << unreadStats;
+ changes |= Change::UnreadStats;
+ if (fullyReadMarker < to) {
+ // Add up to unreadStats instead of counting same events again
+ partiallyReadStats = EventStats::fromRange(q, readReceiptMarker,
+ q->fullyReadMarker(),
+ unreadStats);
+ Q_ASSERT(!partiallyReadStats.isEstimate);
+
+ qCDebug(MESSAGES).nospace()
+ << "Recalculated partially read event statistics in "
+ << q->objectName() << ": " << partiallyReadStats;
+ return changes | Change::PartiallyReadStats;
+ }
}
- Q_ASSERT(to <= readMarker);
-
- QElapsedTimer et;
- et.start();
- const auto newUnreadMessages =
- 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);
- }
-}
-
-Room::Changes Room::Private::promoteReadMarker(User* u,
- const rev_iter_t& newMarker,
- bool force)
-{
- Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
- Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
-
- const auto prevMarker = q->readMarker(u);
- if (!force && prevMarker <= newMarker) // Remember, we deal with reverse
- // iterators
- return Change::NoChange;
-
- Q_ASSERT(newMarker < historyEdge());
-
- // 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();
- });
+ // As of here, at least the fully read marker (but maybe also read receipt)
+ // points to somewhere beyond the "oldest" message from the arrived batch -
+ // add up newly arrived messages to the current stats, instead of a complete
+ // recalculation.
+ Q_ASSERT(fullyReadMarker >= to);
- auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
- if (isLocalUser(u)) {
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(eagerMarker, syncEdge(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages took" << et;
+ const auto newStats = EventStats::fromRange(q, from, to);
+ Q_ASSERT(!newStats.isEstimate);
+ if (newStats.empty())
+ return changes;
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
+ const auto doAddStats = [this, &changes, newStats](EventStats& s,
+ const rev_iter_t& marker,
+ Change c) {
+ s.notableCount += newStats.notableCount;
+ s.highlightCount += newStats.highlightCount;
+ if (!s.isEstimate)
+ s.isEstimate = marker == historyEdge();
+ changes |= c;
+ };
- 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;
- }
+ doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats);
+ if (readReceiptMarker >= to) {
+ // readReceiptMarker < to branch shouldn't have been entered
+ Q_ASSERT(!changes.testFlag(Change::UnreadStats));
+ doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats);
}
+ qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats
+ << "notable/highlighted event(s); total statistics:"
+ << partiallyReadStats << "since the fully read marker,"
+ << unreadStats << "since read receipt";
+
+ // Check invariants
+ Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker));
+ Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker));
return changes;
}
-Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
{
- const auto prevMarker = q->readMarker();
- auto changes = promoteReadMarker(q->localUser(), upToMarker);
- if (prevMarker != upToMarker)
- qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker();
+ if (fullyReadUntilEventId == eventId)
+ return Change::None;
- // 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;
+ const auto prevReadMarker = q->fullyReadMarker();
+ const auto newReadMarker = q->findInTimeline(eventId);
+ if (newReadMarker > prevReadMarker)
+ return Change::None;
+
+ const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
+ qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
+ << "set to" << fullyReadUntilEventId;
+
+ QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;)
+ if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) {
+ // Pull read receipt if it's behind, and update statistics
+ changes |= setLastReadReceipt(connection->userId(), rm);
+ if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) {
+ changes |= Change::PartiallyReadStats;
+ qCDebug(MESSAGES)
+ << "Updated partially read event statistics in"
+ << q->objectName()
+ << "after moving m.fully_read marker: " << partiallyReadStats;
}
+ Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check
}
+ emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
+ // TODO: Remove in 0.8
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
return changes;
}
-void Room::markMessagesAsRead(QString uptoEventId)
+void Room::setReadReceipt(const QString& atEventId)
+{
+ if (const auto changes = d->setLastReadReceipt(localUser()->id(),
+ historyEdge(),
+ { atEventId })) {
+ connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
+ QStringLiteral("m.read"),
+ QUrl::toPercentEncoding(atEventId));
+ d->postprocessChanges(changes);
+ } else
+ qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id()
+ << "in" << objectName()
+ << "is at or behind the old one, skipping";
+}
+
+bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+{
+ if (upToMarker == q->historyEdge())
+ qCWarning(MESSAGES) << "Cannot mark an unknown event in"
+ << q->objectName() << "as fully read";
+ else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) {
+ // The assumption below is that if a read receipt was sent on a newer
+ // event, the homeserver will keep it there instead of reverting to
+ // m.fully_read
+ connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
+ fullyReadUntilEventId,
+ fullyReadUntilEventId);
+ postprocessChanges(changes);
+ return true;
+ } else
+ qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName()
+ << "is behind the current fully read marker at"
+ << *q->fullyReadMarker()
+ << "- won't move fully read marker back in timeline";
+ return false;
+}
+
+void Room::markMessagesAsRead(const QString& uptoEventId)
{
d->markMessagesAsRead(findInTimeline(uptoEventId));
}
void Room::markAllMessagesAsRead()
{
- if (!d->timeline.empty())
- d->markMessagesAsRead(d->timeline.crbegin());
+ d->markMessagesAsRead(d->timeline.crbegin());
}
bool Room::canSwitchVersions() const
@@ -790,9 +893,40 @@ bool Room::canSwitchVersions() const
return true;
}
-bool Room::hasUnreadMessages() const { return unreadCount() >= 0; }
+bool Room::isEventNotable(const TimelineItem &ti) const
+{
+ const auto& evt = *ti;
+ const auto* rme = ti.viewAs<RoomMessageEvent>();
+ return !evt.isRedacted()
+ && (is<RoomTopicEvent>(evt) || is<RoomNameEvent>(evt)
+ || is<RoomAvatarEvent>(evt) || is<RoomTombstoneEvent>(evt)
+ || (rme && rme->msgtype() != MessageEventType::Notice
+ && rme->replacedEvent().isEmpty()))
+ && evt.senderId() != localUser()->id();
+}
-int Room::unreadCount() const { return d->unreadMessages; }
+Notification Room::notificationFor(const TimelineItem &ti) const
+{
+ return d->notifications.value(ti->id());
+}
+
+Notification Room::checkForNotifications(const TimelineItem &ti)
+{
+ return { Notification::None };
+}
+
+bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); }
+
+int countFromStats(const EventStats& s)
+{
+ return s.empty() ? -1 : int(s.notableCount);
+}
+
+int Room::unreadCount() const { return countFromStats(partiallyReadStats()); }
+
+EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; }
+
+EventStats Room::unreadStats() const { return d->unreadStats; }
Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); }
@@ -868,7 +1002,7 @@ void Room::Private::getAllMembers()
allMembersJob = connection->callApi<GetMembersByRoomJob>(
id, connection->nextBatchToken(), "join");
auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
- connect(allMembersJob, &BaseJob::success, q, [=] {
+ connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] {
Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
auto roomChanges = updateStateFrom(allMembersJob->chunk());
// Replay member events that arrived after the point for which
@@ -878,8 +1012,7 @@ void Room::Private::getAllMembers()
it != syncEdge(); ++it)
if (is<RoomMemberEvent>(**it))
roomChanges |= q->processStateEvent(**it);
- if (roomChanges & MembersChange)
- emit q->memberListChanged();
+ postprocessChanges(roomChanges);
emit q->allMembersLoaded();
});
}
@@ -893,11 +1026,8 @@ void Room::setDisplayed(bool displayed)
d->displayed = displayed;
emit displayedChanged(displayed);
- if (displayed) {
- resetHighlightCount();
- resetNotificationCount();
+ if (displayed)
d->getAllMembers();
- }
}
QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; }
@@ -958,38 +1088,70 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index)
Room::rev_iter_t Room::readMarker(const User* user) const
{
Q_ASSERT(user);
- return findInTimeline(d->lastReadEventIds.value(user));
+ return findInTimeline(lastReadReceipt(user->id()).eventId);
+}
+
+Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); }
+
+QString Room::readMarkerEventId() const { return lastFullyReadEventId(); }
+
+ReadReceipt Room::lastReadReceipt(const QString& userId) const
+{
+ return d->lastReadReceipts.value(userId);
}
-Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); }
+ReadReceipt Room::lastLocalReadReceipt() const
+{
+ return d->lastReadReceipts.value(localUser()->id());
+}
-QString Room::readMarkerEventId() const
+Room::rev_iter_t Room::localReadReceiptMarker() const
{
- return d->lastReadEventIds.value(localUser());
+ return findInTimeline(lastLocalReadReceipt().eventId);
}
-QList<User*> Room::usersAtEventId(const QString& eventId)
+QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; }
+
+Room::rev_iter_t Room::fullyReadMarker() const
{
- return d->eventIdReadUsers.values(eventId);
+ return findInTimeline(d->fullyReadUntilEventId);
}
-int Room::notificationCount() const { return d->notificationCount; }
+QSet<QString> Room::userIdsAtEvent(const QString& eventId)
+{
+ return d->eventIdReadUsers.value(eventId);
+}
+
+QSet<User*> Room::usersAtEventId(const QString& eventId)
+{
+ const auto& userIds = d->eventIdReadUsers.value(eventId);
+ QSet<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& uId : userIds)
+ users.insert(user(uId));
+ return users;
+}
+
+qsizetype Room::notificationCount() const
+{
+ return d->unreadStats.notableCount;
+}
void Room::resetNotificationCount()
{
- if (d->notificationCount == 0)
+ if (d->unreadStats.notableCount == 0)
return;
- d->notificationCount = 0;
+ d->unreadStats.notableCount = 0;
emit notificationCountChanged();
}
-int Room::highlightCount() const { return d->highlightCount; }
+qsizetype Room::highlightCount() const { return d->serverHighlightCount; }
void Room::resetHighlightCount()
{
- if (d->highlightCount == 0)
+ if (d->serverHighlightCount == 0)
return;
- d->highlightCount = 0;
+ d->serverHighlightCount = 0;
emit highlightCountChanged();
}
@@ -1378,11 +1540,10 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; }
Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
{
if (!summary.merge(newSummary))
- return Change::NoChange;
+ return Change::None;
qCDebug(STATE).nospace().noquote()
<< "Updated room summary for " << q->objectName() << ": " << summary;
- emit q->memberListChanged();
- return Change::SummaryChange;
+ return Change::Summary;
}
void Room::Private::insertMemberIntoMap(User* u)
@@ -1460,7 +1621,8 @@ void Room::Private::removeMemberFromMap(User* u)
inline auto makeErrorStr(const Event& e, QByteArray msg)
{
- return msg.append("; event dump follows:\n").append(e.originalJson());
+ return msg.append("; event dump follows:\n")
+ .append(QJsonDocument(e.fullJson()).toJson());
}
Room::Timeline::size_type
@@ -1486,11 +1648,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
!eventsIndex.contains(eId), __FUNCTION__,
makeErrorStr(*e, "Event is already in the timeline; "
"incoming events were not properly deduplicated"));
- if (placement == Older)
- timeline.emplace_front(move(e), --index);
- else
- timeline.emplace_back(move(e), ++index);
+ const auto& ti = placement == Older
+ ? timeline.emplace_front(move(e), --index)
+ : timeline.emplace_back(move(e), ++index);
eventsIndex.insert(eId, index);
+ if (auto n = q->checkForNotifications(ti); n.type != Notification::None)
+ notifications.insert(e->id(), n);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
const auto insertedSize = (index - baseIndex) * placement;
@@ -1565,61 +1728,132 @@ QUrl Room::memberAvatarUrl(const QString &mxId) const
: QUrl();
}
+Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
+ bool fromCache)
+{
+ Changes changes {};
+ if (fromCache) {
+ // Initial load of cached statistics
+ partiallyReadStats =
+ EventStats::fromCachedCounters(data.partiallyReadCount);
+ unreadStats = EventStats::fromCachedCounters(data.unreadCount,
+ data.highlightCount);
+ // Migrate from lib 0.6: -1 in the old unread counter overrides 0
+ // (which loads to an estimate) in notification_count. Next caching will
+ // save -1 in both places, completing the migration.
+ if (data.unreadCount == 0 && data.partiallyReadCount == -1)
+ unreadStats.isEstimate = false;
+ changes |= Change::PartiallyReadStats | Change::UnreadStats;
+ qCDebug(MESSAGES) << "Loaded" << q->objectName()
+ << "event statistics from cache:" << partiallyReadStats
+ << "since m.fully_read," << unreadStats
+ << "since m.read";
+ } else if (timeline.empty()) {
+ // In absence of actual events use statistics from the homeserver
+ if (merge(unreadStats.notableCount, data.unreadCount))
+ changes |= Change::PartiallyReadStats;
+ if (merge(unreadStats.highlightCount, data.highlightCount))
+ changes |= Change::UnreadStats;
+ unreadStats.isEstimate = !data.unreadCount.has_value()
+ || *data.unreadCount > 0;
+ qCDebug(MESSAGES)
+ << "Using server-side unread event statistics while the"
+ << q->objectName() << "timeline is empty:" << unreadStats;
+ }
+ bool correctedStats = false;
+ if (unreadStats.highlightCount > partiallyReadStats.highlightCount) {
+ correctedStats = true;
+ partiallyReadStats.highlightCount = unreadStats.highlightCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (unreadStats.notableCount > partiallyReadStats.notableCount) {
+ correctedStats = true;
+ partiallyReadStats.notableCount = unreadStats.notableCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) {
+ correctedStats = true;
+ partiallyReadStats.isEstimate = true;
+ }
+ if (correctedStats)
+ qCDebug(MESSAGES) << "Partially read event statistics in"
+ << q->objectName() << "were adjusted to"
+ << partiallyReadStats
+ << "to be consistent with the m.read receipt";
+ Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker()));
+ Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker()));
+
+ // TODO: Once the library learns to count highlights, drop
+ // serverHighlightCount and only use the server-side counter when
+ // the timeline is empty (see the code above).
+ if (merge(serverHighlightCount, data.highlightCount)) {
+ qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName()
+ << "to" << serverHighlightCount;
+ changes |= Change::Highlights;
+ }
+ return changes;
+}
+
void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if (d->prevBatch.isEmpty())
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
- Changes roomChanges = Change::NoChange;
- QElapsedTimer et;
- et.start();
+ Changes roomChanges {};
+ // The order of calculation is important - don't merge the lines!
+ roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->setSummary(move(data.summary));
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
+
+ for (auto&& ephemeralEvent : data.ephemeral)
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+
for (auto&& event : data.accountData)
roomChanges |= processAccountDataEvent(move(event));
- roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->updateStatsFromSyncData(data, fromCache);
- if (!data.timeline.empty()) {
- et.restart();
- roomChanges |= d->addNewMessageEvents(move(data.timeline));
- if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::addNewMessageEvents():" << data.timeline.size()
- << "event(s)," << et;
- }
- if (roomChanges & TopicChange)
+ if (roomChanges & Change::Topic)
emit topicChanged();
- if (roomChanges & (NameChange | AliasesChange))
+ if (roomChanges & (Change::Name | Change::Aliases))
emit namesChanged(this);
- if (roomChanges & MembersChange)
- emit memberListChanged();
+ d->postprocessChanges(roomChanges, !fromCache);
+}
- roomChanges |= d->setSummary(move(data.summary));
+void Room::Private::postprocessChanges(Changes changes, bool saveState)
+{
+ if (!changes)
+ return;
- for (auto&& ephemeralEvent : data.ephemeral)
- roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+ if (changes & Change::Members)
+ emit q->memberListChanged();
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (merge(d->unreadMessages, data.unreadCount)) {
- qCDebug(MESSAGES) << "Loaded unread_count:" << *data.unreadCount //
- << "in" << objectName();
- emit unreadMessagesChanged(this);
+ if (changes
+ & (Change::Name | Change::Aliases | Change::Members | Change::Summary))
+ updateDisplayname();
+
+ if (changes & Change::PartiallyReadStats) {
+ emit q->unreadMessagesChanged(q); // TODO: remove in 0.8
+ emit q->partiallyReadStatsChanged();
}
- if (merge(d->highlightCount, data.highlightCount))
- emit highlightCountChanged();
+ if (changes & Change::UnreadStats)
+ emit q->unreadStatsChanged();
- if (merge(d->notificationCount, data.notificationCount))
- emit notificationCountChanged();
+ if (changes & Change::Highlights)
+ emit q->highlightCountChanged();
- if (roomChanges != Change::NoChange) {
- d->updateDisplayname();
- emit changed(roomChanges);
- if (!fromCache)
- connection()->saveRoomState(this);
- }
+ qCDebug(MAIN) << terse << changes << "= hex" <<
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ Qt::
+#endif
+ hex << uint(changes) << "in" << q->objectName();
+ emit q->changed(changes);
+ if (saveState)
+ connection->saveRoomState(q);
}
RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
@@ -1991,7 +2225,7 @@ void Room::Private::getPreviousContent(int limit, const QString &filter)
eventsHistoryJob =
connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter);
emit q->eventsHistoryJobChanged();
- connect(eventsHistoryJob, &BaseJob::success, q, [=] {
+ connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
@@ -2162,7 +2396,7 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
- auto originalJson = target.originalJsonObject();
+ auto originalJson = target.fullJson();
// clang-format off
static const QStringList keepKeys {
EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey,
@@ -2208,7 +2442,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
originalJson.insert(ContentKey, content);
}
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
- unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject();
+ unsignedData[RedactedCauseKeyL] = redaction.fullJson();
originalJson.insert(QStringLiteral("unsigned"), unsignedData);
return loadEvent<RoomEvent>(originalJson);
@@ -2278,7 +2512,7 @@ RoomEventPtr makeReplaced(const RoomEvent& target,
if (!targetReply.empty()) {
newContent["m.relates_to"] = targetReply;
}
- auto originalJson = target.originalJsonObject();
+ auto originalJson = target.fullJson();
originalJson[ContentKeyL] = newContent;
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
@@ -2339,8 +2573,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return Change::NoChange;
+ return Change::None;
+ QElapsedTimer et;
+ et.start();
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2390,7 +2626,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// clients historically expect. This may eventually change though if we
// postulate that the current state is only current between syncs but not
// within a sync.
- Changes roomChanges = Change::NoChange;
+ Changes roomChanges {};
for (const auto& eptr : events)
roomChanges |= q->processStateEvent(*eptr);
@@ -2463,28 +2699,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< totalInserted << "new events; the last event is now"
<< timeline.back();
- // 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
- // the new message events.
- if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) {
- auto* const firstWriter = q->user(senderId);
- if (q->readMarker(firstWriter) != historyEdge()) {
- roomChanges |=
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
- qCDebug(MESSAGES)
- << "Auto-promoted read marker for" << senderId
- << "to" << *q->readMarker(firstWriter);
- }
- }
+ roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from));
- updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
- roomChanges |= Change::UnreadNotifsChange;
+ // If the local user's message(s) is/are first in the batch
+ // and the fully read marker was right before it, promote
+ // the fully read marker to the same event as the read receipt.
+ const auto& firstWriterId = (*from)->senderId();
+ if (firstWriterId == connection->userId()
+ && q->fullyReadMarker().base() == from)
+ roomChanges |=
+ setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId);
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to"
+ << q->objectName() << "in" << et;
return roomChanges;
}
@@ -2498,6 +2728,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
if (events.empty())
return;
+ Changes changes {};
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
@@ -2506,7 +2737,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto& e = *eptr;
if (e.isStateEvent()
&& !currentState.contains({ e.matrixType(), e.stateKey() })) {
- q->processStateEvent(e);
+ changes |= q->processStateEvent(e);
}
}
@@ -2526,19 +2757,20 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
emit q->updatedEvent(relation.eventId);
}
}
- if (from <= q->readMarker())
- updateUnreadCount(from, historyEdge());
-
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():"
- << insertedSize << "event(s)," << et;
+ qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
+ << q->objectName() << "in" << et;
+
+ changes |= updateStats(from, historyEdge());
+ if (changes)
+ postprocessChanges(changes);
}
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return NoChange;
+ return Change::None;
// Find a value (create an empty one if necessary) and get a reference
// to it. Can't use getCurrentState<>() because it (creates and) returns
@@ -2625,8 +2857,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
}
, true); // By default, go forward with the state change
// clang-format on
- if (!proceed)
- return NoChange;
+ if (!proceed) {
+ if (!curStateEvent) // Remove the empty placeholder if one was created
+ d->currentState.remove({ e.matrixType(), e.stateKey() });
+ return Change::None;
+ }
// Change the state
const auto* const oldStateEvent =
@@ -2644,7 +2879,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
// clang-format off
const auto result = visit(e
, [] (const RoomNameEvent&) {
- return NameChange;
+ return Change::Name;
}
, [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
// clang-format on
@@ -2663,16 +2898,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
newAliases.push_front(cae.alias());
connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
- return AliasesChange;
+ return Change::Aliases;
// clang-format off
}
, [] (const RoomTopicEvent&) {
- return TopicChange;
+ return Change::Topic;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
emit avatarChanged();
- return AvatarChange;
+ return Change::Avatar;
}
, [this,oldStateEvent] (const RoomMemberEvent& evt) {
// clang-format on
@@ -2711,14 +2946,14 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
case Membership::Undefined:
qCWarning(MEMBERS) << "Ignored undefined membership type";
}
- return MembersChange;
+ return Change::Members;
// clang-format off
}
, [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
- return OtherChange;
+ return Change::Other;
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2734,29 +2969,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return true;
});
- return OtherChange;
+ return Change::Other;
// clang-format off
}
- , OtherChange);
+ , Change::Other);
// clang-format on
- Q_ASSERT(result != NoChange);
+ Q_ASSERT(result != Change::None);
return result;
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
QElapsedTimer et;
et.start();
if (auto* evt = eventCast<TypingEvent>(event)) {
d->usersTyping.clear();
+ d->usersTyping.reserve(evt->users().size()); // Assume all are members
for (const auto& userId : evt->users())
if (isMember(userId))
d->usersTyping.append(user(userId));
if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):"
- << evt->users().size() << "users," << et;
+ qCDebug(PROFILER)
+ << "Processing typing events from" << evt->users().size()
+ << "user(s) in" << objectName() << "took" << et;
emit typingChanged();
}
if (auto* evt = eventCast<ReceiptEvent>(event)) {
@@ -2764,68 +3001,54 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
const auto& eventsWithReceipts = evt->eventsWithReceipts();
for (const auto& p : eventsWithReceipts) {
totalReceipts += p.receipts.size();
- {
- if (p.receipts.size() == 1)
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts[0].userId;
- else
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts.size() << "users";
- }
const auto newMarker = findInTimeline(p.evtId);
- if (newMarker != historyEdge()) {
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- if (isMember(r.userId))
- changes |=
- d->promoteReadMarker(user(r.userId), newMarker);
- }
- } else {
- qCDebug(EPHEMERAL) << "Event" << p.evtId
- << "not found; saving read receipts anyway";
- // 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.
- // Otherwise, blindly store the event id for this user.
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- if (!isMember(r.userId))
- continue;
- auto u = user(r.userId);
- if (readMarker(u) == historyEdge())
- changes |= d->setLastReadEvent(u, p.evtId);
- }
- }
+ if (newMarker == historyEdge())
+ qCDebug(EPHEMERAL)
+ << "Event" << p.evtId
+ << "is not found; saving read receipt(s) anyway";
+ // 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 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.
+ const auto updatedCount = std::count_if(
+ p.receipts.cbegin(), p.receipts.cend(),
+ [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) {
+ const auto change =
+ d->setLastReadReceipt(r.userId, newMarker,
+ { evtId, r.timestamp });
+ changes |= change;
+ return change & Change::Any;
+ });
+
+ if (p.receipts.size() > 1)
+ qCDebug(EPHEMERAL) << p.evtId << "marked as read for"
+ << updatedCount << "user(s)";
+ if (updatedCount < p.receipts.size())
+ qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount
+ << "receipts were skipped";
}
if (eventsWithReceipts.size() > 3 || totalReceipts > 10
|| et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::processEphemeralEvent(receipts):"
- << eventsWithReceipts.size() << "event(s) with"
- << totalReceipts << "receipt(s)," << et;
+ qCDebug(PROFILER) << "Processing" << totalReceipts
+ << "receipt(s) on" << eventsWithReceipts.size()
+ << "event(s) in" << objectName() << "took" << et;
}
return changes;
}
Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
if (auto* evt = eventCast<TagEvent>(event)) {
d->setTags(evt->tags());
- changes |= Change::TagsChange;
+ changes |= Change::Tags;
}
- 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 != historyEdge()
- ? 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
@@ -2836,7 +3059,10 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
qCDebug(STATE) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
- changes |= Change::AccountDataChange;
+ // TODO: Drop AccountDataChange in 0.8
+ // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around
+ // a statement, not within a statement
+ QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;)
}
return changes;
}
@@ -3006,19 +3232,28 @@ QJsonObject Room::Private::toJson() const
{ QStringLiteral("events"), accountDataEvents } });
}
- QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
- unreadMessages } };
-
- if (highlightCount > 0)
- unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount);
- if (notificationCount > 0)
- unreadNotifObj.insert(QStringLiteral("notification_count"),
- notificationCount);
-
- result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
+ if (const auto& readReceipt = q->lastReadReceipt(connection->userId());
+ !readReceipt.eventId.isEmpty()) //
+ {
+ result.insert(
+ QStringLiteral("ephemeral"),
+ QJsonObject {
+ { QStringLiteral("events"),
+ QJsonArray { ReceiptEvent({ { readReceipt.eventId,
+ { { connection->userId(),
+ readReceipt.timestamp } } } })
+ .fullJson() } } });
+ }
+
+ result.insert(UnreadNotificationsKey,
+ QJsonObject { { PartiallyReadCountKey,
+ countFromStats(partiallyReadStats) },
+ { HighlightCountKey, serverHighlightCount } });
+ result.insert(NewUnreadCountKey, countFromStats(unreadStats));
if (et.elapsed() > 30)
- qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
+ qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
+ << et;
return result;
}