From 07485711f867813098180afcfe15e4af393b66ec Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 28 Feb 2018 14:46:36 +0900 Subject: Profiling logs: added µs, less empty profiling log lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #177. --- connection.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 9a5a5a4e..06da17e0 100644 --- a/connection.cpp +++ b/connection.cpp @@ -684,8 +684,7 @@ void Connection::saveState(const QUrl &toFile) const qCDebug(MAIN) << "Writing state to file" << outfile.fileName(); outfile.write(data.data(), data.size()); - qCDebug(PROFILER) << "*** Cached state for" << userId() - << "saved in" << et.elapsed() << "ms"; + qCDebug(PROFILER) << "*** Cached state for" << userId() << "saved in" << et; } void Connection::loadState(const QUrl &fromFile) @@ -722,8 +721,7 @@ void Connection::loadState(const QUrl &fromFile) SyncData sync; sync.parseJson(jsonDoc); onSyncSuccess(std::move(sync)); - qCDebug(PROFILER) << "*** Cached state for" << userId() - << "loaded in" << et.elapsed() << "ms"; + qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } QString Connection::stateCachePath() const -- cgit v1.2.3 From efd37913fdb4223168a0bb0e7897d75be4606d1f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 28 Feb 2018 16:41:43 +0900 Subject: Don't resolve the homeserver port from a user MXID Closes #176. The right way (c) of server name resolution will be done in #178 (see also the Google Doc mentioned in it). --- connection.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 06da17e0..7914639e 100644 --- a/connection.cpp +++ b/connection.cpp @@ -98,12 +98,13 @@ void Connection::resolveServer(const QString& mxidOrDomain) // Try to parse as an FQID; if there's no @ part, assume it's a domain name. QRegularExpression parser( "^(@.+?:)?" // Optional username (allow everything for compatibility) - "((\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(:\\d{1,5})?)$", // Optional port + "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address + "(:\\d{1,5})?$", // Optional port QRegularExpression::UseUnicodePropertiesOption); // Because asian digits auto match = parser.match(mxidOrDomain); QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); + maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" if (!match.hasMatch() || !maybeBaseUrl.isValid()) { emit resolveError( @@ -112,16 +113,14 @@ void Connection::resolveServer(const QString& mxidOrDomain) return; } - maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" - if (maybeBaseUrl.port() != -1) - { - setHomeserver(maybeBaseUrl); - emit resolved(); - return; - } + setHomeserver(maybeBaseUrl); + emit resolved(); + return; + // FIXME, #178: The below code is incorrect and is no more executed. The + // correct server resolution should be done from .well-known/matrix/client auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Resolving server" << domain; + qCDebug(MAIN) << "Finding the server" << domain; // Check if the Matrix server has a dedicated service record. QDnsLookup* dns = new QDnsLookup(); dns->setType(QDnsLookup::SRV); @@ -190,9 +189,8 @@ void Connection::Private::connectWithToken(const QString& user, userId = user; data->setToken(accessToken.toLatin1()); data->setDeviceId(deviceId); - qCDebug(MAIN) << "Using server" << data->baseUrl() << "by user" - << userId - << "from device" << deviceId; + qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() + << "by user" << userId << "from device" << deviceId; emit q->connected(); } -- cgit v1.2.3 From e15645d0e7afc0f45727d0d6611445fc26ef5219 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 1 Mar 2018 20:19:48 +0900 Subject: Connection::tagNames() To be able to get all available tags throughout the connection. --- connection.cpp | 11 +++++++++++ connection.h | 3 +++ 2 files changed, 14 insertions(+) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 7914639e..1c65d66f 100644 --- a/connection.cpp +++ b/connection.cpp @@ -534,6 +534,17 @@ QHash> Connection::tagsToRooms() const return result; } +QStringList Connection::tagNames() const +{ + QStringList tags ({"m.favourite"}); + for (auto* r: d->roomMap) + for (const auto& tag: r->tagNames()) + if (tag != "m.lowpriority" && !tags.contains(tag)) + tags.push_back(tag); + tags.push_back("m.lowpriority"); + return tags; +} + QVector Connection::roomsWithTag(const QString& tagName) const { QVector rooms; diff --git a/connection.h b/connection.h index b45a171d..1e9df5e2 100644 --- a/connection.h +++ b/connection.h @@ -83,6 +83,9 @@ namespace QMatrixClient */ QHash> tagsToRooms() const; + /** Get all room tags known on this connection */ + QStringList tagNames() const; + /** Get the list of rooms with the specified tag */ QVector roomsWithTag(const QString& tagName) const; -- cgit v1.2.3 From 2e6585db10d905e4e7e93fc6765240789f43fdaa Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 3 Mar 2018 23:26:29 +0900 Subject: Caching: switching to binary JSON format; yield to event loop while making a cache payload --- connection.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 1c65d66f..ad893168 100644 --- a/connection.cpp +++ b/connection.cpp @@ -636,7 +636,7 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 3; +static constexpr int CACHE_VERSION_MAJOR = 4; static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const @@ -666,12 +666,13 @@ void Connection::saveState(const QUrl &toFile) const { QJsonObject rooms; QJsonObject inviteRooms; - for (auto i : roomMap()) // Pass on rooms in Leave state + for (const auto* i : roomMap()) // Pass on rooms in Leave state { if (i->joinState() == JoinState::Invite) inviteRooms.insert(i->id(), i->toJson()); else rooms.insert(i->id(), i->toJson()); + QCoreApplication::instance()->processEvents(); } if (!rooms.isEmpty()) @@ -689,7 +690,7 @@ void Connection::saveState(const QUrl &toFile) const versionObj.insert("minor", CACHE_VERSION_MINOR); rootObj.insert("cache_version", versionObj); - QByteArray data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); + QByteArray data = QJsonDocument(rootObj).toBinaryData(); qCDebug(MAIN) << "Writing state to file" << outfile.fileName(); outfile.write(data.data(), data.size()); @@ -713,7 +714,7 @@ void Connection::loadState(const QUrl &fromFile) file.open(QFile::ReadOnly); QByteArray data = file.readAll(); - auto jsonDoc = QJsonDocument::fromJson(data); + auto jsonDoc = QJsonDocument::fromBinaryData(data); auto actualCacheVersionMajor = jsonDoc.object() .value("cache_version").toObject() -- cgit v1.2.3 From 2222bb226f43e9419c4a231c9f95da8466de4c0a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Mar 2018 15:00:32 +0900 Subject: QCoreApplication::processEvents() is static, doesn't need instance() Thanks to clang-tidy for spotting that. --- connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index ad893168..f76d9fbe 100644 --- a/connection.cpp +++ b/connection.cpp @@ -274,7 +274,7 @@ void Connection::onSyncSuccess(SyncData &&data) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) r->updateData(std::move(roomData)); - QCoreApplication::instance()->processEvents(); + QCoreApplication::processEvents(); } } @@ -672,7 +672,7 @@ void Connection::saveState(const QUrl &toFile) const inviteRooms.insert(i->id(), i->toJson()); else rooms.insert(i->id(), i->toJson()); - QCoreApplication::instance()->processEvents(); + QCoreApplication::processEvents(); } if (!rooms.isEmpty()) -- cgit v1.2.3 From fc4daac8862ad6a9824ec86628f222d74ce21201 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Mar 2018 15:22:27 +0900 Subject: Connection: allow to choose between binary and text JSON cache --- README.md | 5 +++++ connection.cpp | 29 ++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) (limited to 'connection.cpp') diff --git a/README.md b/README.md index fc9ad5e0..dc6b22ca 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ This will get you `debug/qmc-example` and `release/qmc-example` console executab ## Troubleshooting +#### Building fails + If `cmake` fails with... ``` CMake Warning at CMakeLists.txt:11 (find_package): @@ -85,3 +87,6 @@ where ``` QT_LOGGING_RULES="libqmatrixclient.*.debug=true,libqmatrixclient.jobs.debug=false" ``` + +#### Cache format +In case of troubles with room state and troubles with caching it may be useful to switch cache format from binary to JSON. To do that, set the following value in your client's configuration file/registry key (you might need to create the libqmatrixclient key for that): `libqmatrixclient/cache_type` to `json`. This will make cache saving and loading work slightly slower but the cache will be in a text JSON file (very long and unindented so prepare a good JSON viewer or text editor with JSON formatting capabilities). diff --git a/connection.cpp b/connection.cpp index f76d9fbe..c00a9b61 100644 --- a/connection.cpp +++ b/connection.cpp @@ -21,6 +21,7 @@ #include "user.h" #include "events/event.h" #include "room.h" +#include "settings.h" #include "jobs/generated/login.h" #include "jobs/generated/logout.h" #include "jobs/generated/receipts.h" @@ -68,6 +69,8 @@ class Connection::Private SyncJob* syncJob = nullptr; bool cacheState = true; + bool cacheToBinary = SettingsGroup("libqmatrixclient") + .value("cache_type").toString() != "json"; void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); @@ -636,7 +639,7 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 4; +static constexpr int CACHE_VERSION_MAJOR = 5; static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const @@ -690,11 +693,13 @@ void Connection::saveState(const QUrl &toFile) const versionObj.insert("minor", CACHE_VERSION_MINOR); rootObj.insert("cache_version", versionObj); - QByteArray data = QJsonDocument(rootObj).toBinaryData(); + QJsonDocument json { rootObj }; + auto data = d->cacheToBinary ? json.toBinaryData() : + json.toJson(QJsonDocument::Compact); + qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - qCDebug(MAIN) << "Writing state to file" << outfile.fileName(); outfile.write(data.data(), data.size()); - qCDebug(PROFILER) << "*** Cached state for" << userId() << "saved in" << et; + qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); } void Connection::loadState(const QUrl &fromFile) @@ -714,17 +719,23 @@ void Connection::loadState(const QUrl &fromFile) file.open(QFile::ReadOnly); QByteArray data = file.readAll(); - auto jsonDoc = QJsonDocument::fromBinaryData(data); + QJsonParseError e; + auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : + QJsonDocument::fromJson(data, &e); + if (e.error == QJsonParseError::NoError) + { + qCWarning(MAIN) << "Cache file not found or broken, discarding"; + return; + } auto actualCacheVersionMajor = jsonDoc.object() .value("cache_version").toObject() .value("major").toInt(); if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) { - qCWarning(MAIN) << "Major version of the cache file is" - << actualCacheVersionMajor << "but" - << CACHE_VERSION_MAJOR - << "required; discarding the cache"; + qCWarning(MAIN) + << "Major version of the cache file is" << actualCacheVersionMajor + << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; return; } -- cgit v1.2.3 From ff85bba352ee23d6e2689804da40a9433a117e71 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 5 Mar 2018 10:22:00 +0900 Subject: Fix a typo leading to cache never working --- connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index c00a9b61..a57bd8b4 100644 --- a/connection.cpp +++ b/connection.cpp @@ -722,7 +722,7 @@ void Connection::loadState(const QUrl &fromFile) QJsonParseError e; auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : QJsonDocument::fromJson(data, &e); - if (e.error == QJsonParseError::NoError) + if (e.error != QJsonParseError::NoError) { qCWarning(MAIN) << "Cache file not found or broken, discarding"; return; -- cgit v1.2.3 From 9057fc02b06bdd3e38e9cf39e68287e02d58596b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 5 Mar 2018 13:17:27 +0900 Subject: Use constants instead of hardcoded strings --- connection.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index a57bd8b4..80685dd1 100644 --- a/connection.cpp +++ b/connection.cpp @@ -539,12 +539,12 @@ QHash> Connection::tagsToRooms() const QStringList Connection::tagNames() const { - QStringList tags ({"m.favourite"}); + QStringList tags ({FavouriteTag}); for (auto* r: d->roomMap) for (const auto& tag: r->tagNames()) - if (tag != "m.lowpriority" && !tags.contains(tag)) + if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); - tags.push_back("m.lowpriority"); + tags.push_back(LowPriorityTag); return tags; } -- cgit v1.2.3 From 9a3a7e39cb8e88ddfd929647325dc919d0a02a1b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 5 Mar 2018 18:30:53 +0900 Subject: Profile-log if saving state has been interrupted by processEvents for long enough --- connection.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 80685dd1..7ba11145 100644 --- a/connection.cpp +++ b/connection.cpp @@ -675,7 +675,10 @@ void Connection::saveState(const QUrl &toFile) const inviteRooms.insert(i->id(), i->toJson()); else rooms.insert(i->id(), i->toJson()); + QElapsedTimer et1; et1.start(); QCoreApplication::processEvents(); + if (et1.elapsed() > 1) + qCDebug(PROFILER) << "processEvents() borrowed" << et1; } if (!rooms.isEmpty()) -- cgit v1.2.3 From 17d3333a8952e3bd2491080081d00a251157e34a Mon Sep 17 00:00:00 2001 From: Lewis Rockliffe Date: Sun, 18 Mar 2018 13:08:28 +0000 Subject: fix error handling in loadState() --- connection.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 7ba11145..b3f1ceb8 100644 --- a/connection.cpp +++ b/connection.cpp @@ -719,15 +719,18 @@ void Connection::loadState(const QUrl &fromFile) qCDebug(MAIN) << "No state cache file found"; return; } - file.open(QFile::ReadOnly); + if(!file.open(QFile::ReadOnly)) + { + qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; + return; + } QByteArray data = file.readAll(); - QJsonParseError e; auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data, &e); - if (e.error != QJsonParseError::NoError) + QJsonDocument::fromJson(data); + if (jsonDoc.isNull()) { - qCWarning(MAIN) << "Cache file not found or broken, discarding"; + qCWarning(MAIN) << "Cache file broken, discarding"; return; } auto actualCacheVersionMajor = -- cgit v1.2.3 From f7ee38b6c1d6c809feddd91647d83e6c6fdd3837 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Mar 2018 15:40:54 +0900 Subject: Dealing with direct chats Receiving and caching m.direct (and other non-room account data along the way); Connection::addToDirectChats/removeFromDirectChats (might get siblings in Room eventually but not now), Connection/User::requestDirectChat. Closes #163. --- connection.cpp | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- connection.h | 49 +++++++++++++++++- room.cpp | 5 ++ user.cpp | 6 +++ user.h | 1 + 5 files changed, 204 insertions(+), 13 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index b3f1ceb8..fb946392 100644 --- a/connection.cpp +++ b/connection.cpp @@ -20,12 +20,14 @@ #include "connectiondata.h" #include "user.h" #include "events/event.h" +#include "events/directchatevent.h" #include "room.h" #include "settings.h" #include "jobs/generated/login.h" #include "jobs/generated/logout.h" #include "jobs/generated/receipts.h" #include "jobs/generated/leaving.h" +#include "jobs/generated/account-data.h" #include "jobs/sendeventjob.h" #include "jobs/joinroomjob.h" #include "jobs/roommessagesjob.h" @@ -45,6 +47,8 @@ using namespace QMatrixClient; +using DirectChatsMap = QMultiHash; + class Connection::Private { public: @@ -64,6 +68,8 @@ class Connection::Private QHash, Room*> roomMap; QVector roomIdsToForget; QMap userMap; + DirectChatsMap directChats; + QHash accountData; QString userId; SyncJob* syncJob = nullptr; @@ -74,6 +80,7 @@ class Connection::Private void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); + void applyDirectChatUpdates(const DirectChatsMap& newMap); }; Connection::Connection(const QUrl& server, QObject* parent) @@ -258,7 +265,7 @@ void Connection::sync(int timeout) void Connection::onSyncSuccess(SyncData &&data) { d->data->setLastEvent(data.nextBatch()); - for( auto&& roomData: data.takeRoomData() ) + for (auto&& roomData: data.takeRoomData()) { const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); if (forgetIdx != -1) @@ -279,7 +286,29 @@ void Connection::onSyncSuccess(SyncData &&data) { r->updateData(std::move(roomData)); QCoreApplication::processEvents(); } - + for (auto&& accountEvent: data.takeAccountData()) + { + if (accountEvent->type() == EventType::DirectChat) + { + DirectChatsMap newDirectChats; + const auto* event = static_cast(accountEvent.get()); + auto usersToDCs = event->usersToDirectChats(); + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) + { + newDirectChats.insert(user(it.key()), it.value()); + qCDebug(MAIN) << "Marked room" << it.value() + << "as a direct chat with" << it.key(); + } + if (newDirectChats != d->directChats) + { + d->directChats = newDirectChats; + emit directChatsListChanged(); + } + continue; + } + d->accountData[accountEvent->jsonType()] = + accountEvent->contentJson().toVariantHash(); + } } void Connection::stopSync() @@ -405,6 +434,42 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, return job; } +void Connection::requestDirectChat(const QString& userId) +{ + auto roomId = d->directChats.value(user(userId)); + if (roomId.isEmpty()) + { + auto j = createDirectChat(userId); + connect(j, &BaseJob::success, this, [this,j,userId,roomId] { + qCDebug(MAIN) << "Direct chat with" << userId + << "has been created as" << roomId; + emit directChatAvailable(roomMap().value({j->roomId(), false})); + }); + return; + } + + auto room = roomMap().value({roomId, false}, nullptr); + if (room) + { + Q_ASSERT(room->id() == roomId); + qCDebug(MAIN) << "Requested direct chat with" << userId + << "is already available as" << room->id(); + emit directChatAvailable(room); + return; + } + room = roomMap().value({roomId, true}, nullptr); + if (room) + { + Q_ASSERT(room->id() == roomId); + auto j = joinRoom(room->id()); + connect(j, &BaseJob::success, this, [this,j,roomId,userId] { + qCDebug(MAIN) << "Joined the already invited direct chat with" + << userId << "as" << roomId; + emit directChatAvailable(roomMap().value({roomId, false})); + }); + } +} + CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { @@ -470,11 +535,14 @@ User* Connection::user(const QString& userId) return user; } -User *Connection::user() +const User* Connection::user() const { - if( d->userId.isEmpty() ) - return nullptr; - return user(d->userId); + return d->userId.isEmpty() ? nullptr : d->userMap.value(d->userId, nullptr); +} + +User* Connection::user() +{ + return d->userId.isEmpty() ? nullptr : user(d->userId); } QString Connection::userId() const @@ -556,6 +624,56 @@ QVector Connection::roomsWithTag(const QString& tagName) const return rooms; } +QJsonObject toJson(const DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it) + json.insert((*it)->id(), toJson(directChats.values(*it))); + return json; +} + +void Connection::Private::applyDirectChatUpdates(const DirectChatsMap& newMap) +{ + auto j = q->callApi(userId, "m.direct", toJson(newMap)); + connect(j, &BaseJob::success, q, [this, newMap] { + if (directChats != newMap) + { + directChats = newMap; + emit q->directChatsListChanged(); + } + }); +} + +void Connection::addToDirectChats(const Room* room, const User* user) +{ + Q_ASSERT(room != nullptr && user != nullptr); + if (d->directChats.contains(user, room->id())) + return; + auto newMap = d->directChats; + newMap.insert(user, room->id()); + d->applyDirectChatUpdates(newMap); +} + +void Connection::removeFromDirectChats(const Room* room, const User* user) +{ + Q_ASSERT(room != nullptr); + if ((user != nullptr && !d->directChats.contains(user, room->id())) || + d->directChats.key(room->id()) == nullptr) + return; + DirectChatsMap newMap; + for (auto it = d->directChats.begin(); it != d->directChats.end(); ++it) + { + if (it.value() != room->id() || (user != nullptr && it.key() != user)) + newMap.insert(it.key(), it.value()); + } + d->applyDirectChatUpdates(newMap); +} + +bool Connection::isDirectChat(const Room* room) const +{ + return d->directChats.key(room->id()) != nullptr; +} + QMap Connection::users() const { return d->userMap; @@ -639,7 +757,7 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 5; +static constexpr int CACHE_VERSION_MAJOR = 6; static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const @@ -665,7 +783,7 @@ void Connection::saveState(const QUrl &toFile) const return; } - QJsonObject roomObj; + QJsonObject rootObj; { QJsonObject rooms; QJsonObject inviteRooms; @@ -681,15 +799,31 @@ void Connection::saveState(const QUrl &toFile) const qCDebug(PROFILER) << "processEvents() borrowed" << et1; } + QJsonObject roomObj; if (!rooms.isEmpty()) roomObj.insert("join", rooms); if (!inviteRooms.isEmpty()) roomObj.insert("invite", inviteRooms); + + rootObj.insert("next_batch", d->data->lastEvent()); + rootObj.insert("rooms", roomObj); } + { + QJsonArray accountDataEvents { + QJsonObject { + { QStringLiteral("type"), QStringLiteral("m.direct") }, + { QStringLiteral("content"), toJson(d->directChats) } + } + }; - QJsonObject rootObj; - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); + for (auto it = d->accountData.begin(); it != d->accountData.end(); ++it) + accountDataEvents.append(QJsonObject { + {"type", it.key()}, + {"content", QJsonObject::fromVariantHash(it.value())} + }); + rootObj.insert("account_data", + QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); + } QJsonObject versionObj; versionObj.insert("major", CACHE_VERSION_MAJOR); diff --git a/connection.h b/connection.h index 1e9df5e2..e046d4a0 100644 --- a/connection.h +++ b/connection.h @@ -89,12 +89,26 @@ namespace QMatrixClient /** Get the list of rooms with the specified tag */ QVector roomsWithTag(const QString& tagName) const; + /** Mark the room as a direct chat with the user */ + void addToDirectChats(const Room* room, const User* user); + + /** Unmark the room from direct chats + * This function removes the room from direct chats either for + * a specific \p user or for all users if \p user in nullptr. + */ + void removeFromDirectChats(const Room* room, + const User* user = nullptr); + + /** Check whether the room is a direct chat */ + bool isDirectChat(const Room* room) const; + QMap users() const; // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES // (breaks back-compatibility) QUrl homeserver() const; Q_INVOKABLE User* user(const QString& userId); + const User* user() const; User* user(); QString userId() const; QString deviceId() const; @@ -223,7 +237,21 @@ namespace QMatrixClient const QVector& invite3pids = {}, const QJsonObject creationContent = {}); - /** Create a direct chat with a single user, optional name and topic */ + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. + * + * \sa directChatAvailable + */ + Q_INVOKABLE void requestDirectChat(const QString& userId); + + /** Create a direct chat with a single user, optional name and topic + * A room will always be created, unlike in requestDirectChat. + * It is advised to use requestDirectChat as a default way of getting + * one-on-one with a person, and only use createDirectChat when + * a new creation is explicitly desired. + */ CreateRoomJob* createDirectChat(const QString& userId, const QString& topic = {}, const QString& name = {}); @@ -341,13 +369,30 @@ namespace QMatrixClient /** The room object is about to be deleted */ void aboutToDeleteRoom(Room* room); - /** The room has just been created by createRoom or createDirectChat + /** The room has just been created by createRoom or requestDirectChat + * * This signal is not emitted in usual room state transitions, * only as an outcome of room creation operations invoked by * the client. + * \note requestDirectChat doesn't necessarily create a new chat; + * use directChatAvailable signal if you just need to obtain + * a direct chat room. */ void createdRoom(Room* room); + /** The direct chat room is ready for using + * This signal is emitted upon any successful outcome from + * requestDirectChat. + */ + void directChatAvailable(Room* directChat); + + /** The list of direct chats has changed + * This signal is emitted every time when the mapping of users + * to direct chat rooms is changed (because of either local updates + * or a different list arrived from the server). + */ + void directChatsListChanged(); + void cacheStateChanged(); protected: diff --git a/room.cpp b/room.cpp index 6c8d762b..48c27ba0 100644 --- a/room.cpp +++ b/room.cpp @@ -1424,6 +1424,11 @@ void Room::processStateEvents(const RoomEvents& events) auto memberEvent = static_cast(event); auto u = user(memberEvent->userId()); u->processEvent(memberEvent, this); + if (u == localUser() && memberJoinState(u) == JoinState::Invite + && memberEvent->isDirect()) + connection()->addToDirectChats(this, + user(memberEvent->senderId())); + if( memberEvent->membership() == MembershipType::Join ) { if (memberJoinState(u) != JoinState::Join) diff --git a/user.cpp b/user.cpp index cfcb2f4d..7a6dbc73 100644 --- a/user.cpp +++ b/user.cpp @@ -287,6 +287,12 @@ bool User::setAvatar(QIODevice* source) std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); } +void User::requestDirectChat() +{ + Q_ASSERT(d->connection); + d->connection->requestDirectChat(d->userId); +} + void User::Private::setAvatarOnServer(QString contentUri, User* q) { auto* j = connection->callApi(userId, contentUri); diff --git a/user.h b/user.h index d19fa8f4..f76f9e0a 100644 --- a/user.h +++ b/user.h @@ -101,6 +101,7 @@ namespace QMatrixClient void rename(const QString& newName, const Room* r); bool setAvatar(const QString& fileName); bool setAvatar(QIODevice* source); + void requestDirectChat(); signals: void nameAboutToChange(QString newName, QString oldName, -- cgit v1.2.3 From e9cdf6c887b80210dcb6d754c1db82d3fad2ec06 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 26 Mar 2018 11:45:46 +0900 Subject: Connection::room() and Connection::invitation() --- connection.cpp | 23 +++++++++++++++++++++++ connection.h | 3 +++ 2 files changed, 26 insertions(+) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index fb946392..df9fd35f 100644 --- a/connection.cpp +++ b/connection.cpp @@ -525,6 +525,29 @@ QUrl Connection::homeserver() const return d->data->baseUrl(); } +Room* Connection::room(const QString& roomId, JoinStates states) const +{ + Room* room = d->roomMap.value({roomId, false}, nullptr); + if (states.testFlag(JoinState::Join) && + room && room->joinState() == JoinState::Join) + return room; + + if (states.testFlag(JoinState::Invite)) + if (Room* invRoom = invitation(roomId)) + return invRoom; + + if (states.testFlag(JoinState::Leave) && + room && room->joinState() == JoinState::Leave) + return room; + + return nullptr; +} + +Room* Connection::invitation(const QString& roomId) const +{ + return d->roomMap.value({roomId, true}, nullptr); +} + User* Connection::user(const QString& userId) { if( d->userMap.contains(userId) ) diff --git a/connection.h b/connection.h index e046d4a0..4497e200 100644 --- a/connection.h +++ b/connection.h @@ -107,6 +107,9 @@ namespace QMatrixClient // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES // (breaks back-compatibility) QUrl homeserver() const; + Q_INVOKABLE Room* room(const QString& roomId, + JoinStates states = JoinState::Invite|JoinState::Join) const; + Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; User* user(); -- cgit v1.2.3 From b385baadc8e73ff3c499a0111e2a553d35dd29b6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 26 Mar 2018 12:30:23 -0700 Subject: Direct chat (un)marking: update internal structure synchronously The asynchronous update first implemented was more verbose and caused more problems than provided solutions. The idea was that the internal directChats map would better reflect the server state if updated asynchronously. However, it also causes a local race condition; e.g., to quickly remove rooms from direct chats one after another becomes very non-trivial (one has to wait until the previous operation succeeds). So after some playing with the code, hitting pitfalls along the way, I decided to align the logic with the one for room tags; synchronously issued signals look uglyish but at least work predictably. And race conditions between several clients generally cannot be cleanly resolved anyway. --- connection.cpp | 50 +++++++++++++++++++++++------------------------- connection.h | 22 ++++++++++++++++----- examples/qmc-example.cpp | 31 +++++++++++------------------- 3 files changed, 52 insertions(+), 51 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index df9fd35f..b32f38ea 100644 --- a/connection.cpp +++ b/connection.cpp @@ -80,7 +80,7 @@ class Connection::Private void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); - void applyDirectChatUpdates(const DirectChatsMap& newMap); + void broadcastDirectChatUpdates(); }; Connection::Connection(const QUrl& server, QObject* parent) @@ -655,16 +655,11 @@ QJsonObject toJson(const DirectChatsMap& directChats) return json; } -void Connection::Private::applyDirectChatUpdates(const DirectChatsMap& newMap) +void Connection::Private::broadcastDirectChatUpdates() { - auto j = q->callApi(userId, "m.direct", toJson(newMap)); - connect(j, &BaseJob::success, q, [this, newMap] { - if (directChats != newMap) - { - directChats = newMap; - emit q->directChatsListChanged(); - } - }); + auto j = q->callApi(userId, QStringLiteral("m.direct"), + toJson(directChats)); + emit q->directChatsListChanged(); } void Connection::addToDirectChats(const Room* room, const User* user) @@ -672,29 +667,32 @@ void Connection::addToDirectChats(const Room* room, const User* user) Q_ASSERT(room != nullptr && user != nullptr); if (d->directChats.contains(user, room->id())) return; - auto newMap = d->directChats; - newMap.insert(user, room->id()); - d->applyDirectChatUpdates(newMap); + d->directChats.insert(user, room->id()); + d->broadcastDirectChatUpdates(); } -void Connection::removeFromDirectChats(const Room* room, const User* user) +void Connection::removeFromDirectChats(const QString& roomId, const User* user) { - Q_ASSERT(room != nullptr); - if ((user != nullptr && !d->directChats.contains(user, room->id())) || - d->directChats.key(room->id()) == nullptr) + Q_ASSERT(!roomId.isEmpty()); + if ((user != nullptr && !d->directChats.contains(user, roomId)) || + d->directChats.key(roomId) == nullptr) return; - DirectChatsMap newMap; - for (auto it = d->directChats.begin(); it != d->directChats.end(); ++it) - { - if (it.value() != room->id() || (user != nullptr && it.key() != user)) - newMap.insert(it.key(), it.value()); - } - d->applyDirectChatUpdates(newMap); + if (user != nullptr) + d->directChats.remove(user, roomId); + else + for (auto it = d->directChats.begin(); it != d->directChats.end();) + { + if (it.value() == roomId) + it = d->directChats.erase(it); + else + ++it; + } + d->broadcastDirectChatUpdates(); } -bool Connection::isDirectChat(const Room* room) const +bool Connection::isDirectChat(const QString& roomId) const { - return d->directChats.key(room->id()) != nullptr; + return d->directChats.key(roomId) != nullptr; } QMap Connection::users() const diff --git a/connection.h b/connection.h index 4497e200..6a5285f9 100644 --- a/connection.h +++ b/connection.h @@ -89,18 +89,30 @@ namespace QMatrixClient /** Get the list of rooms with the specified tag */ QVector roomsWithTag(const QString& tagName) const; - /** Mark the room as a direct chat with the user */ + /** Mark the room as a direct chat with the user + * This function marks \p room as a direct chat with \p user. + * Emits the signal synchronously, without waiting to complete + * synchronisation with the server. + * + * \sa directChatsListChanged + */ void addToDirectChats(const Room* room, const User* user); /** Unmark the room from direct chats - * This function removes the room from direct chats either for + * This function removes the room id from direct chats either for * a specific \p user or for all users if \p user in nullptr. + * The room id is used to allow removal of, e.g., ids of forgotten + * rooms; a Room object need not exist. Emits the signal + * immediately, without waiting to complete synchronisation with + * the server. + * + * \sa directChatsListChanged */ - void removeFromDirectChats(const Room* room, + void removeFromDirectChats(const QString& roomId, const User* user = nullptr); - /** Check whether the room is a direct chat */ - bool isDirectChat(const Room* room) const; + /** Check whether the room id corresponds to a direct chat */ + bool isDirectChat(const QString& roomId) const; QMap users() const; diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 5ea91856..23a1bff1 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -126,7 +126,8 @@ void QMCTest::addAndRemoveTag() if (targetRoom->tags().contains(TestTag)) targetRoom->removeTag(TestTag); - QObject::connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { + // Connect first because the signal is emitted synchronously. + connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { cout << "Room " << targetRoom->id().toStdString() << ", tag(s) changed:" << endl << " " << targetRoom->tagNames().join(", ").toStdString() << endl; @@ -138,7 +139,6 @@ void QMCTest::addAndRemoveTag() QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); } }); - // The reverse order because tagsChanged is emitted synchronously. cout << "Adding a tag" << endl; targetRoom->addTag(TestTag); } @@ -211,40 +211,31 @@ void QMCTest::checkRedactionOutcome(QString evtIdToRedact, void QMCTest::markDirectChat() { - if (c->isDirectChat(targetRoom)) + if (c->isDirectChat(targetRoom->id())) { cout << "Warning: the room is already a direct chat," " only unmarking will be tested" << endl; checkDirectChatOutcome(); } - cout << "Marking the room as a direct chat" << endl; - c->addToDirectChats(targetRoom, c->user()); + // Connect first because the signal is emitted synchronously. connect(c.data(), &Connection::directChatsListChanged, this, &QMCTest::checkDirectChatOutcome); + cout << "Marking the room as a direct chat" << endl; + c->addToDirectChats(targetRoom, c->user()); } void QMCTest::checkDirectChatOutcome() { - if (!c->isDirectChat(targetRoom)) + disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); + if (!c->isDirectChat(targetRoom->id())) { - cout << "Room not (yet?) added to direct chats, waiting" << endl; + QMC_CHECK("Direct chat test", false); return; } cout << "Room marked as a direct chat, unmarking now" << endl; - disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); - c->removeFromDirectChats(targetRoom, c->user()); - connect(c.data(), &Connection::directChatsListChanged, this, [this] { - if (c->isDirectChat(targetRoom)) - { - cout << "Room not (yet?) removed from direct chats, waiting" << endl; - return; - } - - QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom)); - disconnect(c.data(), &Connection::directChatsListChanged, - nullptr, nullptr); - }); + c->removeFromDirectChats(targetRoom->id(), c->user()); + QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); } void QMCTest::finalize() -- cgit v1.2.3 From 381ab25563cce26be8e3983b3fb3b8090385a766 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 26 Mar 2018 12:35:49 +0900 Subject: Connection::doInDirectChat() and refactored direct chat requesting logic Basically, the whole requestDirectChat() body has been moved and generalised to doInDirectChat(), and requestDirectChat() delegates to doInDirectChat(). The logic has been updated to cope with formerly left/forgotten rooms present in the list of direct chats (cleaning up the list along the way). --- connection.cpp | 65 ++++++++++++++++++++++++++++++++-------------------------- connection.h | 9 ++++++++ 2 files changed, 45 insertions(+), 29 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index b32f38ea..2a748eb1 100644 --- a/connection.cpp +++ b/connection.cpp @@ -436,38 +436,45 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, void Connection::requestDirectChat(const QString& userId) { - auto roomId = d->directChats.value(user(userId)); - if (roomId.isEmpty()) - { - auto j = createDirectChat(userId); - connect(j, &BaseJob::success, this, [this,j,userId,roomId] { - qCDebug(MAIN) << "Direct chat with" << userId - << "has been created as" << roomId; - emit directChatAvailable(roomMap().value({j->roomId(), false})); - }); - return; - } + doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); }); +} - auto room = roomMap().value({roomId, false}, nullptr); - if (room) - { - Q_ASSERT(room->id() == roomId); - qCDebug(MAIN) << "Requested direct chat with" << userId - << "is already available as" << room->id(); - emit directChatAvailable(room); - return; - } - room = roomMap().value({roomId, true}, nullptr); - if (room) +void Connection::doInDirectChat(const QString& userId, + std::function operation) +{ + // There can be more than one DC; find the first valid, and delete invalid + // (left/forgotten) ones along the way. + for (auto roomId: d->directChats.values(user(userId))) { - Q_ASSERT(room->id() == roomId); - auto j = joinRoom(room->id()); - connect(j, &BaseJob::success, this, [this,j,roomId,userId] { - qCDebug(MAIN) << "Joined the already invited direct chat with" - << userId << "as" << roomId; - emit directChatAvailable(roomMap().value({roomId, false})); - }); + if (auto r = room(roomId, JoinState::Join)) + { + Q_ASSERT(r->id() == roomId); + qCDebug(MAIN) << "Requested direct chat with" << userId + << "is already available as" << r->id(); + operation(r); + return; + } + if (auto ir = invitation(roomId)) + { + Q_ASSERT(ir->id() == roomId); + auto j = joinRoom(ir->id()); + connect(j, &BaseJob::success, this, [this,roomId,userId,operation] { + qCDebug(MAIN) << "Joined the already invited direct chat with" + << userId << "as" << roomId; + operation(room(roomId, JoinState::Join)); + }); + } + qCWarning(MAIN) << "Direct chat with" << userId << "known as room" + << roomId << "is not valid, discarding it"; + removeFromDirectChats(roomId); } + + auto j = createDirectChat(userId); + connect(j, &BaseJob::success, this, [this,j,userId,operation] { + qCDebug(MAIN) << "Direct chat with" << userId + << "has been created as" << j->roomId(); + operation(room(j->roomId(), JoinState::Join)); + }); } CreateRoomJob* Connection::createDirectChat(const QString& userId, diff --git a/connection.h b/connection.h index 6a5285f9..7c11c32d 100644 --- a/connection.h +++ b/connection.h @@ -261,6 +261,15 @@ namespace QMatrixClient */ Q_INVOKABLE void requestDirectChat(const QString& userId); + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + Q_INVOKABLE void doInDirectChat(const QString& userId, + std::function operation); + /** Create a direct chat with a single user, optional name and topic * A room will always be created, unlike in requestDirectChat. * It is advised to use requestDirectChat as a default way of getting -- cgit v1.2.3 From a73bb17418a6dfea556a837c48463e454c8db130 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 25 Mar 2018 22:06:46 -0700 Subject: Minor cleanup --- connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index 2a748eb1..db8bb085 100644 --- a/connection.cpp +++ b/connection.cpp @@ -664,8 +664,8 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::Private::broadcastDirectChatUpdates() { - auto j = q->callApi(userId, QStringLiteral("m.direct"), - toJson(directChats)); + q->callApi(userId, QStringLiteral("m.direct"), + toJson(directChats)); emit q->directChatsListChanged(); } -- cgit v1.2.3 From de05579f18a5c264fe0dbea68ea83abcff526d00 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 26 Mar 2018 08:03:16 -0400 Subject: Connection::directChatUsers() --- connection.cpp | 6 ++++++ connection.h | 6 ++++++ 2 files changed, 12 insertions(+) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index db8bb085..e4e622a3 100644 --- a/connection.cpp +++ b/connection.cpp @@ -702,6 +702,12 @@ bool Connection::isDirectChat(const QString& roomId) const return d->directChats.key(roomId) != nullptr; } +QList Connection::directChatUsers(const Room* room) const +{ + Q_ASSERT(room != nullptr); + return d->directChats.keys(room->id()); +} + QMap Connection::users() const { return d->userMap; diff --git a/connection.h b/connection.h index 7c11c32d..c6d543ec 100644 --- a/connection.h +++ b/connection.h @@ -114,6 +114,12 @@ namespace QMatrixClient /** Check whether the room id corresponds to a direct chat */ bool isDirectChat(const QString& roomId) const; + /** Retrieve the list of users the room is a direct chat with + * @return The list of users for which this room is marked as + * a direct chat; an empty list if the room is not a direct chat + */ + QList directChatUsers(const Room* room) const; + QMap users() const; // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES -- cgit v1.2.3 From f213e02daa6b9e83e8e76d1576e446357c6c3bc7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 27 Mar 2018 20:34:46 +0900 Subject: Rework unread messages counting logic The previous one didn't cover all the cases; the current one seems to do. Closes #192. Accompanied by the developer's notes at: https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count --- connection.cpp | 2 +- jobs/syncjob.cpp | 22 ++++-- jobs/syncjob.h | 3 + room.cpp | 237 ++++++++++++++++++++++++++----------------------------- room.h | 24 +++++- 5 files changed, 153 insertions(+), 135 deletions(-) (limited to 'connection.cpp') 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; diff --git a/room.cpp b/room.cpp index d7014a10..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 members_map_t; - typedef std::pair rev_iter_pair_t; Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(std::move(id_)) @@ -113,8 +112,6 @@ class Room::Private QString prevBatch; QPointer roomMessagesJob; - static const QString UnreadMsgsKey; - struct FileTransferPrivateInfo { #ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST @@ -192,12 +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); - void updateUnreadCount(timeline_iter_t from, int knownMinimum = 0); - void addUnreadCount(timeline_iter_t from, timeline_iter_t to); - 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); @@ -230,9 +225,6 @@ class Room::Private } }; -const QString Room::Private::UnreadMsgsKey = - QStringLiteral("x-qmatrixclient.unread_messages_count"); - RoomEventPtr TimelineItem::replaceEvent(RoomEventPtr&& other) { return std::exchange(evt, std::move(other)); @@ -366,57 +358,55 @@ void Room::Private::setLastReadEvent(User* u, const QString& eventId) } } -void Room::Private::updateUnreadCount(timeline_iter_t from, int knownMinimum) +void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) { - Q_ASSERT(from >= q->readMarker().base() && from <= timeline.cend()); - auto oldUnreadCount = unreadMessages; - QElapsedTimer et; et.start(); - // A cast to int, because on some environments count_if returns a long; - // but Qt can only handle int's for container indices etc. - unreadMessages = std::max(knownMinimum, - int(count_if(from, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)))); - if (et.elapsed() > 10) - qCDebug(PROFILER) << "Recounting unread messages took" << et; - - if (unreadMessages != oldUnreadCount) + 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) << "Room" << displayname - << (q->readMarker() == timeline.crend() ? "has at least" : "has") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); + qCDebug(MAIN) << "Discovered last read event in room" << displayname; + promoteReadMarker(q->localUser(), readMarker, true); + return; } -} -void Room::Private::addUnreadCount(timeline_iter_t from, timeline_iter_t to) -{ + Q_ASSERT(to <= readMarker); + QElapsedTimer et; et.start(); const auto newUnreadMessages = count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.elapsed() > 10) + 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 messages, total unread" - << (q->readMarker() == timeline.crend() ? " at least" : "") - << unreadMessages << "message(s)"; + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() ? + "in total at least" : "in total") + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } } -Room::Private::rev_iter_pair_t -Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker, - bool force) +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()); @@ -428,35 +418,47 @@ Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker, setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { - updateUnreadCount(eagerMarker); + 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) { - qCDebug(MAIN) << "Room" << displayname << "has no more unread messages"; - } else - qCDebug(MAIN) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; + 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); + } } - - // 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(id, "m.read", - (*markers.second)->id()); + (*upToMarker)->id()); break; } } @@ -475,10 +477,10 @@ void Room::markAllMessagesAsRead() bool Room::hasUnreadMessages() const { - return unreadMessagesCount() > 0; + return unreadCount() >= 0; } -int Room::unreadMessagesCount() const +int Room::unreadCount() const { return d->unreadMessages; } @@ -1032,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; @@ -1352,42 +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); -} + // 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); + } -void Room::Private::checkUnreadMessages(timeline_iter_t from) -{ - Q_ASSERT(from < timeline.cend()); - // 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)); } - addUnreadCount(from, timeline.cend()); + Q_ASSERT(timeline.size() == timelineSize + insertedSize); } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1402,30 +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. - const auto curReadMarker = q->readMarker(); - if (thereWasNoReadMarker) - { - if (curReadMarker != timeline.crend()) - { - qCDebug(MAIN) << "Discovered last read event in a historical batch"; - promoteReadMarker(q->localUser(), curReadMarker, true); - } - else - addUnreadCount(timeline.cbegin(), - timeline.cbegin() + insertedSize); - } 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); } @@ -1610,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; @@ -1619,22 +1612,13 @@ void Room::processAccountDataEvent(EventPtr event) { const auto* rmEvent = static_cast(event.get()); const auto& readEventId = rmEvent->event_id(); - qCDebug(MAIN) << "Server-side read marker at " << readEventId; + qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); if (newMarker != timelineEdge()) d->markMessagesAsRead(newMarker); else { d->setLastReadEvent(localUser(), readEventId); - // No accurate information about the read marker whereabouts. - // There two sources of information; the (difference between - // the old and the) new read marker and the contents of - // UnreadMsgsKey. - if (rmEvent->contentJson().contains(d->UnreadMsgsKey)) - d->unreadMessages = - rmEvent->contentJson().value(d->UnreadMsgsKey).toInt(); - - d->updateUnreadCount(timelineEdge().base(), d->unreadMessages); } break; } @@ -1744,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 +void appendEvent(QJsonArray& events, const EvtT& event) +{ + appendEvent(events, EvtT::TypeId, event.toJson()); +} + QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); @@ -1775,39 +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(UnreadMsgsKey, 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 c153bfc3..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,8 +231,26 @@ namespace QMatrixClient */ void markMessagesAsRead(QString uptoEventId); - Q_INVOKABLE bool hasUnreadMessages() const; - int unreadMessagesCount() const; + /** 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(); -- cgit v1.2.3 From e7868adbf5b275f66529fb2dae323ed8aeb69e05 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 29 Mar 2018 08:53:40 +0900 Subject: Room: Track encryption state and do not allow sending (unencrypted) messages to it Closes #138. This is of course not encryption support yet, rather a safeguard for rooms that use encryption. --- connection.cpp | 4 ++-- room.cpp | 21 +++++++++++++++++++++ room.h | 3 +++ 3 files changed, 26 insertions(+), 2 deletions(-) (limited to 'connection.cpp') diff --git a/connection.cpp b/connection.cpp index ee719b40..2d7235b9 100644 --- a/connection.cpp +++ b/connection.cpp @@ -791,8 +791,8 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 6; -static constexpr int CACHE_VERSION_MINOR = 1; +static constexpr int CACHE_VERSION_MAJOR = 7; +static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const { diff --git a/room.cpp b/room.cpp index 9dec2b4a..25669889 100644 --- a/room.cpp +++ b/room.cpp @@ -94,6 +94,7 @@ class Room::Private QString name; QString displayname; QString topic; + QString encryptionAlgorithm; Avatar avatar; JoinState joinState; int highlightCount = 0; @@ -883,6 +884,11 @@ int Room::timelineSize() const return int(d->timeline.size()); } +bool Room::usesEncryption() const +{ + return !d->encryptionAlgorithm.isEmpty(); +} + void Room::Private::insertMemberIntoMap(User *u) { const auto userName = u->name(q); @@ -1066,6 +1072,11 @@ void Room::postMessage(const QString& plainText, MessageEventType type) void Room::postMessage(const RoomMessageEvent& event) { + if (usesEncryption()) + { + qCCritical(MAIN) << "Room" << displayName() + << "enforces encryption; sending encrypted messages is not supported yet"; + } connection()->callApi(id(), event); } @@ -1509,6 +1520,14 @@ void Room::processStateEvents(const RoomEvents& events) } break; } + case EventType::RoomEncryption: + { + d->encryptionAlgorithm = + static_cast(event)->algorithm(); + qCDebug(MAIN) << "Encryption switched on in" << displayName(); + emit encryption(); + break; + } default: /* Ignore events of other types */; } } @@ -1759,6 +1778,8 @@ QJsonObject Room::Private::toJson() const QJsonArray::fromStringList(aliases)); ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", canonicalAlias); + ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", + encryptionAlgorithm); for (const auto *m : membersMap) appendStateEvent(stateEvents, QStringLiteral("m.room.member"), diff --git a/room.h b/room.h index 015e9dfc..bdef04ee 100644 --- a/room.h +++ b/room.h @@ -103,6 +103,7 @@ namespace QMatrixClient Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) @@ -145,6 +146,7 @@ namespace QMatrixClient QStringList memberNames() const; int memberCount() const; int timelineSize() const; + bool usesEncryption() const; /** * Returns a square room avatar with the given size and requests it @@ -360,6 +362,7 @@ namespace QMatrixClient void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); void memberListChanged(); + void encryption(); void joinStateChanged(JoinState oldState, JoinState newState); void typingChanged(); -- cgit v1.2.3