diff options
-rw-r--r-- | connection.cpp | 124 | ||||
-rw-r--r-- | connection.h | 46 | ||||
-rw-r--r-- | events/receiptevent.cpp | 3 | ||||
-rw-r--r-- | events/receiptevent.h | 2 | ||||
-rw-r--r-- | jobs/syncjob.cpp | 34 | ||||
-rw-r--r-- | jobs/syncjob.h | 20 | ||||
-rw-r--r-- | room.cpp | 93 | ||||
-rw-r--r-- | room.h | 2 |
8 files changed, 289 insertions, 35 deletions
diff --git a/connection.cpp b/connection.cpp index 2c9ee88a..efc40fe9 100644 --- a/connection.cpp +++ b/connection.cpp @@ -32,6 +32,12 @@ #include "jobs/mediathumbnailjob.h" #include <QtNetwork/QDnsLookup> +#include <QtCore/QFile> +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QStandardPaths> +#include <QtCore/QStringBuilder> +#include <QtCore/QElapsedTimer> using namespace QMatrixClient; @@ -57,6 +63,8 @@ class Connection::Private QString userId; SyncJob* syncJob; + + bool cacheState = true; }; Connection::Connection(const QUrl& server, QObject* parent) @@ -157,12 +165,7 @@ void Connection::sync(int timeout) auto job = d->syncJob = callApi<SyncJob>(d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, [=] () { - d->data->setLastEvent(job->nextBatch()); - for( auto&& roomData: job->takeRoomData() ) - { - if ( auto* r = provideRoom(roomData.roomId) ) - r->updateData(std::move(roomData)); - } + onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); @@ -176,6 +179,16 @@ void Connection::sync(int timeout) }); } +void Connection::onSyncSuccess(SyncData &&data) { + d->data->setLastEvent(data.nextBatch()); + for( auto&& roomData: data.takeRoomData() ) + { + if ( auto* r = provideRoom(roomData.roomId) ) + r->updateData(std::move(roomData)); + } + +} + void Connection::stopSync() { if (d->syncJob) @@ -319,3 +332,102 @@ QByteArray Connection::generateTxnId() { return d->data->generateTxnId(); } + +void Connection::saveState(const QUrl &toFile) const +{ + if (!d->cacheState) + return; + + QElapsedTimer et; et.start(); + + QFileInfo stateFile { + toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() + }; + if (!stateFile.dir().exists()) + stateFile.dir().mkpath("."); + + QFile outfile { stateFile.absoluteFilePath() }; + if (!outfile.open(QFile::WriteOnly)) + { + qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() + << ":" << outfile.errorString(); + qCWarning(MAIN) << "Caching the rooms state disabled"; + d->cacheState = false; + return; + } + + QJsonObject roomObj; + { + QJsonObject rooms; + QJsonObject inviteRooms; + for (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()); + } + + if (!rooms.isEmpty()) + roomObj.insert("join", rooms); + if (!inviteRooms.isEmpty()) + roomObj.insert("invite", inviteRooms); + } + + QJsonObject rootObj; + rootObj.insert("next_batch", d->data->lastEvent()); + rootObj.insert("rooms", roomObj); + + QByteArray data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); + + 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"; +} + +void Connection::loadState(const QUrl &fromFile) +{ + if (!d->cacheState) + return; + + QElapsedTimer et; et.start(); + QFile file { + fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() + }; + if (!file.exists()) + { + qCDebug(MAIN) << "No state cache file found"; + return; + } + file.open(QFile::ReadOnly); + QByteArray data = file.readAll(); + + SyncData sync; + sync.parseJson(QJsonDocument::fromJson(data)); + onSyncSuccess(std::move(sync)); + qCDebug(PROFILER) << "*** Cached state for" << userId() + << "loaded in" << et.elapsed() << "ms"; +} + +QString Connection::stateCachePath() const +{ + auto safeUserId = userId(); + safeUserId.replace(':', '_'); + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % safeUserId % "_state.json"; +} + +bool Connection::cacheState() const +{ + return d->cacheState; +} + +void Connection::setCacheState(bool newValue) +{ + if (d->cacheState != newValue) + { + d->cacheState = newValue; + emit cacheStateChanged(); + } +} diff --git a/connection.h b/connection.h index 08d216d1..96cfb63d 100644 --- a/connection.h +++ b/connection.h @@ -33,6 +33,7 @@ namespace QMatrixClient class ConnectionData; class SyncJob; + class SyncData; class RoomMessagesJob; class PostReceiptJob; class MediaThumbnailJob; @@ -40,6 +41,11 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT + + /** Whether or not the rooms state should be cached locally + * \sa loadState(), saveState() + */ + Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) public: explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); @@ -89,6 +95,38 @@ namespace QMatrixClient Q_INVOKABLE int millisToReconnect() const; /** + * Call this before first sync to load from previously saved file. + * + * \param fromFile A local path to read the state from. Uses QUrl + * to be QML-friendly. Empty parameter means using a path + * defined by stateCachePath(). + */ + Q_INVOKABLE void loadState(const QUrl &fromFile = {}); + /** + * This method saves the current state of rooms (but not messages + * in them) to a local cache file, so that it could be loaded by + * loadState() on a next run of the client. + * + * \param toFile A local path to save the state to. Uses QUrl to be + * QML-friendly. Empty parameter means using a path defined by + * stateCachePath(). + */ + Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; + + /** + * The default path to store the cached room state, defined as + * follows: + * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json" + * where `_safeUserId` is userId() with `:` (colon) replaced with + * `_` (underscore) + * /see loadState(), saveState() + */ + Q_INVOKABLE QString stateCachePath() const; + + bool cacheState() const; + void setCacheState(bool newValue); + + /** * This is a universal method to start a job of a type passed * as a template parameter. Arguments to callApi() are arguments * to the job constructor _except_ the first ConnectionData* @@ -138,6 +176,8 @@ namespace QMatrixClient void syncError(QString error); //void jobError(BaseJob* job); + void cacheStateChanged(); + protected: /** * @brief Access the underlying ConnectionData class @@ -155,6 +195,12 @@ namespace QMatrixClient */ Room* provideRoom(const QString& roomId); + + /** + * Completes loading sync data. + */ + void onSyncSuccess(SyncData &&data); + private: class Private; Private* d; diff --git a/events/receiptevent.cpp b/events/receiptevent.cpp index e3478cf1..3d6be9f1 100644 --- a/events/receiptevent.cpp +++ b/events/receiptevent.cpp @@ -46,7 +46,7 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj) { Q_ASSERT(obj["type"].toString() == jsonType); - const QJsonObject contents = obj["content"].toObject(); + const QJsonObject contents = contentJson(); _eventsWithReceipts.reserve(static_cast<size_t>(contents.size())); for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt ) { @@ -66,5 +66,6 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj) } _eventsWithReceipts.push_back({eventIt.key(), receipts}); } + _unreadMessages = obj["x-qmatrixclient.unread_messages"].toBool(); } diff --git a/events/receiptevent.h b/events/receiptevent.h index 1d280822..cbe36b10 100644 --- a/events/receiptevent.h +++ b/events/receiptevent.h @@ -41,9 +41,11 @@ namespace QMatrixClient EventsWithReceipts eventsWithReceipts() const { return _eventsWithReceipts; } + bool unreadMessages() const { return _unreadMessages; } private: EventsWithReceipts _eventsWithReceipts; + bool _unreadMessages; // Spec extension for caching purposes static constexpr const char * jsonType = "m.receipt"; }; diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 29ddc2e6..062f1b15 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -22,20 +22,12 @@ using namespace QMatrixClient; -class SyncJob::Private -{ - public: - QString nextBatch; - SyncData roomData; -}; - static size_t jobId = 0; SyncJob::SyncJob(const ConnectionData* connection, const QString& since, const QString& filter, int timeout, const QString& presence) : BaseJob(connection, HttpVerb::Get, QString("SyncJob-%1").arg(++jobId), "_matrix/client/r0/sync") - , d(new Private) { setLoggingCategory(SYNCJOB); QUrlQuery query; @@ -52,26 +44,25 @@ SyncJob::SyncJob(const ConnectionData* connection, const QString& since, setMaxRetries(std::numeric_limits<int>::max()); } -SyncJob::~SyncJob() +QString SyncData::nextBatch() const { - delete d; + return nextBatch_; } -QString SyncJob::nextBatch() const +SyncDataList&& SyncData::takeRoomData() { - return d->nextBatch; + return std::move(roomData); } -SyncData&& SyncJob::takeRoomData() +BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - return std::move(d->roomData); + return d.parseJson(data); } -BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) -{ +BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { QElapsedTimer et; et.start(); QJsonObject json = data.object(); - d->nextBatch = json.value("next_batch").toString(); + nextBatch_ = json.value("next_batch").toString(); // TODO: presence // TODO: account_data QJsonObject rooms = json.value("rooms").toObject(); @@ -86,13 +77,12 @@ BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { const QJsonObject rs = rooms.value(roomState.jsonKey).toObject(); // We have a Qt container on the right and an STL one on the left - d->roomData.reserve(static_cast<size_t>(rs.size())); + roomData.reserve(static_cast<size_t>(rs.size())); for( auto rkey: rs.keys() ) - d->roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject()); + roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject()); } - qCDebug(PROFILER) << "*** SyncJob::parseJson():" << et.elapsed() << "ms"; - - return Success; + qCDebug(PROFILER) << "*** SyncData::parseJson():" << et.elapsed() << "ms"; + return BaseJob::Success; } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 07824e23..2ded0df3 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -67,7 +67,18 @@ Q_DECLARE_TYPEINFO(QMatrixClient::SyncRoomData, Q_MOVABLE_TYPE); namespace QMatrixClient { // QVector cannot work with non-copiable objects, std::vector can. - using SyncData = std::vector<SyncRoomData>; + using SyncDataList = std::vector<SyncRoomData>; + + class SyncData { + public: + BaseJob::Status parseJson(const QJsonDocument &data); + SyncDataList&& takeRoomData(); + QString nextBatch() const; + + private: + QString nextBatch_; + SyncDataList roomData; + }; class SyncJob: public BaseJob { @@ -75,16 +86,13 @@ namespace QMatrixClient explicit SyncJob(const ConnectionData* connection, const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); - virtual ~SyncJob(); - SyncData&& takeRoomData(); - QString nextBatch() const; + SyncData &&takeData() { return std::move(d); } protected: Status parseJson(const QJsonDocument& data) override; private: - class Private; - Private* d; + SyncData d; }; } // namespace QMatrixClient @@ -119,6 +119,8 @@ class Room::Private void setLastReadEvent(User* u, const QString& eventId); rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); + QJsonObject toJson() const; + private: QString calculateDisplayname() const; QString roomNameFromMemberNames(const QList<User*>& userlist) const; @@ -799,6 +801,8 @@ void Room::processEphemeralEvent(Event* event) d->setLastReadEvent(m, p.evtId); } } + if (receiptEvent->unreadMessages()) + d->unreadMessages = true; break; } default: @@ -886,6 +890,95 @@ void Room::Private::updateDisplayname() emit q->displaynameChanged(q); } +QJsonObject stateEventToJson(const QString& type, const QString& name, + const QJsonValue& content) +{ + QJsonObject contentObj; + contentObj.insert(name, content); + + QJsonObject eventObj; + eventObj.insert("type", type); + eventObj.insert("content", contentObj); + + return eventObj; +} + +QJsonObject Room::Private::toJson() const +{ + QJsonObject result; + { + QJsonArray stateEvents; + + stateEvents.append(stateEventToJson("m.room.name", "name", name)); + stateEvents.append(stateEventToJson("m.room.topic", "topic", topic)); + stateEvents.append(stateEventToJson("m.room.aliases", "aliases", + QJsonArray::fromStringList(aliases))); + stateEvents.append(stateEventToJson("m.room.canonical_alias", "alias", + canonicalAlias)); + + for (const auto &i : membersMap) + { + QJsonObject content; + content.insert("membership", QStringLiteral("join")); + content.insert("displayname", i->displayname()); + // avatar URL is not available + + QJsonObject memberEvent; + memberEvent.insert("type", QStringLiteral("m.room.member")); + memberEvent.insert("state_key", i->id()); + memberEvent.insert("content", content); + stateEvents.append(memberEvent); + } + + QJsonObject roomStateObj; + roomStateObj.insert("events", stateEvents); + + result.insert("state", roomStateObj); + } + + if (!q->readMarkerEventId().isEmpty()) + { + QJsonArray ephemeralEvents; + { + // Don't dump the timestamp because it's useless in the cache. + QJsonObject user; + user.insert(connection->userId(), {}); + + QJsonObject receipt; + receipt.insert("m.read", user); + + QJsonObject lastReadEvent; + lastReadEvent.insert(q->readMarkerEventId(), receipt); + + QJsonObject receiptsObj; + receiptsObj.insert("type", QStringLiteral("m.receipt")); + receiptsObj.insert("content", lastReadEvent); + // In extension of the spec we add a hint to the receipt event + // to allow setting the unread indicator without downloading + // and analysing the timeline. + receiptsObj.insert("x-qmatrixclient.unread_messages", unreadMessages); + ephemeralEvents.append(receiptsObj); + } + + QJsonObject ephemeralObj; + ephemeralObj.insert("events", ephemeralEvents); + + result.insert("ephemeral", ephemeralObj); + } + + QJsonObject unreadNotificationsObj; + unreadNotificationsObj.insert("highlight_count", highlightCount); + unreadNotificationsObj.insert("notification_count", notificationCount); + result.insert("unread_notifications", unreadNotificationsObj); + + return result; +} + +QJsonObject Room::toJson() const +{ + return d->toJson(); +} + MemberSorter Room::memberSorter() const { return MemberSorter(this); @@ -145,6 +145,8 @@ namespace QMatrixClient MemberSorter memberSorter() const; + QJsonObject toJson() const; + public slots: void postMessage(const QString& plainText, MessageEventType type = MessageEventType::Text); |