From eed5a6f127ec3bb1553ac629457f196d8893665a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 26 Jul 2021 08:19:29 +0200 Subject: Room::usersAtEventId(): switch from QMultiHash to QHash of QSets While slightly more complex for updating, this allows COW to kick in in the read accessor; using QSet instead of QList also provides better consistency guarantees. For QML both are converted to an Array-like collection since Qt 5.15; Qt 5.12 turns QSet<> in a QVariantList, according to the documentation, which is quite reasonable too. --- lib/room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/room.h') diff --git a/lib/room.h b/lib/room.h index cdbfe58f..fa7b6e6d 100644 --- a/lib/room.h +++ b/lib/room.h @@ -364,7 +364,7 @@ public: rev_iter_t readMarker(const User* user) const; rev_iter_t readMarker() const; QString readMarkerEventId() const; - QList usersAtEventId(const QString& eventId); + QSet usersAtEventId(const QString& eventId); /** * \brief Mark the event with uptoEventId as read * -- cgit v1.2.3 From 62c829cc8de8a870c08926c41331f2766e766f37 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 29 Jul 2021 21:58:56 +0200 Subject: Quotient::ReadReceipt; deprecated readMarker() It's now possible to get receipts along with their timestamps by calling Room::lastReadReceipt(). Together this new method, fullyReadMarker(), and lastFullyReadEventId() deprecate readMarker() overloads and readMarkerEventId() respectively. lastFullyReadEventId is also a Q_PROPERTY (deprecating readMarkerEventId); readMarkerMoved() signal is deprecated by fullyReadMarkerMoved(), while readMarkerForUserMoved() is deprecated in favour of existing lastReadEventChanged(). --- lib/room.cpp | 79 ++++++++++++++++++++++++++++++++++-------------------------- lib/room.h | 39 +++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 35 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index ec3a9532..9a571127 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -128,7 +128,7 @@ public: bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; - QHash lastReadEventIds; + QHash lastReadReceipts; QString fullyReadUntilEventId; TagsMap tags; UnorderedMap accountData; @@ -291,7 +291,7 @@ public: void dropDuplicateEvents(RoomEvents& events) const; void setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId = {}); + ReadReceipt newReceipt = {}); Changes setFullyReadMarker(const QString &eventId); Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); Changes recalculateUnreadCount(bool force = false); @@ -624,7 +624,7 @@ void Room::setJoinState(JoinState state) } void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId) + ReadReceipt newReceipt) { if (!u) { Q_ASSERT(u != nullptr); // For Debug builds @@ -636,11 +636,12 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, return; } - if (newMarker == timeline.crend() && !newEvtId.isEmpty()) - newMarker = q->findInTimeline(newEvtId); + auto& storedReceipt = lastReadReceipts[u]; + if (newMarker == timeline.crend() && !newReceipt.eventId.isEmpty()) + newMarker = q->findInTimeline(newReceipt.eventId); if (newMarker != timeline.crend()) { // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->readMarker(u)) + if (newMarker >= q->findInTimeline(storedReceipt.eventId)) return; // Try to auto-promote the read marker over the user's own messages @@ -650,25 +651,26 @@ void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, return ti->senderId() != u->id(); }) - 1; - newEvtId = (*eagerMarker)->id(); + newReceipt.eventId = (*eagerMarker)->id(); if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() - << "to" << newEvtId; + << "to" << newReceipt.eventId; } - auto& storedId = lastReadEventIds[u]; - if (storedId == newEvtId) + if (storedReceipt == newReceipt) return; // Finally make the change - auto& oldEventReadUsers = eventIdReadUsers[storedId]; + auto& oldEventReadUsers = eventIdReadUsers[storedReceipt.eventId]; oldEventReadUsers.remove(u); if (oldEventReadUsers.isEmpty()) - eventIdReadUsers.remove(storedId); - eventIdReadUsers[newEvtId].insert(u); - swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId + eventIdReadUsers.remove(storedReceipt.eventId); + eventIdReadUsers[newReceipt.eventId].insert(u); + swap(storedReceipt, newReceipt); // Now newReceipt actually stores the old receipt emit q->lastReadEventChanged(u); + // TODO: remove in 0.8 if (!isLocalUser(u)) - emit q->readMarkerForUserMoved(u, newEvtId, storedId); + emit q->readMarkerForUserMoved(u, newReceipt.eventId, + storedReceipt.eventId); } Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, @@ -677,7 +679,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); - auto fullyReadMarker = q->readMarker(); + auto fullyReadMarker = q->fullyReadMarker(); if (fullyReadMarker < from) return NoChange; // What's arrived is already fully read @@ -718,7 +720,7 @@ Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, unreadMessages += newUnreadMessages; qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() + << (q->fullyReadMarker() == timeline.crend() ? "in total at least" : "in total") << unreadMessages << "unread message(s)"; @@ -730,12 +732,12 @@ 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->readMarker() != timeline.crend()); + Q_ASSERT(q->fullyReadMarker() != timeline.crend()); const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); unreadMessages = - int(count_if(timeline.crbegin(), q->readMarker(), + int(count_if(timeline.crbegin(), q->fullyReadMarker(), [this](const auto& ti) { return isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -765,22 +767,22 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // << "moved to" << fullyReadUntilEventId; + emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); + // TODO: Remove in 0.8 emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); Changes changes = ReadMarkerChange; - if (const auto rm = q->readMarker(); rm != timeline.crend()) { + if (const auto rm = q->fullyReadMarker(); 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(); + setLastReadReceipt(q->localUser(), rm); + changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? } return changes; } void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { - if (upToMarker < q->readMarker()) { + 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 @@ -989,17 +991,23 @@ 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 +Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); } + +QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } + +ReadReceipt Room::lastReadReceipt(const QString& userId) const { - return findInTimeline(d->fullyReadUntilEventId); + return d->lastReadReceipts.value(user(userId)); } -QString Room::readMarkerEventId() const +QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } + +Room::rev_iter_t Room::fullyReadMarker() const { - return d->fullyReadUntilEventId; + return findInTimeline(d->fullyReadUntilEventId); } QSet Room::usersAtEventId(const QString& eventId) @@ -2477,12 +2485,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // the new message events. auto* const firstWriter = q->user((*from)->senderId()); setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); - if (firstWriter == q->localUser() && q->readMarker().base() == from) { + if (firstWriter == q->localUser() + && q->fullyReadMarker().base() == 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. roomChanges |= - setFullyReadMarker(lastReadEventIds.value(firstWriter)); + setFullyReadMarker(lastReadReceipts.value(firstWriter).eventId); } roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } @@ -2533,7 +2543,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) // 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/unread_count). - Q_ASSERT(unreadMessages != 0 || q->readMarker() == timeline.crend()); + Q_ASSERT(unreadMessages != 0 || q->fullyReadMarker() == timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) @@ -2793,7 +2803,8 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) // 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); + d->setLastReadReceipt(u, newMarker, + { p.evtId, r.timestamp }); } } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 diff --git a/lib/room.h b/lib/room.h index fa7b6e6d..4833bd27 100644 --- a/lib/room.h +++ b/lib/room.h @@ -71,6 +71,28 @@ public: bool failed() const { return status == Failed; } }; +//! \brief Data structure for a room member's read receipt +//! \sa Room::lastReadReceipt +class ReadReceipt { + Q_GADGET + Q_PROPERTY(QString eventId MEMBER eventId CONSTANT) + Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) +public: + QString eventId; + QDateTime timestamp; + + bool operator==(const ReadReceipt& other) + { + return eventId == other.eventId && timestamp == other.timestamp; + } + bool operator!=(const ReadReceipt& other) { return !operator==(other); } +}; +inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) +{ + swap(lhs.eventId, rhs.eventId); + swap(lhs.timestamp, rhs.timestamp); +} + class Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) @@ -104,9 +126,11 @@ class Room : public QObject { setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - + //! \deprecated since 0.7 Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE + markMessagesAsRead NOTIFY fullyReadMarkerMoved) Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) @@ -361,9 +385,18 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker(const User* user) const; + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // QString readMarkerEventId() const; + ReadReceipt lastReadReceipt(const QString& userId) const; + QString lastFullyReadEventId() const; + rev_iter_t fullyReadMarker() const; QSet usersAtEventId(const QString& eventId); /** * \brief Mark the event with uptoEventId as read @@ -704,7 +737,10 @@ Q_SIGNALS: void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); void lastReadEventChanged(Quotient::User* user); + void fullyReadMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use fullyReadMarkerMoved void readMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use lastReadEventChanged void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); void unreadMessagesChanged(Quotient::Room* room); @@ -779,4 +815,5 @@ private: }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::FileTransferInfo) +Q_DECLARE_METATYPE(Quotient::ReadReceipt) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) -- cgit v1.2.3 From 2975b435f1ae198187183b883b5e666c7659e8fc Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 30 Jul 2021 23:25:01 +0200 Subject: Room::setReadReceipt() --- lib/room.cpp | 9 +++++++++ lib/room.h | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index c7f29b6d..649b1ed2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -765,6 +765,15 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) return changes; } +void Room::setReadReceipt(const QString& atEventId) +{ + d->setLastReadReceipt(localUser(), historyEdge(), + { atEventId, QDateTime::currentDateTime() }); + connection()->callApi(BackgroundRequest, id(), + QStringLiteral("m.read"), + QUrl::toPercentEncoding(atEventId)); +} + void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) { if (upToMarker < q->fullyReadMarker()) { diff --git a/lib/room.h b/lib/room.h index 9ef553df..afe223c5 100644 --- a/lib/room.h +++ b/lib/room.h @@ -626,7 +626,12 @@ public Q_SLOTS: void downloadFile(const QString& eventId, const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); - /// Mark all messages in the room as read + //! \brief Set a given event as last read and post a read receipt on it + //! + //! Does nothing if the event is behind the current read receipt. + //! \sa lastReadReceipt, markMessagesAsRead, markAllMessagesAsRead + void setReadReceipt(const QString& atEventId); + //! Put the fully-read marker at the latest message in the room void markAllMessagesAsRead(); /// Switch the room's version (aka upgrade) -- cgit v1.2.3 From 6d558fa9c12b52fe3cc47cc143d0a758c4ea929a Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sat, 31 Jul 2021 08:30:44 +0200 Subject: Add/update doc-comments on the new/updated methods [skip ci] --- lib/room.h | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 10 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.h b/lib/room.h index afe223c5..a8c3cac2 100644 --- a/lib/room.h +++ b/lib/room.h @@ -378,30 +378,85 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); + //! \brief Obtain a read receipt of any user + //! + //! \deprecated Use lastReadReceipt or fullyReadMarker instead + //! Historically, readMarker was returning a "converged" read marker + //! representing both the read receipt and the fully read marker, as + //! Quotient managed them together. Since 0.6.8, a single-argument call of + //! readMarker returns the last read receipt position (for any room member) + //! and a call without arguments returns the last _fully read_ position, + //! to provide access to both positions separately while maintaining API + //! stability guarantees. 0.7 has separate methods to return read receipts + //! and the fully read marker - use them instead. + //! \sa lastReadReceipt [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker(const User* user) const; + //! \brief Obtain the local user's fully-read marker + //! \deprecated Use fullyReadMarker instead + //! See the documentation for the single-argument overload + //! \sa fullyReadMarker [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; + //! \brief Get the event id for the local user's fully-read marker + //! \deprecated Use lastFullyReadEventId instead + //! See the readMarker documentation [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // QString readMarkerEventId() const; + + //! \brief Get the latest read receipt from a user + //! + //! The user id must be valid. A read receipt with an empty event id + //! is returned if the user id is valid but there was no read receipt + //! from them. + //! \sa usersAtEventId ReadReceipt lastReadReceipt(const QString& userId) const; + + //! \brief Get the latest event id marked as fully read + //! + //! This can be either the event id pointed to by the actual latest + //! m.fully_read event, or the latest event id marked locally as fully read + //! if markMessagesAsRead or markAllMessagesAsRead has been called and + //! the homeserver didn't return an updated m.fully_read event yet. + //! \sa markMessagesAsRead, markAllMessagesAsRead, fullyReadMarker QString lastFullyReadEventId() const; + + //! \brief Get the iterator to the latest timeline item marked as fully read + //! + //! This method calls findInTimeline on the result of lastFullyReadEventId. + //! If the fully read marker turns out to be outside the timeline (because + //! the event marked as fully read is too far back in the history) the + //! returned value will be equal to historyEdge. + //! + //! Be sure to read the caveats on iterators returned by findInTimeline. + //! \sa lastFullyReadEventId, findInTimeline rev_iter_t fullyReadMarker() const; + + //! \brief Get users whose latest read receipts point to the event + //! + //! This method is for cases when you need to show users who have read + //! an event. Calling it on inexistent or empty event id will return + //! an empty set. + //! \sa lastReadReceipt QSet usersAtEventId(const QString& eventId); - /** - * \brief Mark the event with uptoEventId as read - * - * Finds in the timeline and marks as read the event with - * the specified id; also posts a read receipt to the server either - * for this message or, if it's from the local user, for - * the nearest non-local message before. uptoEventId must be non-empty. - */ - void markMessagesAsRead(QString uptoEventId); - /// Check whether there are unread messages in the room + //! + //! \brief Mark the event with uptoEventId as fully read + //! + //! Marks the event with the specified id as fully read locally and also + //! sends an update to m.fully_read account data to the server either + //! for this message or, if it's from the local user, for + //! the nearest non-local message before. uptoEventId must point to a known + //! event in the timeline; the method will do nothing if the event is behind + //! the current m.fully_read marker or is not loaded, to prevent + //! accidentally trying to move the marker back in the timeline. + //! \sa markAllMessagesAsRead, fullyReadMarker + Q_INVOKABLE void markMessagesAsRead(QString uptoEventId); + + //! Check whether there are unread messages in the room bool hasUnreadMessages() const; /** Get the number of unread messages in the room -- cgit v1.2.3 From 762be1e73d6f5021e63fc9a4fea273951b3fb113 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 10 Oct 2021 22:26:08 +0200 Subject: ReadReceipt::operator==/!=: Add const where it's due --- lib/room.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.h b/lib/room.h index ca9f36cb..73f28a6e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -81,11 +81,14 @@ public: QString eventId; QDateTime timestamp; - bool operator==(const ReadReceipt& other) + bool operator==(const ReadReceipt& other) const { return eventId == other.eventId && timestamp == other.timestamp; } - bool operator!=(const ReadReceipt& other) { return !operator==(other); } + bool operator!=(const ReadReceipt& other) const + { + return !operator==(other); + } }; inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) { -- cgit v1.2.3 From edb63528e6f3048045f70eb6a48412917bdbea0b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 15 Nov 2021 21:37:04 +0100 Subject: Bind read receipts to userIds, not to User* values This reduces the surface interacting with the User class that eventually will be split into LocalUser (most part) and RoomMember (a tiny wrapper around the member data in a given room, used almost everywhere in Room where User currently is). Also: dropped a log message when the new receipt is at or behind the old one as it causes a lot of noise in the logs. --- lib/room.cpp | 163 +++++++++++++++++++++++++++++++++-------------------------- lib/room.h | 12 ++++- 2 files changed, 100 insertions(+), 75 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index 58950aac..ee76a85c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -120,14 +120,14 @@ public: int notificationCount = 0; members_map_t membersMap; QList usersTyping; - QHash> eventIdReadUsers; + QHash> eventIdReadUsers; QList usersInvited; QList membersLeft; int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; - QHash lastReadReceipts; + QHash lastReadReceipts; QString fullyReadUntilEventId; TagsMap tags; UnorderedMap accountData; @@ -291,7 +291,7 @@ public: */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadReceipt(User* u, rev_iter_t newMarker, + bool setLastReadReceipt(const QString& userId, rev_iter_t newMarker, ReadReceipt newReceipt = {}); Changes setFullyReadMarker(const QString &eventId); Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); @@ -619,60 +619,62 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, +bool Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, ReadReceipt newReceipt) { - if (!u) { - Q_ASSERT(u != nullptr); // For Debug builds - qCCritical(MAIN) << "Empty user, skipping read receipt registration"; - return; // For Release builds - } - if (!q->isMember(u->id())) { - qCWarning(EPHEMERAL) - << "Won't record read receipt for non-member" << u->id(); - return; - } - - auto& storedReceipt = lastReadReceipts[u]; - if (newMarker == timeline.crend() && !newReceipt.eventId.isEmpty()) + if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) newMarker = q->findInTimeline(newReceipt.eventId); - if (newMarker != timeline.crend()) { - // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->findInTimeline(storedReceipt.eventId)) { - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() - << "is at or behind the old one, skipping"; - return; - } - + 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(), timeline.cend(), + const auto eagerMarker = find_if(newMarker.base(), syncEdge(), [=](const TimelineItem& ti) { - return ti->senderId() != u->id(); - }) - - 1; - newReceipt.eventId = (*eagerMarker)->id(); - if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() + return ti->senderId() != userId; + }); + // eagerMarker is now just after the desired event for newMarker + if (eagerMarker != newMarker.base()) { + qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId << "to" << newReceipt.eventId; - } + newMarker = rev_iter_t(eagerMarker); + } + // 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; + // NB: with reverse iterators, timeline history >= sync edge + if (newMarker >= q->findInTimeline(prevEventId)) + return false; - if (storedReceipt == newReceipt) - return; // Finally make the change - auto& oldEventReadUsers = eventIdReadUsers[storedReceipt.eventId]; - oldEventReadUsers.remove(u); - if (oldEventReadUsers.isEmpty()) - eventIdReadUsers.remove(storedReceipt.eventId); - eventIdReadUsers[newReceipt.eventId].insert(u); - swap(storedReceipt, newReceipt); // Now newReceipt actually stores the old receipt - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at" + + 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); + qCDebug(EPHEMERAL) << "The new read receipt for" << userId << "is now at" << storedReceipt.eventId; - emit q->lastReadEventChanged(u); + + // 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); + emit q->lastReadEventChanged(member); // TODO: remove in 0.8 - if (!isLocalUser(u)) - emit q->readMarkerForUserMoved(u, newReceipt.eventId, + if (!isLocalUser(member)) + emit q->readMarkerForUserMoved(member, prevEventId, storedReceipt.eventId); + return true; } Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from, @@ -777,7 +779,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) Changes changes = ReadMarkerChange; if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { // Pull read receipt if it's behind - setLastReadReceipt(q->localUser(), rm); + setLastReadReceipt(connection->userId(), rm); changes |= recalculateUnreadCount(); // TODO: updateUnreadCount()? } return changes; @@ -785,8 +787,13 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) void Room::setReadReceipt(const QString& atEventId) { - d->setLastReadReceipt(localUser(), historyEdge(), - { atEventId, QDateTime::currentDateTime() }); + if (!d->setLastReadReceipt(localUser()->id(), historyEdge(), + { atEventId, QDateTime::currentDateTime() })) { + qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id() + << "in" << objectName() + << "is at or behind the old one, skipping"; + return; + } connection()->callApi(BackgroundRequest, id(), QStringLiteral("m.read"), QUrl::toPercentEncoding(atEventId)); @@ -1004,7 +1011,7 @@ QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } ReadReceipt Room::lastReadReceipt(const QString& userId) const { - return d->lastReadReceipts.value(user(userId)); + return d->lastReadReceipts.value(userId); } QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } @@ -1014,11 +1021,21 @@ Room::rev_iter_t Room::fullyReadMarker() const return findInTimeline(d->fullyReadUntilEventId); } -QSet Room::usersAtEventId(const QString& eventId) +QSet Room::userIdsAtEvent(const QString& eventId) { return d->eventIdReadUsers.value(eventId); } +QSet Room::usersAtEventId(const QString& eventId) +{ + const auto& userIds = d->eventIdReadUsers.value(eventId); + QSet users; + users.reserve(userIds.size()); + for (const auto& uId : userIds) + users.insert(user(uId)); + return users; +} + int Room::notificationCount() const { return d->notificationCount; } void Room::resetNotificationCount() @@ -2498,17 +2515,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << totalInserted << "new events; the last event is now" << timeline.back(); - auto* const firstWriter = q->user((*from)->senderId()); - setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); - if (firstWriter == q->localUser() + const auto& firstWriterId = (*from)->senderId(); + setLastReadReceipt(firstWriterId, rev_iter_t(from + 1)); + // 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. + if (firstWriterId == connection->userId() && q->fullyReadMarker().base() == 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. roomChanges |= - setFullyReadMarker(lastReadReceipts.value(firstWriter).eventId); - } + setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId); + roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } @@ -2781,6 +2797,7 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) et.start(); if (auto* evt = eventCast(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)); @@ -2795,29 +2812,29 @@ 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()) qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " - "found; saving them anyway"; + "found; saving 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. - for (const auto& r : p.receipts) - if (isMember(r.userId)) { - d->setLastReadReceipt(user(r.userId), newMarker, - { p.evtId, r.timestamp }); - } + 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 }); + }); + + 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()) diff --git a/lib/room.h b/lib/room.h index 260510e6..e4d33c4e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -442,10 +442,18 @@ public: //! This method is for cases when you need to show users who have read //! an event. Calling it on inexistent or empty event id will return //! an empty set. - //! \sa lastReadReceipt + //! \note The returned list may contain ids resolving to users that are + //! not loaded as room members yet (in particular, if members are not + //! yet lazy-loaded). For now this merely means that the user's + //! room-specific name and avatar will not be there; but generally + //! it's recommended to ensure that all room members are loaded + //! before operating on the result of this function. + //! \sa lastReadReceipt, allMembersLoaded + QSet userIdsAtEvent(const QString& eventId); + + [[deprecated("Use userIdsAtEvent instead")]] QSet usersAtEventId(const QString& eventId); - //! //! \brief Mark the event with uptoEventId as fully read //! //! Marks the event with the specified id as fully read locally and also -- cgit v1.2.3 From 7b633ba257fc8643ef8cc2ef724f3b6ac9e186ba Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 18 Oct 2021 05:42:12 +0200 Subject: Room: doc-comments cleanup [skip ci] --- lib/room.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.h b/lib/room.h index e4d33c4e..24025a88 100644 --- a/lib/room.h +++ b/lib/room.h @@ -381,8 +381,8 @@ public: void setLastDisplayedEvent(TimelineItem::index_t index); //! \brief Obtain a read receipt of any user + //! \deprecated Use lastReadReceipt or fullyReadMarker instead. //! - //! \deprecated Use lastReadReceipt or fullyReadMarker instead //! Historically, readMarker was returning a "converged" read marker //! representing both the read receipt and the fully read marker, as //! Quotient managed them together. Since 0.6.8, a single-argument call of @@ -397,16 +397,19 @@ public: rev_iter_t readMarker(const User* user) const; //! \brief Obtain the local user's fully-read marker //! \deprecated Use fullyReadMarker instead - //! See the documentation for the single-argument overload + //! + //! See the documentation for the single-argument overload. //! \sa fullyReadMarker [[deprecated("Use lastReadReceipt() to get m.read receipt or" " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker() const; //! \brief Get the event id for the local user's fully-read marker //! \deprecated Use lastFullyReadEventId instead + //! //! See the readMarker documentation [[deprecated("Use lastReadReceipt() to get m.read receipt or" - " fullyReadMarker() to get m.fully_read marker")]] // + " lastFullyReadEventId() to get an event id that" + " m.fully_read marker points to")]] // QString readMarkerEventId() const; //! \brief Get the latest read receipt from a user -- cgit v1.2.3 From b472fc355dc5ce70391ca2b9bc8da35b973ae3a3 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 8 Nov 2021 21:03:37 +0100 Subject: Room: lastLocalReadReceipt(), localReadReceiptMarker() To simplify retrieval of the local m.read receipt and the marker for it. --- lib/room.cpp | 10 ++++++++++ lib/room.h | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index ee76a85c..273a5753 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1014,6 +1014,16 @@ ReadReceipt Room::lastReadReceipt(const QString& userId) const return d->lastReadReceipts.value(userId); } +ReadReceipt Room::lastLocalReadReceipt() const +{ + return d->lastReadReceipts.value(localUser()->id()); +} + +Room::rev_iter_t Room::localReadReceiptMarker() const +{ + return findInTimeline(lastLocalReadReceipt().eventId); +} + QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } Room::rev_iter_t Room::fullyReadMarker() const diff --git a/lib/room.h b/lib/room.h index 24025a88..d94de51c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -400,8 +400,7 @@ public: //! //! See the documentation for the single-argument overload. //! \sa fullyReadMarker - [[deprecated("Use lastReadReceipt() to get m.read receipt or" - " fullyReadMarker() to get m.fully_read marker")]] // + [[deprecated("Use localReadReceiptMarker() or fullyReadMarker()")]] // rev_iter_t readMarker() const; //! \brief Get the event id for the local user's fully-read marker //! \deprecated Use lastFullyReadEventId instead @@ -420,6 +419,19 @@ public: //! \sa usersAtEventId ReadReceipt lastReadReceipt(const QString& userId) const; + //! \brief Get the latest read receipt from the local user + //! + //! This is a shortcut for lastReadReceipt(localUserId). + //! \sa lastReadReceipt + ReadReceipt lastLocalReadReceipt() const; + + //! \brief Find the timeline item the local read receipt is at + //! + //! This is a shortcut for \code + //! room->findInTimeline(room->lastLocalReadReceipt().eventId); + //! \endcode + rev_iter_t localReadReceiptMarker() const; + //! \brief Get the latest event id marked as fully read //! //! This can be either the event id pointed to by the actual latest -- cgit v1.2.3