aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--connection.cpp2
-rw-r--r--jobs/syncjob.cpp22
-rw-r--r--jobs/syncjob.h3
-rw-r--r--room.cpp236
-rw-r--r--room.h23
5 files changed, 178 insertions, 108 deletions
diff --git a/connection.cpp b/connection.cpp
index e4e622a3..ee719b40 100644
--- a/connection.cpp
+++ b/connection.cpp
@@ -792,7 +792,7 @@ void Connection::setHomeserver(const QUrl& url)
}
static constexpr int CACHE_VERSION_MAJOR = 6;
-static constexpr int CACHE_VERSION_MINOR = 0;
+static constexpr int CACHE_VERSION_MINOR = 1;
void Connection::saveState(const QUrl &toFile) const
{
diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp
index ed579f12..435dfd0e 100644
--- a/jobs/syncjob.cpp
+++ b/jobs/syncjob.cpp
@@ -89,6 +89,9 @@ BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
return BaseJob::Success;
}
+const QString SyncRoomData::UnreadCountKey =
+ QStringLiteral("x-qmatrixclient.unread_count");
+
SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
const QJsonObject& room_)
: roomId(roomId_)
@@ -116,12 +119,15 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
qCWarning(SYNCJOB) << "SyncRoomData: Unknown JoinState value, ignoring:" << int(joinState);
}
- QJsonObject timeline = room_.value("timeline").toObject();
- timelineLimited = timeline.value("limited").toBool();
- timelinePrevBatch = timeline.value("prev_batch").toString();
-
- QJsonObject unread = room_.value("unread_notifications").toObject();
- highlightCount = unread.value("highlight_count").toInt();
- notificationCount = unread.value("notification_count").toInt();
- qCDebug(SYNCJOB) << "Highlights: " << highlightCount << " Notifications:" << notificationCount;
+ auto timelineJson = room_.value("timeline").toObject();
+ timelineLimited = timelineJson.value("limited").toBool();
+ timelinePrevBatch = timelineJson.value("prev_batch").toString();
+
+ auto unreadJson = room_.value("unread_notifications").toObject();
+ unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
+ highlightCount = unreadJson.value("highlight_count").toInt();
+ notificationCount = unreadJson.value("notification_count").toInt();
+ if (highlightCount > 0 || notificationCount > 0)
+ qCDebug(SYNCJOB) << "Highlights: " << highlightCount
+ << " Notifications:" << notificationCount;
}
diff --git a/jobs/syncjob.h b/jobs/syncjob.h
index 5956e73b..919060be 100644
--- a/jobs/syncjob.h
+++ b/jobs/syncjob.h
@@ -53,6 +53,7 @@ namespace QMatrixClient
bool timelineLimited;
QString timelinePrevBatch;
+ int unreadCount;
int highlightCount;
int notificationCount;
@@ -60,6 +61,8 @@ namespace QMatrixClient
const QJsonObject& room_);
SyncRoomData(SyncRoomData&&) = default;
SyncRoomData& operator=(SyncRoomData&&) = default;
+
+ static const QString UnreadCountKey;
};
// QVector cannot work with non-copiable objects, std::vector can.
using SyncDataList = std::vector<SyncRoomData>;
diff --git a/room.cpp b/room.cpp
index 01055013..9dec2b4a 100644
--- a/room.cpp
+++ b/room.cpp
@@ -72,7 +72,6 @@ class Room::Private
public:
/** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */
typedef QMultiHash<QString, User*> members_map_t;
- typedef std::pair<rev_iter_t, rev_iter_t> rev_iter_pair_t;
Private(Connection* c, QString id_, JoinState initialJoinState)
: q(nullptr), connection(c), id(std::move(id_))
@@ -102,7 +101,7 @@ class Room::Private
members_map_t membersMap;
QList<User*> usersTyping;
QList<User*> membersLeft;
- bool unreadMessages = false;
+ int unreadMessages = 0;
bool displayed = false;
QString firstDisplayedEventId;
QString lastDisplayedEventId;
@@ -190,10 +189,10 @@ class Room::Private
* Removes events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents* events) const;
- void checkUnreadMessages(timeline_iter_t from);
void setLastReadEvent(User* u, const QString& eventId);
- rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker,
+ void updateUnreadCount(rev_iter_t from, rev_iter_t to);
+ void promoteReadMarker(User* u, rev_iter_t newMarker,
bool force = false);
void markMessagesAsRead(rev_iter_t upToMarker);
@@ -359,16 +358,55 @@ void Room::Private::setLastReadEvent(User* u, const QString& eventId)
}
}
-Room::Private::rev_iter_pair_t
-Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker,
- bool force)
+void Room::Private::updateUnreadCount(rev_iter_t from, 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.
+ const auto readMarker = q->readMarker();
+ if (readMarker >= from && readMarker < to)
+ {
+ qCDebug(MAIN) << "Discovered last read event in room" << displayname;
+ promoteReadMarker(q->localUser(), readMarker, true);
+ return;
+ }
+
+ 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() > 10000)
+ qCDebug(PROFILER) << "Counting gained unread messages took" << et;
+
+ if(newUnreadMessages > 0)
+ {
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (unreadMessages < 0)
+ unreadMessages = 0;
+
+ unreadMessages += newUnreadMessages;
+ qCDebug(MAIN) << "Room" << displayname << "has gained"
+ << newUnreadMessages << "unread message(s),"
+ << (q->readMarker() == timeline.crend() ?
+ "in total at least" : "in total")
+ << unreadMessages << "unread message(s)";
+ emit q->unreadMessagesChanged(q);
+ }
+}
+
+void Room::Private::promoteReadMarker(User* u, 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 { prevMarker, prevMarker };
+ return;
Q_ASSERT(newMarker < timeline.crend());
@@ -378,41 +416,49 @@ Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker,
[=](const TimelineItem& ti) { return ti->senderId() != u->id(); });
setLastReadEvent(u, (*(eagerMarker - 1))->id());
- if (isLocalUser(u) && unreadMessages)
+ if (isLocalUser(u))
{
- auto stillUnreadMessagesCount = count_if(eagerMarker, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
-
- if (stillUnreadMessagesCount == 0)
+ const auto oldUnreadCount = unreadMessages;
+ QElapsedTimer et; et.start();
+ unreadMessages = count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1));
+ if (et.nsecsElapsed() > 10000)
+ qCDebug(PROFILER) << "Recounting unread messages took" << et;
+
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (unreadMessages == 0)
+ unreadMessages = -1;
+
+ if (force || unreadMessages != oldUnreadCount)
{
- unreadMessages = false;
- qCDebug(MAIN) << "Room" << displayname << "has no more unread messages";
+ if (unreadMessages == -1)
+ {
+ qCDebug(MAIN) << "Room" << displayname
+ << "has no more unread messages";
+ } else
+ qCDebug(MAIN) << "Room" << displayname << "still has"
+ << unreadMessages << "unread message(s)";
emit q->unreadMessagesChanged(q);
- } else
- qCDebug(MAIN) << "Room" << displayname << "still has"
- << stillUnreadMessagesCount << "unread message(s)";
+ }
}
-
- // Return newMarker, rather than eagerMarker, to save markMessagesAsRead()
- // (that calls this method) from going back through knowingly-local messages.
- return { prevMarker, newMarker };
}
-void Room::Private::markMessagesAsRead(Room::rev_iter_t upToMarker)
+void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
- rev_iter_pair_t markers = promoteReadMarker(q->localUser(), upToMarker);
- if (markers.first != markers.second)
+ const auto prevMarker = q->readMarker();
+ promoteReadMarker(q->localUser(), upToMarker);
+ if (prevMarker != upToMarker)
qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker();
// We shouldn't send read receipts for the local user's own messages - so
// search earlier messages for the latest message not from the local user
// until the previous last-read message, whichever comes first.
- for (; markers.second < markers.first; ++markers.second)
+ for (; upToMarker < prevMarker; ++upToMarker)
{
- if ((*markers.second)->senderId() != q->localUser()->id())
+ if ((*upToMarker)->senderId() != q->localUser()->id())
{
connection->callApi<PostReceiptJob>(id, "m.read",
- (*markers.second)->id());
+ (*upToMarker)->id());
break;
}
}
@@ -429,7 +475,12 @@ void Room::markAllMessagesAsRead()
d->markMessagesAsRead(d->timeline.crbegin());
}
-bool Room::hasUnreadMessages()
+bool Room::hasUnreadMessages() const
+{
+ return unreadCount() >= 0;
+}
+
+int Room::unreadCount() const
{
return d->unreadMessages;
}
@@ -983,6 +1034,14 @@ void Room::updateData(SyncRoomData&& data)
for( auto&& ephemeralEvent: data.ephemeral )
processEphemeralEvent(move(ephemeralEvent));
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages)
+ {
+ qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount;
+ d->unreadMessages = data.unreadCount;
+ emit unreadMessagesChanged(this);
+ }
+
if( data.highlightCount != d->highlightCount )
{
d->highlightCount = data.highlightCount;
@@ -1303,50 +1362,38 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
if (!normalEvents.empty())
emit q->aboutToAddNewMessages(normalEvents);
const auto insertedSize = insertEvents(std::move(normalEvents), Newer);
+ const auto from = timeline.cend() - insertedSize;
if (insertedSize > 0)
{
qCDebug(MAIN)
<< "Room" << displayname << "received" << insertedSize
<< "new events; the last event is now" << timeline.back();
- q->onAddNewTimelineEvents(timeline.cend() - insertedSize);
+ q->onAddNewTimelineEvents(from);
}
for (auto&& r: redactions)
processRedaction(move(r));
if (insertedSize > 0)
{
emit q->addedMessages();
- checkUnreadMessages(timeline.cend() - insertedSize);
- }
-
- Q_ASSERT(timeline.size() == timelineSize + insertedSize);
-}
-void Room::Private::checkUnreadMessages(timeline_iter_t from)
-{
- Q_ASSERT(from < timeline.cend());
- const auto newUnreadMessages = count_if(from, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
+ // 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.
+ auto firstWriter = q->user((*from)->senderId());
+ if (q->readMarker(firstWriter) != timeline.crend())
+ {
+ promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
+ qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
+ << "to" << *q->readMarker(firstWriter);
+ }
- // 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.
- auto firstWriter = q->user((*from)->senderId());
- if (q->readMarker(firstWriter) != timeline.crend())
- {
- promoteReadMarker(firstWriter, q->findInTimeline((*from)->id()));
- qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
- << "to" << *q->readMarker(firstWriter);
+ updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
}
- if(!unreadMessages && newUnreadMessages > 0)
- {
- unreadMessages = true;
- emit q->unreadMessagesChanged(q);
- qCDebug(MAIN) << "Room" << displayname << "has unread messages";
- }
+ Q_ASSERT(timeline.size() == timelineSize + insertedSize);
}
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
@@ -1361,24 +1408,17 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
return;
emit q->aboutToAddHistoricalMessages(normalEvents);
- const bool thereWasNoReadMarker = q->readMarker() == timeline.crend();
const auto insertedSize = insertEvents(std::move(normalEvents), Older);
+ const auto from = timeline.crend() - insertedSize;
- // Catch a special case when the last read event id refers to an event
- // that was outside the loaded timeline and has just arrived. Depending on
- // other messages next to the last read one, we might need to promote
- // the read marker and update unreadMessages flag.
- const auto curReadMarker = q->readMarker();
- if (thereWasNoReadMarker && curReadMarker != timeline.crend())
- {
- qCDebug(MAIN) << "Discovered last read event in a historical batch";
- promoteReadMarker(q->localUser(), curReadMarker, true);
- }
qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize
<< "past events; the oldest event is now" << timeline.front();
- q->onAddHistoricalTimelineEvents(timeline.crend() - insertedSize);
+ q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages();
+ if (from <= q->readMarker())
+ updateUnreadCount(from, timeline.crend());
+
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
}
@@ -1563,7 +1603,7 @@ void Room::processAccountDataEvent(EventPtr event)
if (newTags == d->tags)
break;
d->tags = newTags;
- qCDebug(MAIN) << "Room" << id() << "is tagged with: "
+ qCDebug(MAIN) << "Room" << id() << "is tagged with:"
<< tagNames().join(", ");
emit tagsChanged();
break;
@@ -1572,18 +1612,14 @@ void Room::processAccountDataEvent(EventPtr event)
{
const auto* rmEvent = static_cast<ReadMarkerEvent*>(event.get());
const auto& readEventId = rmEvent->event_id();
- qCDebug(MAIN) << "Server-side read marker at " << readEventId;
- static const auto UnreadMsgsKey =
- QStringLiteral("x-qmatrixclient.unread_messages");
- if (rmEvent->contentJson().contains(UnreadMsgsKey))
- d->unreadMessages =
- rmEvent->contentJson().value(UnreadMsgsKey).toBool();
+ qCDebug(MAIN) << "Server-side read marker at" << readEventId;
d->serverReadMarker = readEventId;
const auto newMarker = findInTimeline(readEventId);
if (newMarker != timelineEdge())
d->markMessagesAsRead(newMarker);
- else
+ else {
d->setLastReadEvent(localUser(), readEventId);
+ }
break;
}
default:
@@ -1692,6 +1728,22 @@ void appendStateEvent(QJsonArray& events, const QString& type,
appendStateEvent((events), QStringLiteral(type), \
{{ QStringLiteral(name), content }});
+void appendEvent(QJsonArray& events, const QString& type,
+ const QJsonObject& content)
+{
+ if (!content.isEmpty())
+ events.append(QJsonObject
+ { { QStringLiteral("type"), type }
+ , { QStringLiteral("content"), content }
+ });
+}
+
+template <typename EvtT>
+void appendEvent(QJsonArray& events, const EvtT& event)
+{
+ appendEvent(events, EvtT::TypeId, event.toJson());
+}
+
QJsonObject Room::Private::toJson() const
{
QElapsedTimer et; et.start();
@@ -1723,40 +1775,28 @@ QJsonObject Room::Private::toJson() const
QJsonArray accountDataEvents;
if (!tags.empty())
- accountDataEvents.append(QJsonObject(
- { { QStringLiteral("type"), TagEvent::typeId() }
- , { QStringLiteral("content"), TagEvent(tags).toJson() }
- }));
+ appendEvent(accountDataEvents, TagEvent(tags));
if (!serverReadMarker.isEmpty())
- {
- auto contentJson = ReadMarkerEvent(serverReadMarker).toJson();
- contentJson.insert(QStringLiteral("x-qmatrixclient.unread_messages"),
- unreadMessages);
- accountDataEvents.append(QJsonObject(
- { { QStringLiteral("type"), ReadMarkerEvent::typeId() }
- , { QStringLiteral("content"), contentJson }
- }));
- }
+ appendEvent(accountDataEvents, ReadMarkerEvent(serverReadMarker));
if (!accountData.empty())
{
for (auto it = accountData.begin(); it != accountData.end(); ++it)
- accountDataEvents.append(QJsonObject {
- {"type", it.key()},
- {"content", QJsonObject::fromVariantHash(it.value())}
- });
+ appendEvent(accountDataEvents, it.key(),
+ QJsonObject::fromVariantHash(it.value()));
}
- QJsonObject accountDataEventsObj;
result.insert("account_data", QJsonObject {{ "events", accountDataEvents }});
QJsonObject unreadNotificationsObj;
+
+ unreadNotificationsObj.insert(SyncRoomData::UnreadCountKey, unreadMessages);
if (highlightCount > 0)
unreadNotificationsObj.insert("highlight_count", highlightCount);
if (notificationCount > 0)
unreadNotificationsObj.insert("notification_count", notificationCount);
- if (!unreadNotificationsObj.isEmpty())
- result.insert("unread_notifications", unreadNotificationsObj);
+
+ result.insert("unread_notifications", unreadNotificationsObj);
if (et.elapsed() > 50)
qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
diff --git a/room.h b/room.h
index 59566092..015e9dfc 100644
--- a/room.h
+++ b/room.h
@@ -113,6 +113,8 @@ namespace QMatrixClient
Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged)
Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved)
+ Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged)
+ Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged)
Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged)
public:
@@ -229,7 +231,26 @@ namespace QMatrixClient
*/
void markMessagesAsRead(QString uptoEventId);
- Q_INVOKABLE bool hasUnreadMessages();
+ /** Check whether there are unread messages in the room */
+ bool hasUnreadMessages() const;
+
+ /** Get the number of unread messages in the room
+ * Depending on the read marker state, this call may return either
+ * a precise or an estimate number of unread events. Only "notable"
+ * events (non-redacted message events from users other than local)
+ * are counted.
+ *
+ * In a case when readMarker() == timelineEdge() (the local read
+ * marker is beyond the local timeline) only the bottom limit of
+ * the unread messages number can be estimated (and even that may
+ * be slightly off due to, e.g., redactions of events not loaded
+ * to the local timeline).
+ *
+ * If all messages are read, this function will return -1 (_not_ 0,
+ * as zero may mean "zero or more unread messages" in a situation
+ * when the read marker is outside the local timeline.
+ */
+ int unreadCount() const;
Q_INVOKABLE int notificationCount() const;
Q_INVOKABLE void resetNotificationCount();