aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--connection.cpp124
-rw-r--r--connection.h46
-rw-r--r--events/receiptevent.cpp3
-rw-r--r--events/receiptevent.h2
-rw-r--r--jobs/syncjob.cpp34
-rw-r--r--jobs/syncjob.h20
-rw-r--r--room.cpp93
-rw-r--r--room.h2
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
diff --git a/room.cpp b/room.cpp
index 85953a5c..1a6b055d 100644
--- a/room.cpp
+++ b/room.cpp
@@ -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);
diff --git a/room.h b/room.h
index 23a1412d..06908e3c 100644
--- a/room.h
+++ b/room.h
@@ -145,6 +145,8 @@ namespace QMatrixClient
MemberSorter memberSorter() const;
+ QJsonObject toJson() const;
+
public slots:
void postMessage(const QString& plainText,
MessageEventType type = MessageEventType::Text);