diff options
Diffstat (limited to 'lib/room.cpp')
-rw-r--r-- | lib/room.cpp | 504 |
1 files changed, 324 insertions, 180 deletions
diff --git a/lib/room.cpp b/lib/room.cpp index a2ec228a..3090cb7b 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" @@ -116,14 +117,16 @@ 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; QHash<QString, QSet<QString>> eventIdReadUsers; QList<User*> usersInvited; QList<User*> membersLeft; - int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; @@ -241,13 +244,6 @@ 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) { @@ -273,6 +269,9 @@ public: 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. @@ -290,12 +289,12 @@ public: */ void dropDuplicateEvents(RoomEvents& events) const; - bool setLastReadReceipt(const QString& userId, rev_iter_t newMarker, - ReadReceipt newReceipt = {}); + Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}, + bool deferStatsUpdate = false); Changes setFullyReadMarker(const QString &eventId); - Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); - Changes recalculateUnreadCount(bool force = false); - void markMessagesAsRead(const rev_iter_t &upToMarker); + Changes updateStats(const rev_iter_t& from, const rev_iter_t& to); + bool markMessagesAsRead(const rev_iter_t& upToMarker); void getAllMembers(); @@ -617,9 +616,10 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -bool Room::Private::setLastReadReceipt(const QString& userId, - rev_iter_t newMarker, - ReadReceipt newReceipt) +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); @@ -647,10 +647,11 @@ bool Room::Private::setLastReadReceipt(const QString& userId, const auto prevEventId = storedReceipt.eventId; // NB: with reverse iterators, timeline history >= sync edge if (newMarker >= q->findInTimeline(prevEventId)) - return false; + return Change::None; // Finally make the change + Changes changes = Change::Other; auto oldEventReadUsersIt = eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member if (oldEventReadUsersIt != eventIdReadUsers.end()) { @@ -667,21 +668,43 @@ bool Room::Private::setLastReadReceipt(const QString& userId, // 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 true; + return changes; } -Room::Changes 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()); - auto fullyReadMarker = q->fullyReadMarker(); + 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 @@ -689,79 +712,70 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, if (fullyReadMarker == historyEdge() && q->allHistoryLoaded()) return setFullyReadMarker(timeline.front()->id()); - // 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 emits unreadMessagesChanged() and returns - // UnreadNotifsChange, even if the estimation luckily matched the exact - // result. - if (fullyReadMarker < to) - return recalculateUnreadCount(true); - - // At this point the fully read marker is somewhere beyond the "oldest" - // message from the arrived batch - add up newly arrived messages to - // the current counter, instead of a complete recalculation. - Q_ASSERT(to <= fullyReadMarker); + // 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; + } + } - 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 in" - << q->objectName() << "took" << et; - - if (newUnreadMessages == 0) - return Change::None; + // 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); - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; + const auto newStats = EventStats::fromRange(q, from, to); + Q_ASSERT(!newStats.isEstimate); + if (newStats.empty()) + return changes; - unreadMessages += newUnreadMessages; - qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->fullyReadMarker() == timeline.crend() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - return Change::UnreadNotifs; -} + 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; + }; -Room::Changes Room::Private::recalculateUnreadCount(bool force) -{ - // The recalculation logic assumes that the fully read marker points at - // a specific position in the timeline - Q_ASSERT(q->fullyReadMarker() != timeline.crend()); - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; - et.start(); - unreadMessages = - int(count_if(timeline.crbegin(), q->fullyReadMarker(), - [this](const auto& ti) { return isEventNotable(ti); })); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName() - << "took" << et; - - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; - - if (!force && unreadMessages == oldUnreadCount) - return Change::None; + 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"; - 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 Change::UnreadNotifs; + // Check invariants + Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker)); + Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker)); + return changes; } Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) @@ -769,58 +783,79 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) if (fullyReadUntilEventId == eventId) return Change::None; + 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; - emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - // TODO: Remove in 0.8 - emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker;) + QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;) if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { - // Pull read receipt if it's behind - setLastReadReceipt(connection->userId(), rm); - changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? + // 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::setReadReceipt(const QString& atEventId) { - if (!d->setLastReadReceipt(localUser()->id(), historyEdge(), - { atEventId, QDateTime::currentDateTime() })) { + 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"; - return; - } - connection()->callApi<PostReceiptJob>(BackgroundRequest, id(), - QStringLiteral("m.read"), - QUrl::toPercentEncoding(atEventId)); } -void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) +bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (upToMarker < q->fullyReadMarker()) { - setFullyReadMarker((*upToMarker)->id()); - // Assuming that if a read receipt was sent on a newer event, it will - // stay there instead of "un-reading" notifications/mentions from - // m.fully_read to m.read + 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(QString uptoEventId) +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 @@ -837,9 +872,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(); +} + +Notification Room::notificationFor(const TimelineItem &ti) const +{ + return d->notifications.value(ti->id()); +} -int Room::unreadCount() const { return d->unreadMessages; } +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(); } @@ -925,8 +991,7 @@ void Room::Private::getAllMembers() it != syncEdge(); ++it) if (is<RoomMemberEvent>(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges & Change::Members) - emit q->memberListChanged(); + postprocessChanges(roomChanges); emit q->allMembersLoaded(); }); } @@ -1046,23 +1111,26 @@ QSet<User*> Room::usersAtEventId(const QString& eventId) return users; } -int Room::notificationCount() const { return d->notificationCount; } +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(); } @@ -1443,7 +1511,6 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) return Change::None; qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; - emit q->memberListChanged(); return Change::Summary; } @@ -1548,11 +1615,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; @@ -1627,6 +1695,72 @@ 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()) @@ -1634,12 +1768,18 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) setJoinState(data.joinState); 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); - // The order of calculation is important - don't merge these lines! - roomChanges |= d->addNewMessageEvents(move(data.timeline)); + roomChanges |= d->updateStatsFromSyncData(data, fromCache); if (roomChanges & Change::Topic) emit topicChanged(); @@ -1647,33 +1787,40 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - if (roomChanges & Change::Members) - emit memberListChanged(); + d->postprocessChanges(roomChanges, !fromCache); +} + +void Room::Private::postprocessChanges(Changes changes, bool saveState) +{ + if (!changes) + return; - roomChanges |= d->setSummary(move(data.summary)); + if (changes & Change::Members) + emit q->memberListChanged(); - for (auto&& ephemeralEvent : data.ephemeral) - roomChanges |= processEphemeralEvent(move(ephemeralEvent)); + if (changes + & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) + updateDisplayname(); - // 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::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) { - 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) @@ -2519,17 +2666,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << totalInserted << "new events; the last event is now" << timeline.back(); - const auto& firstWriterId = (*from)->senderId(); - setLastReadReceipt(firstWriterId, rev_iter_t(from + 1)); + roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from)); + // 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) // + && q->fullyReadMarker().base() == from) roomChanges |= setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId); - - roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } Q_ASSERT(timeline.size() == timelineSize + totalInserted); @@ -2549,6 +2695,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 @@ -2557,7 +2704,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); } } @@ -2577,16 +2724,14 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->updatedEvent(relation.eventId); } } - updateUnreadCount(from, historyEdge()); - // When there are no unread messages and the read marker is within the - // known timeline, unreadMessages == -1 - // (see https://github.com/quotient-im/libQuotient/wiki/Developer-notes#2-saving-unread-event-counts). - Q_ASSERT(unreadMessages != 0 || q->fullyReadMarker() == historyEdge()); - Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) 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) @@ -2822,8 +2967,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) totalReceipts += p.receipts.size(); const auto newMarker = findInTimeline(p.evtId); if (newMarker == historyEdge()) - qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " - "found; saving anyway"; + 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 @@ -2832,9 +2978,12 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) // the event is fetched later on. const auto updatedCount = std::count_if( p.receipts.cbegin(), p.receipts.cend(), - [this, &newMarker, &evtId = p.evtId](const auto& r) -> bool { - return d->setLastReadReceipt(r.userId, newMarker, - { evtId, r.timestamp }); + [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) { + const auto change = + d->setLastReadReceipt(r.userId, newMarker, + { evtId, r.timestamp }); + changes |= change; + return change.testFlag(Change::Any); }); if (p.receipts.size() > 1) @@ -3060,16 +3209,11 @@ QJsonObject Room::Private::toJson() const .fullJson() } } }); } - 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); + result.insert(UnreadNotificationsKey, + QJsonObject { { PartiallyReadCountKey, + countFromStats(partiallyReadStats) }, + { HighlightCountKey, serverHighlightCount } }); + result.insert(NewUnreadCountKey, countFromStats(unreadStats)); if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" |