From c1929dc22c87ac61e5369cb752e6ddd0ef6a79bf Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Tue, 8 Aug 2017 15:52:52 +0800 Subject: WIP saving intermediate state to JSON --- connection.cpp | 43 ++++++++++++++++++++++++++++++++++++++ connection.h | 4 ++++ room.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ room.h | 2 ++ 4 files changed, 114 insertions(+) diff --git a/connection.cpp b/connection.cpp index f9f1490c..3ecabdc5 100644 --- a/connection.cpp +++ b/connection.cpp @@ -32,6 +32,8 @@ #include "jobs/mediathumbnailjob.h" #include +#include +#include using namespace QMatrixClient; @@ -320,3 +322,44 @@ QByteArray Connection::generateTxnId() { return d->data->generateTxnId(); } + +QFile Connection::getStateSaveFile() const { + return QFile(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/data.json"); +} + +void Connection::saveState() { + QJsonObject rooms; + + for (auto i : this->roomMap()) { + QJsonObject roomObj; + i->toJson(roomObj); + rooms[i->id()] = roomObj; + } + + QJsonObject rootObj{ + {"next_batch", QJsonValue(d->data->lastEvent())}, + {"presence", QJsonValue(QJsonObject())}, + {"rooms", QJsonValue({ + qMakePair(QString("leave"), QJsonValue(QJsonObject())), + qMakePair(QString("join"), QJsonValue(rooms)), + qMakePair(QString("invite"), QJsonValue(QJsonObject())) + })} + }; + QJsonDocument doc { rootObj }; + QByteArray data = doc.toJson(); + + QFile outfile = getStateSaveFile(); + outfile.open(QFile::WriteOnly); + qInfo() << "Writing state to file=" << outfile.fileName(); + //QFile outfile(path); + outfile.write(data.data(), data.size()); +} + +void Connection::loadState() { + QFile file = getStateSaveFile(); + if (!file.exists()) return; + file.open(QFile::ReadOnly); + QByteArray data = file.readAll(); + + QJsonDocument doc = QJsonDocument.fromJson(data.data(), data.size()); +} diff --git a/connection.h b/connection.h index e3f33155..f199ed35 100644 --- a/connection.h +++ b/connection.h @@ -83,6 +83,10 @@ namespace QMatrixClient Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; + /** call this before first sync */ + Q_INVOKABLE void loadState(); + Q_INVOKABLE void saveState(); + template JobT* callApi(JobArgTs... jobArgs) const { diff --git a/room.cpp b/room.cpp index cfdd33ac..982ae47e 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); + void toJson(QJsonObject &out); + private: QString calculateDisplayname() const; QString roomNameFromMemberNames(const QList& userlist) const; @@ -875,6 +877,69 @@ void Room::Private::updateDisplayname() emit q->displaynameChanged(q); } +void Room::Private::toJson(QJsonObject &out) { + QJsonValue nowTimestamp { QDateTime::currentMSecsSinceEpoch() }; + QJsonArray stateEvents; + + QJsonObject nameEvent { + {"type", QJsonValue("m.room.name")}, + {"content", QJsonValue({qMakePair(QString("name"), QJsonValue(this->name))})}}; + stateEvents.append(QJsonValue(nameEvent)); + + for (auto i : this->membersMap) { + QJsonObject content { + {"membership", QJsonValue("join")}, + {"displayname", QJsonValue(i->displayname())} + // avatar URL is not available + }; + QJsonObject memberEvent { + {"type", QJsonValue("m.room.member")}, + {"sender", QJsonValue(i->id())}, + {"state_key", QJsonValue(i->id())}, + {"content", QJsonValue(content)}, + {"membership", QJsonValue("join")}, + {"origin_server_ts", nowTimestamp} + }; + stateEvents.append(QJsonValue(memberEvent)); + } + + { + QJsonArray aliases; + for (auto i : this->aliases) { + aliases.append(QJsonValue(i)); + } + + QJsonObject content { + {"aliases", QJsonValue(aliases)} + }; + + QJsonObject aliasEvent { + {"type", QJsonValue("m.room.aliases")}, + {"origin_server_ts", nowTimestamp}, + {"content", QJsonValue(content)} + }; + + stateEvents.append(QJsonValue(aliasEvent)); + } + + { + QJsonObject content { + {"alias", QJsonValue(this->canonicalAlias)} + }; + QJsonObject canonicalAliasEvent { + {"type", QJsonValue("m.room.canonical_alias")}, + {"origin_server_ts", nowTimestamp} + }; + stateEvents.append(QJsonValue(canonicalAliasEvent)); + } + + out["state"] = QJsonValue({qMakePair(QString("events"), QJsonValue(stateEvents))}); +} + +void Room::toJson(QJsonObject &out) const { + d->toJson(out); +} + MemberSorter Room::memberSorter() const { return MemberSorter(this); diff --git a/room.h b/room.h index 03827a55..9e363556 100644 --- a/room.h +++ b/room.h @@ -142,6 +142,8 @@ namespace QMatrixClient MemberSorter memberSorter() const; + void toJson(QJsonObject &out) const; + public slots: void postMessage(const QString& plainText, MessageEventType type = MessageEventType::Text); -- cgit v1.2.3 From 6ae8e3d78b5c4a75ca7d5ca88af730071047d148 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Wed, 16 Aug 2017 13:56:13 +0800 Subject: Implement saving save to enable incremental sync even after shutdown --- connection.cpp | 32 ++++++++++++++++++++------------ connection.h | 10 ++++++++++ jobs/syncjob.cpp | 32 ++++++++++++++------------------ jobs/syncjob.h | 19 ++++++++++++++----- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/connection.cpp b/connection.cpp index 3ecabdc5..7650b4dd 100644 --- a/connection.cpp +++ b/connection.cpp @@ -159,12 +159,7 @@ void Connection::sync(int timeout) auto job = d->syncJob = callApi(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->data()); d->syncJob = nullptr; emit syncDone(); }); @@ -178,6 +173,17 @@ void Connection::sync(int timeout) }); } +void Connection::onSyncSuccess(SyncData &data) { + d->data->setLastEvent(data.nextBatch()); + qInfo() << "last event " << d->data->lastEvent(); + for( auto&& roomData: data.takeRoomData() ) + { + if ( auto* r = provideRoom(roomData.roomId) ) + r->updateData(std::move(roomData)); + } + +} + void Connection::stopSync() { if (d->syncJob) @@ -323,8 +329,8 @@ QByteArray Connection::generateTxnId() return d->data->generateTxnId(); } -QFile Connection::getStateSaveFile() const { - return QFile(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/data.json"); +QString Connection::getStateSaveFile() const { + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/state.json"; } void Connection::saveState() { @@ -348,18 +354,20 @@ void Connection::saveState() { QJsonDocument doc { rootObj }; QByteArray data = doc.toJson(); - QFile outfile = getStateSaveFile(); + QFile outfile { getStateSaveFile() }; outfile.open(QFile::WriteOnly); qInfo() << "Writing state to file=" << outfile.fileName(); - //QFile outfile(path); outfile.write(data.data(), data.size()); } void Connection::loadState() { - QFile file = getStateSaveFile(); + QFile file { getStateSaveFile() }; if (!file.exists()) return; file.open(QFile::ReadOnly); QByteArray data = file.readAll(); - QJsonDocument doc = QJsonDocument.fromJson(data.data(), data.size()); + QJsonDocument doc { QJsonDocument::fromJson(data) }; + SyncData sync; + sync.parseJson(doc); + onSyncSuccess(sync); } diff --git a/connection.h b/connection.h index f199ed35..58a3de6b 100644 --- a/connection.h +++ b/connection.h @@ -31,6 +31,7 @@ namespace QMatrixClient class ConnectionData; class SyncJob; + class SyncData; class RoomMessagesJob; class PostReceiptJob; class MediaThumbnailJob; @@ -143,6 +144,15 @@ namespace QMatrixClient */ virtual Room* createRoom(const QString& roomId); + /** + * Returns the path to file for saving state (rooms, presence, ...) + */ + QString getStateSaveFile() const; + + /** + * Completes loading sync data. + */ + void onSyncSuccess(SyncData &data); private: class Private; Private* d; diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 29ddc2e6..3adf6b0c 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -22,20 +22,13 @@ 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) + , d(new SyncData) { setLoggingCategory(SYNCJOB); QUrlQuery query; @@ -57,21 +50,26 @@ SyncJob::~SyncJob() delete d; } -QString SyncJob::nextBatch() const +QString SyncData::nextBatch() const { - return d->nextBatch; + return nextBatch_; } -SyncData&& SyncJob::takeRoomData() +SyncDataList&& SyncData::takeRoomData() { - return std::move(d->roomData); + return std::move(roomData); } BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { + d->parseJson(data); + return Success; +} + +void 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 +84,11 @@ 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(rs.size())); + roomData.reserve(static_cast(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"; } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 07824e23..9dc221b5 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; + using SyncDataList = std::vector; + + class SyncData { + public: + void parseJson(const QJsonDocument &data); + SyncDataList&& takeRoomData(); + QString nextBatch() const; + + private: + QString nextBatch_; + SyncDataList roomData; + }; class SyncJob: public BaseJob { @@ -77,14 +88,12 @@ namespace QMatrixClient int timeout = -1, const QString& presence = {}); virtual ~SyncJob(); - SyncData&& takeRoomData(); - QString nextBatch() const; + SyncData *data() const { return d; } protected: Status parseJson(const QJsonDocument& data) override; private: - class Private; - Private* d; + SyncData* d; }; } // namespace QMatrixClient -- cgit v1.2.3 From f3d893150f3991db2aed7a890d6156a576ae2e42 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Fri, 18 Aug 2017 14:29:06 +0800 Subject: Fix compilation for Qt 5.2.1 --- connection.cpp | 34 +++++++++++++++++++------------ connection.h | 3 +++ room.cpp | 64 +++++++++++++++++++++++++++++----------------------------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/connection.cpp b/connection.cpp index 7650b4dd..4d76f7cd 100644 --- a/connection.cpp +++ b/connection.cpp @@ -59,6 +59,7 @@ class Connection::Private QString userId; SyncJob* syncJob; + QString stateSaveFile; }; Connection::Connection(const QUrl& server, QObject* parent) @@ -175,7 +176,6 @@ void Connection::sync(int timeout) void Connection::onSyncSuccess(SyncData &data) { d->data->setLastEvent(data.nextBatch()); - qInfo() << "last event " << d->data->lastEvent(); for( auto&& roomData: data.takeRoomData() ) { if ( auto* r = provideRoom(roomData.roomId) ) @@ -330,7 +330,11 @@ QByteArray Connection::generateTxnId() } QString Connection::getStateSaveFile() const { - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/state.json"; + return d->stateSaveFile; +} + +void Connection::setStateSaveFile(const QString &path) { + d->stateSaveFile = path; } void Connection::saveState() { @@ -342,21 +346,25 @@ void Connection::saveState() { rooms[i->id()] = roomObj; } - QJsonObject rootObj{ - {"next_batch", QJsonValue(d->data->lastEvent())}, - {"presence", QJsonValue(QJsonObject())}, - {"rooms", QJsonValue({ - qMakePair(QString("leave"), QJsonValue(QJsonObject())), - qMakePair(QString("join"), QJsonValue(rooms)), - qMakePair(QString("invite"), QJsonValue(QJsonObject())) - })} - }; + QJsonObject roomObj; + roomObj["leave"] = QJsonObject(); + roomObj["join"] = rooms; + roomObj["invite"] = QJsonObject(); + + QJsonObject rootObj; + rootObj["next_batch"] = d->data->lastEvent(); + rootObj["presence"] = QJsonObject(); + rootObj["rooms"] = roomObj; + QJsonDocument doc { rootObj }; QByteArray data = doc.toJson(); - QFile outfile { getStateSaveFile() }; + QString filepath = getStateSaveFile(); + if (filepath.isEmpty()) return; + + QFile outfile { filepath }; outfile.open(QFile::WriteOnly); - qInfo() << "Writing state to file=" << outfile.fileName(); + qCDebug(MAIN) << "Writing state to file=" << outfile.fileName(); outfile.write(data.data(), data.size()); } diff --git a/connection.h b/connection.h index 58a3de6b..063018f9 100644 --- a/connection.h +++ b/connection.h @@ -39,6 +39,7 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT + Q_PROPERTY(QString stateSaveFile READ getStateSaveFile WRITE setStateSaveFile) public: explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); @@ -148,11 +149,13 @@ namespace QMatrixClient * Returns the path to file for saving state (rooms, presence, ...) */ QString getStateSaveFile() const; + void setStateSaveFile(const QString &path); /** * Completes loading sync data. */ void onSyncSuccess(SyncData &data); + private: class Private; Private* d; diff --git a/room.cpp b/room.cpp index 982ae47e..e993fd04 100644 --- a/room.cpp +++ b/room.cpp @@ -881,25 +881,26 @@ void Room::Private::toJson(QJsonObject &out) { QJsonValue nowTimestamp { QDateTime::currentMSecsSinceEpoch() }; QJsonArray stateEvents; - QJsonObject nameEvent { - {"type", QJsonValue("m.room.name")}, - {"content", QJsonValue({qMakePair(QString("name"), QJsonValue(this->name))})}}; + QJsonObject nameEvent; + nameEvent["type"] = QString("m.room.name"); + QJsonObject nameEventContent; + nameEventContent["name"] = this->name; + nameEvent["content"] = nameEventContent; stateEvents.append(QJsonValue(nameEvent)); for (auto i : this->membersMap) { - QJsonObject content { - {"membership", QJsonValue("join")}, - {"displayname", QJsonValue(i->displayname())} - // avatar URL is not available - }; - QJsonObject memberEvent { - {"type", QJsonValue("m.room.member")}, - {"sender", QJsonValue(i->id())}, - {"state_key", QJsonValue(i->id())}, - {"content", QJsonValue(content)}, - {"membership", QJsonValue("join")}, - {"origin_server_ts", nowTimestamp} - }; + QJsonObject content; + content["membership"] = QString("join"); + content["displayname"] = i->displayname(); + // avatar URL is not available + + QJsonObject memberEvent; + memberEvent["type"] = QString("m.room.member"); + memberEvent["sender"] = (i->id()); + memberEvent["state_key"] = (i->id()); + memberEvent["content"] = (content); + memberEvent["membership"] = QString("join"); + memberEvent["origin_server_ts"] = nowTimestamp; stateEvents.append(QJsonValue(memberEvent)); } @@ -909,31 +910,30 @@ void Room::Private::toJson(QJsonObject &out) { aliases.append(QJsonValue(i)); } - QJsonObject content { - {"aliases", QJsonValue(aliases)} - }; + QJsonObject content; + content["aliases"] = QJsonValue(aliases); - QJsonObject aliasEvent { - {"type", QJsonValue("m.room.aliases")}, - {"origin_server_ts", nowTimestamp}, - {"content", QJsonValue(content)} - }; + QJsonObject aliasEvent; + aliasEvent["type"] = QString("m.room.aliases"); + aliasEvent["origin_server_ts"] = nowTimestamp; + aliasEvent["content"] = (content); stateEvents.append(QJsonValue(aliasEvent)); } { - QJsonObject content { - {"alias", QJsonValue(this->canonicalAlias)} - }; - QJsonObject canonicalAliasEvent { - {"type", QJsonValue("m.room.canonical_alias")}, - {"origin_server_ts", nowTimestamp} - }; + QJsonObject content; + content["alias"] = (this->canonicalAlias); + + QJsonObject canonicalAliasEvent; + canonicalAliasEvent["type"] = QString("m.room.canonical_alias"); + canonicalAliasEvent["origin_server_ts"] = nowTimestamp; stateEvents.append(QJsonValue(canonicalAliasEvent)); } - out["state"] = QJsonValue({qMakePair(QString("events"), QJsonValue(stateEvents))}); + QJsonObject roomStateObj; + roomStateObj["events"] = QJsonValue(stateEvents); + out["state"] = roomStateObj; } void Room::toJson(QJsonObject &out) const { -- cgit v1.2.3 From 456604040ce9cf3c22b726e8ef279a1fe4a85f39 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Wed, 23 Aug 2017 14:49:06 +0800 Subject: Use QUrl for state save file, create directory if it doesn't exist --- connection.cpp | 28 ++++++++++++++++++---------- connection.h | 6 +++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/connection.cpp b/connection.cpp index 4d76f7cd..136ccda1 100644 --- a/connection.cpp +++ b/connection.cpp @@ -34,6 +34,8 @@ #include #include #include +#include +#include using namespace QMatrixClient; @@ -59,7 +61,7 @@ class Connection::Private QString userId; SyncJob* syncJob; - QString stateSaveFile; + QUrl stateSaveFile; }; Connection::Connection(const QUrl& server, QObject* parent) @@ -329,15 +331,17 @@ QByteArray Connection::generateTxnId() return d->data->generateTxnId(); } -QString Connection::getStateSaveFile() const { +QUrl Connection::getStateSaveFile() const { return d->stateSaveFile; } -void Connection::setStateSaveFile(const QString &path) { +void Connection::setStateSaveFile(const QUrl &path) { d->stateSaveFile = path; } void Connection::saveState() { + if (getStateSaveFile().isEmpty()) return; + QJsonObject rooms; for (auto i : this->roomMap()) { @@ -359,17 +363,21 @@ void Connection::saveState() { QJsonDocument doc { rootObj }; QByteArray data = doc.toJson(); - QString filepath = getStateSaveFile(); - if (filepath.isEmpty()) return; + QFileInfo stateFile { getStateSaveFile().toLocalFile() }; + QFile outfile { stateFile.absoluteFilePath() }; + if (!stateFile.dir().exists()) stateFile.dir().mkpath("."); + + if (outfile.open(QFile::WriteOnly)) { + qCDebug(MAIN) << "Writing state to file=" << outfile.fileName(); + outfile.write(data.data(), data.size()); - QFile outfile { filepath }; - outfile.open(QFile::WriteOnly); - qCDebug(MAIN) << "Writing state to file=" << outfile.fileName(); - outfile.write(data.data(), data.size()); + } else { + qCWarning(MAIN) << outfile.errorString(); + } } void Connection::loadState() { - QFile file { getStateSaveFile() }; + QFile file { getStateSaveFile().toLocalFile() }; if (!file.exists()) return; file.open(QFile::ReadOnly); QByteArray data = file.readAll(); diff --git a/connection.h b/connection.h index 063018f9..5a5ce3ac 100644 --- a/connection.h +++ b/connection.h @@ -39,7 +39,7 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT - Q_PROPERTY(QString stateSaveFile READ getStateSaveFile WRITE setStateSaveFile) + Q_PROPERTY(QUrl stateSaveFile READ getStateSaveFile WRITE setStateSaveFile) public: explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); @@ -148,8 +148,8 @@ namespace QMatrixClient /** * Returns the path to file for saving state (rooms, presence, ...) */ - QString getStateSaveFile() const; - void setStateSaveFile(const QString &path); + QUrl getStateSaveFile() const; + void setStateSaveFile(const QUrl &path); /** * Completes loading sync data. -- cgit v1.2.3 From 1a00b1c38c0957df9758b0edeff4559f2515fe5e Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Sun, 27 Aug 2017 12:32:00 +0800 Subject: Use QJsonObject.insert and QStringLiteral --- connection.cpp | 12 ++++++------ room.cpp | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/connection.cpp b/connection.cpp index 136ccda1..89da47cd 100644 --- a/connection.cpp +++ b/connection.cpp @@ -351,14 +351,14 @@ void Connection::saveState() { } QJsonObject roomObj; - roomObj["leave"] = QJsonObject(); - roomObj["join"] = rooms; - roomObj["invite"] = QJsonObject(); + roomObj.insert("leave", QJsonObject()); + roomObj.insert("join", rooms); + roomObj.insert("invite", QJsonObject()); QJsonObject rootObj; - rootObj["next_batch"] = d->data->lastEvent(); - rootObj["presence"] = QJsonObject(); - rootObj["rooms"] = roomObj; + rootObj.insert("next_batch", d->data->lastEvent()); + rootObj.insert("presence", QJsonObject()); + rootObj.insert("rooms", roomObj); QJsonDocument doc { rootObj }; QByteArray data = doc.toJson(); diff --git a/room.cpp b/room.cpp index e993fd04..3841eab8 100644 --- a/room.cpp +++ b/room.cpp @@ -882,26 +882,28 @@ void Room::Private::toJson(QJsonObject &out) { QJsonArray stateEvents; QJsonObject nameEvent; - nameEvent["type"] = QString("m.room.name"); + nameEvent.insert("type", QStringLiteral("m.room.name")); + QJsonObject nameEventContent; - nameEventContent["name"] = this->name; - nameEvent["content"] = nameEventContent; - stateEvents.append(QJsonValue(nameEvent)); + nameEventContent.insert("name", this->name); + + nameEvent.insert("content", nameEventContent); + stateEvents.append(nameEvent); for (auto i : this->membersMap) { QJsonObject content; - content["membership"] = QString("join"); - content["displayname"] = i->displayname(); + content.insert("membership", QStringLiteral("join")); + content.insert("displayname", i->displayname()); // avatar URL is not available QJsonObject memberEvent; - memberEvent["type"] = QString("m.room.member"); - memberEvent["sender"] = (i->id()); - memberEvent["state_key"] = (i->id()); - memberEvent["content"] = (content); - memberEvent["membership"] = QString("join"); - memberEvent["origin_server_ts"] = nowTimestamp; - stateEvents.append(QJsonValue(memberEvent)); + memberEvent.insert("type", QStringLiteral("m.room.member")); + memberEvent.insert("sender", i->id()); + memberEvent.insert("state_key", i->id()); + memberEvent.insert("content", content); + memberEvent.insert("membership", QStringLiteral("join")); + memberEvent.insert("origin_server_ts", nowTimestamp); + stateEvents.append(memberEvent); } { @@ -911,29 +913,29 @@ void Room::Private::toJson(QJsonObject &out) { } QJsonObject content; - content["aliases"] = QJsonValue(aliases); + content.insert("aliases", aliases); QJsonObject aliasEvent; - aliasEvent["type"] = QString("m.room.aliases"); - aliasEvent["origin_server_ts"] = nowTimestamp; - aliasEvent["content"] = (content); + aliasEvent.insert("type", QStringLiteral("m.room.aliases")); + aliasEvent.insert("origin_server_ts", nowTimestamp); + aliasEvent.insert("content", content); - stateEvents.append(QJsonValue(aliasEvent)); + stateEvents.append(aliasEvent); } { QJsonObject content; - content["alias"] = (this->canonicalAlias); + content.insert("alias", this->canonicalAlias); QJsonObject canonicalAliasEvent; - canonicalAliasEvent["type"] = QString("m.room.canonical_alias"); - canonicalAliasEvent["origin_server_ts"] = nowTimestamp; - stateEvents.append(QJsonValue(canonicalAliasEvent)); + canonicalAliasEvent.insert("type", QStringLiteral("m.room.canonical_alias")); + canonicalAliasEvent.insert("origin_server_ts", nowTimestamp); + stateEvents.append(canonicalAliasEvent); } QJsonObject roomStateObj; - roomStateObj["events"] = QJsonValue(stateEvents); - out["state"] = roomStateObj; + roomStateObj.insert("events", stateEvents); + out.insert("state", roomStateObj); } void Room::toJson(QJsonObject &out) const { -- cgit v1.2.3 From 1b459a0f32b28b2f3fe8dcc0cd2553c3aea9ccee Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Sun, 27 Aug 2017 14:45:43 +0800 Subject: Remove saveStateFile property and just use an argument. --- connection.cpp | 19 ++++--------------- connection.h | 15 ++++++--------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/connection.cpp b/connection.cpp index 89da47cd..97594e7d 100644 --- a/connection.cpp +++ b/connection.cpp @@ -61,7 +61,6 @@ class Connection::Private QString userId; SyncJob* syncJob; - QUrl stateSaveFile; }; Connection::Connection(const QUrl& server, QObject* parent) @@ -331,17 +330,7 @@ QByteArray Connection::generateTxnId() return d->data->generateTxnId(); } -QUrl Connection::getStateSaveFile() const { - return d->stateSaveFile; -} - -void Connection::setStateSaveFile(const QUrl &path) { - d->stateSaveFile = path; -} - -void Connection::saveState() { - if (getStateSaveFile().isEmpty()) return; - +void Connection::saveState(const QUrl &toFile) { QJsonObject rooms; for (auto i : this->roomMap()) { @@ -363,7 +352,7 @@ void Connection::saveState() { QJsonDocument doc { rootObj }; QByteArray data = doc.toJson(); - QFileInfo stateFile { getStateSaveFile().toLocalFile() }; + QFileInfo stateFile { toFile.toLocalFile() }; QFile outfile { stateFile.absoluteFilePath() }; if (!stateFile.dir().exists()) stateFile.dir().mkpath("."); @@ -376,8 +365,8 @@ void Connection::saveState() { } } -void Connection::loadState() { - QFile file { getStateSaveFile().toLocalFile() }; +void Connection::loadState(const QUrl &fromFile) { + QFile file { fromFile.toLocalFile() }; if (!file.exists()) return; file.open(QFile::ReadOnly); QByteArray data = file.readAll(); diff --git a/connection.h b/connection.h index 5a5ce3ac..0265d92f 100644 --- a/connection.h +++ b/connection.h @@ -39,7 +39,6 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT - Q_PROPERTY(QUrl stateSaveFile READ getStateSaveFile WRITE setStateSaveFile) public: explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); @@ -85,9 +84,12 @@ namespace QMatrixClient Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; - /** call this before first sync */ - Q_INVOKABLE void loadState(); - Q_INVOKABLE void saveState(); + /** + * Call this before first sync to load from previously saved file. + * Uses QUrl to be QML-friendly. + */ + Q_INVOKABLE void loadState(const QUrl &fromFile); + Q_INVOKABLE void saveState(const QUrl &toFile); template JobT* callApi(JobArgTs... jobArgs) const @@ -145,11 +147,6 @@ namespace QMatrixClient */ virtual Room* createRoom(const QString& roomId); - /** - * Returns the path to file for saving state (rooms, presence, ...) - */ - QUrl getStateSaveFile() const; - void setStateSaveFile(const QUrl &path); /** * Completes loading sync data. -- cgit v1.2.3 From 9a26c52f8c3e71f803fd38128ecddfd6ce9748f6 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Sat, 2 Sep 2017 00:58:58 +0800 Subject: Use status return type for parseJson --- connection.cpp | 7 +++---- jobs/syncjob.cpp | 3 ++- jobs/syncjob.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/connection.cpp b/connection.cpp index 97594e7d..fe0cb251 100644 --- a/connection.cpp +++ b/connection.cpp @@ -32,10 +32,9 @@ #include "jobs/mediathumbnailjob.h" #include -#include -#include -#include -#include +#include +#include +#include using namespace QMatrixClient; diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 3adf6b0c..1e71e215 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -66,7 +66,7 @@ BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) return Success; } -void SyncData::parseJson(const QJsonDocument &data) { +BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { QElapsedTimer et; et.start(); QJsonObject json = data.object(); nextBatch_ = json.value("next_batch").toString(); @@ -89,6 +89,7 @@ void SyncData::parseJson(const QJsonDocument &data) { roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject()); } qCDebug(PROFILER) << "*** SyncData::parseJson():" << et.elapsed() << "ms"; + return Success; } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 9dc221b5..16ac5895 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -71,7 +71,7 @@ namespace QMatrixClient class SyncData { public: - void parseJson(const QJsonDocument &data); + BaseJob::Status parseJson(const QJsonDocument &data); SyncDataList&& takeRoomData(); QString nextBatch() const; -- cgit v1.2.3 From a7ee0dfacc2c571572240191b3cf0846a9e32998 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Sun, 3 Sep 2017 14:43:05 +0800 Subject: More fixes --- connection.cpp | 3 +-- jobs/syncjob.cpp | 5 ++--- room.cpp | 17 ++++++++++------- room.h | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/connection.cpp b/connection.cpp index fe0cb251..d2acf928 100644 --- a/connection.cpp +++ b/connection.cpp @@ -333,8 +333,7 @@ void Connection::saveState(const QUrl &toFile) { QJsonObject rooms; for (auto i : this->roomMap()) { - QJsonObject roomObj; - i->toJson(roomObj); + QJsonObject roomObj = i->toJson(); rooms[i->id()] = roomObj; } diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 1e71e215..bbec968e 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -62,8 +62,7 @@ SyncDataList&& SyncData::takeRoomData() BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - d->parseJson(data); - return Success; + return d->parseJson(data); } BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { @@ -89,7 +88,7 @@ BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject()); } qCDebug(PROFILER) << "*** SyncData::parseJson():" << et.elapsed() << "ms"; - return Success; + return BaseJob::Success; } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, diff --git a/room.cpp b/room.cpp index 3841eab8..212c8acd 100644 --- a/room.cpp +++ b/room.cpp @@ -119,7 +119,7 @@ class Room::Private void setLastReadEvent(User* u, const QString& eventId); rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); - void toJson(QJsonObject &out); + QJsonObject toJson() const; private: QString calculateDisplayname() const; @@ -877,7 +877,7 @@ void Room::Private::updateDisplayname() emit q->displaynameChanged(q); } -void Room::Private::toJson(QJsonObject &out) { +QJsonObject Room::Private::toJson() const { QJsonValue nowTimestamp { QDateTime::currentMSecsSinceEpoch() }; QJsonArray stateEvents; @@ -890,7 +890,7 @@ void Room::Private::toJson(QJsonObject &out) { nameEvent.insert("content", nameEventContent); stateEvents.append(nameEvent); - for (auto i : this->membersMap) { + for (const auto &i : this->membersMap) { QJsonObject content; content.insert("membership", QStringLiteral("join")); content.insert("displayname", i->displayname()); @@ -908,7 +908,7 @@ void Room::Private::toJson(QJsonObject &out) { { QJsonArray aliases; - for (auto i : this->aliases) { + for (const auto &i : this->aliases) { aliases.append(QJsonValue(i)); } @@ -935,11 +935,14 @@ void Room::Private::toJson(QJsonObject &out) { QJsonObject roomStateObj; roomStateObj.insert("events", stateEvents); - out.insert("state", roomStateObj); + + QJsonObject result; + result.insert("state", roomStateObj); + return result; } -void Room::toJson(QJsonObject &out) const { - d->toJson(out); +QJsonObject Room::toJson() const { + return d->toJson(); } MemberSorter Room::memberSorter() const diff --git a/room.h b/room.h index 9e363556..12de0f31 100644 --- a/room.h +++ b/room.h @@ -142,7 +142,7 @@ namespace QMatrixClient MemberSorter memberSorter() const; - void toJson(QJsonObject &out) const; + QJsonObject toJson() const; public slots: void postMessage(const QString& plainText, -- cgit v1.2.3 From 56d34ecab5eb35c426a6e64b034bf2507761dd09 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Mon, 4 Sep 2017 10:35:45 +0800 Subject: Use SyncJob::SyncData as a plain member --- connection.cpp | 2 +- jobs/syncjob.cpp | 8 +------- jobs/syncjob.h | 5 ++--- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/connection.cpp b/connection.cpp index d2acf928..9fc2f85b 100644 --- a/connection.cpp +++ b/connection.cpp @@ -160,7 +160,7 @@ void Connection::sync(int timeout) auto job = d->syncJob = callApi(d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, [=] () { - onSyncSuccess(*job->data()); + onSyncSuccess(job->data()); d->syncJob = nullptr; emit syncDone(); }); diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index bbec968e..062f1b15 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -28,7 +28,6 @@ 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 SyncData) { setLoggingCategory(SYNCJOB); QUrlQuery query; @@ -45,11 +44,6 @@ SyncJob::SyncJob(const ConnectionData* connection, const QString& since, setMaxRetries(std::numeric_limits::max()); } -SyncJob::~SyncJob() -{ - delete d; -} - QString SyncData::nextBatch() const { return nextBatch_; @@ -62,7 +56,7 @@ SyncDataList&& SyncData::takeRoomData() BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - return d->parseJson(data); + return d.parseJson(data); } BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 16ac5895..80cc6735 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -86,14 +86,13 @@ namespace QMatrixClient explicit SyncJob(const ConnectionData* connection, const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); - virtual ~SyncJob(); - SyncData *data() const { return d; } + SyncData &data() { return d; } protected: Status parseJson(const QJsonDocument& data) override; private: - SyncData* d; + SyncData d; }; } // namespace QMatrixClient -- cgit v1.2.3 From 071856d31995f665ee9219b3e05510ab83f9f4d8 Mon Sep 17 00:00:00 2001 From: Roman Plášil Date: Mon, 4 Sep 2017 19:23:50 +0800 Subject: Use move on SyncData --- connection.cpp | 9 ++++----- connection.h | 2 +- jobs/syncjob.h | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/connection.cpp b/connection.cpp index 9fc2f85b..27f0a86f 100644 --- a/connection.cpp +++ b/connection.cpp @@ -160,7 +160,7 @@ void Connection::sync(int timeout) auto job = d->syncJob = callApi(d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, [=] () { - onSyncSuccess(job->data()); + onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); @@ -174,7 +174,7 @@ void Connection::sync(int timeout) }); } -void Connection::onSyncSuccess(SyncData &data) { +void Connection::onSyncSuccess(SyncData &&data) { d->data->setLastEvent(data.nextBatch()); for( auto&& roomData: data.takeRoomData() ) { @@ -333,8 +333,7 @@ void Connection::saveState(const QUrl &toFile) { QJsonObject rooms; for (auto i : this->roomMap()) { - QJsonObject roomObj = i->toJson(); - rooms[i->id()] = roomObj; + rooms[i->id()] = i->toJson(); } QJsonObject roomObj; @@ -372,5 +371,5 @@ void Connection::loadState(const QUrl &fromFile) { QJsonDocument doc { QJsonDocument::fromJson(data) }; SyncData sync; sync.parseJson(doc); - onSyncSuccess(sync); + onSyncSuccess(std::move(sync)); } diff --git a/connection.h b/connection.h index 0265d92f..ad161d7c 100644 --- a/connection.h +++ b/connection.h @@ -151,7 +151,7 @@ namespace QMatrixClient /** * Completes loading sync data. */ - void onSyncSuccess(SyncData &data); + void onSyncSuccess(SyncData &&data); private: class Private; diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 80cc6735..2ded0df3 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -87,7 +87,7 @@ namespace QMatrixClient const QString& filter = {}, int timeout = -1, const QString& presence = {}); - SyncData &data() { return d; } + SyncData &&takeData() { return std::move(d); } protected: Status parseJson(const QJsonDocument& data) override; -- cgit v1.2.3 From d1fd237d8f917d393a2a8491abc1554abd398085 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 5 Sep 2017 12:09:29 +0900 Subject: Include unread/notification counters to the cache --- room.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/room.cpp b/room.cpp index 212c8acd..1393e145 100644 --- a/room.cpp +++ b/room.cpp @@ -938,6 +938,12 @@ QJsonObject Room::Private::toJson() const { QJsonObject result; result.insert("state", roomStateObj); + + QJsonObject unreadNotificationsObj; + unreadNotificationsObj.insert("highlight_count", highlightCount); + unreadNotificationsObj.insert("notification_count", notificationCount); + result.insert("unread_notifications", unreadNotificationsObj); + return result; } -- cgit v1.2.3 From be258954da33ea3f96fa947569bf617caae68452 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 10:26:41 +0900 Subject: Connection: More deprecations; documented callApi<>() --- connection.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/connection.h b/connection.h index 4b0413e3..08d216d1 100644 --- a/connection.h +++ b/connection.h @@ -65,13 +65,16 @@ namespace QMatrixClient /** @deprecated Use callApi() or Room::postReceipt() instead */ Q_INVOKABLE virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; + /** @deprecated Use callApi() instead */ Q_INVOKABLE virtual JoinRoomJob* joinRoom(const QString& roomAlias); /** @deprecated Use callApi() or Room::leaveRoom() instead */ Q_INVOKABLE virtual void leaveRoom( Room* room ); Q_INVOKABLE virtual RoomMessagesJob* getMessages(Room* room, const QString& from) const; + /** @deprecated Use callApi() instead */ virtual MediaThumbnailJob* getThumbnail(const QUrl& url, QSize requestedSize) const; + /** @deprecated Use callApi() instead */ MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, int requestedHeight) const; @@ -85,6 +88,12 @@ namespace QMatrixClient Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; + /** + * 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* + * argument - callApi() will pass it automatically. + */ template JobT* callApi(JobArgTs... jobArgs) const { -- cgit v1.2.3 From 1b11a6ee708291db37b0c7879eb103d81d70a6b7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 10:10:18 +0900 Subject: Event::originalJsonObject(), RoomEvent validations commented out * Event::originalJsonObject() exposes the original JSON for the event without converting it to QByteArray. This is useful to quickly dump an event into a bigger JSON without reconstructing a JSON object. * Validations in RoomEvent::RoomEvent() do more harm than good. The rest of the library tolerates absence of those attributes pretty well (it wouldn't be able to do much with that anyway); at the same time, dumping JSON to logs turns out to be pretty heavy, and throwing many invalid events at a client is a good way to hit its performance. --- events/event.cpp | 35 ++++++++++++++++++++--------------- events/event.h | 1 + 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/events/event.cpp b/events/event.cpp index 8a6de822..d718306d 100644 --- a/events/event.cpp +++ b/events/event.cpp @@ -48,6 +48,11 @@ QByteArray Event::originalJson() const return QJsonDocument(_originalJson).toJson(); } +QJsonObject Event::originalJsonObject() const +{ + return _originalJson; +} + QDateTime Event::toTimestamp(const QJsonValue& v) { Q_ASSERT(v.isDouble() || v.isNull() || v.isUndefined()); @@ -97,21 +102,21 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& rep) , _senderId(rep["sender"].toString()) , _txnId(rep["unsigned"].toObject().value("transactionId").toString()) { - if (_id.isEmpty()) - { - qCWarning(EVENTS) << "Can't find event_id in a room event"; - qCWarning(EVENTS) << formatJson << rep; - } - if (!rep.contains("origin_server_ts")) - { - qCWarning(EVENTS) << "Can't find server timestamp in a room event"; - qCWarning(EVENTS) << formatJson << rep; - } - if (_senderId.isEmpty()) - { - qCWarning(EVENTS) << "Can't find sender in a room event"; - qCWarning(EVENTS) << formatJson << rep; - } +// if (_id.isEmpty()) +// { +// qCWarning(EVENTS) << "Can't find event_id in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } +// if (!rep.contains("origin_server_ts")) +// { +// qCWarning(EVENTS) << "Can't find server timestamp in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } +// if (_senderId.isEmpty()) +// { +// qCWarning(EVENTS) << "Can't find sender in a room event"; +// qCWarning(EVENTS) << formatJson << rep; +// } if (!_txnId.isEmpty()) qCDebug(EVENTS) << "Event transactionId:" << _txnId; } diff --git a/events/event.h b/events/event.h index 8760aa28..7db14100 100644 --- a/events/event.h +++ b/events/event.h @@ -43,6 +43,7 @@ namespace QMatrixClient Type type() const { return _type; } QByteArray originalJson() const; + QJsonObject originalJsonObject() const; // According to the CS API spec, every event also has // a "content" object; but since its structure is different for -- cgit v1.2.3 From 4e23da3de66e425997506c75204a9e3ea22ccfa5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 10:43:10 +0900 Subject: Room: Slight optimization of makeErrorStr() The previous version constructed QString from const char* and QByteArray parts, only to convert it back to QByteArray; the current version does the whole thing in QByteArray terms. --- room.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/room.cpp b/room.cpp index 547b74c4..85953a5c 100644 --- a/room.cpp +++ b/room.cpp @@ -406,10 +406,9 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u) emit q->memberRenamed(formerNamesakes[0]); } -inline QByteArray makeErrorStr(const Event* e, const char* msg) +inline QByteArray makeErrorStr(const Event* e, QByteArray msg) { - return QString("%1; event dump follows:\n%2") - .arg(msg, QString(e->originalJson())).toUtf8(); + return msg.append("; event dump follows:\n").append(e->originalJson()); } void Room::Private::insertEvent(RoomEvent* e, Timeline::iterator where, -- cgit v1.2.3 From d4cb62523585137dee7879f2143ae1b4dd62513d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 11:39:30 +0900 Subject: Fix a race condition leading to a crash on close It seems that some reply processing still might have happened after BaseJob::abandon() (caused in turn by destroying a Connection object), probably because the event from QNetworkReply landed in the event queue after BaseJob::abandon() but before actual deletion of a job object. Now countered by disconnecting from QNetworkReply signals in abandon() and stop(). --- jobs/basejob.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 26ceb268..8c9ef222 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -219,16 +219,17 @@ BaseJob::Status BaseJob::parseJson(const QJsonDocument&) void BaseJob::stop() { d->timer.stop(); - if (!d->reply) + if (d->reply) { - qCWarning(d->logCat) << this << "stopped with empty network reply"; - } - else if (d->reply->isRunning()) - { - qCWarning(d->logCat) << this << "stopped without ready network reply"; d->reply->disconnect(this); // Ignore whatever comes from the reply - d->reply->abort(); + if (d->reply->isRunning()) + { + qCWarning(d->logCat) << this << "stopped without ready network reply"; + d->reply->abort(); + } } + else + qCWarning(d->logCat) << this << "stopped with empty network reply"; } void BaseJob::finishJob() @@ -320,6 +321,9 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { + this->disconnect(); + if (d->reply) + d->reply->disconnect(this); deleteLater(); } -- cgit v1.2.3 From 0b11b06379fe668063ea5658a261f53f1dcf117a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 12:56:29 +0900 Subject: BaseJob: improved logging Your QT_LOGGING_RULES (especially useful with Qt 5.6 and newer) should work a bit better now: * "Job" prefix is no more needed because the Qt logging prefix (libqmatrixclient.jobs) says it already; * The "created" record didn't follow the logging category if overridden from the concrete job class (see SyncJob); so instead of "created" there's now much more useful "sending request" record. --- jobs/basejob.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 8c9ef222..3057ed75 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include @@ -76,7 +77,7 @@ class BaseJob::Private inline QDebug operator<<(QDebug dbg, const BaseJob* j) { - return dbg << "Job" << j->objectName(); + return dbg << j->objectName(); } BaseJob::BaseJob(const ConnectionData* connection, HttpVerb verb, @@ -89,7 +90,6 @@ BaseJob::BaseJob(const ConnectionData* connection, HttpVerb verb, connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout); d->retryTimer.setSingleShot(true); connect (&d->retryTimer, &QTimer::timeout, this, &BaseJob::start); - qCDebug(d->logCat) << this << "created"; } BaseJob::~BaseJob() @@ -159,6 +159,8 @@ void BaseJob::start() { emit aboutToStart(); d->retryTimer.stop(); // In case we were counting down at the moment + qCDebug(d->logCat) << this << "sending request to" + << d->apiEndpoint % '?' % d->requestQuery.toString(); d->sendRequest(); connect( d->reply.data(), &QNetworkReply::sslErrors, this, &BaseJob::sslErrors ); connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); -- cgit v1.2.3 From 95e6ae003e3f5ed806bc7adf4e10713cd4e35d1f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 10:40:31 +0900 Subject: Connection::cacheState property, load/saveState() tweaks and fixes The property controls whether or not the rooms state is cached: if it's off, loadState() and saveState() become no-ops. Other changes: * loadState/saveState properly deal with rooms in Invite state (this is not quite relevant to the current branch but very much is in the light of a concurrent kitsune-invite-kick PR); * Profile loadState/saveState (because dumping and especially parsing JSON takes time); * Use QJsonDocument::Compact layout, it's about 3 times smaller and quicker to parse than Indented, and we really don't care about the cache being human-friendly; * Have a default path for the state cache, based on the connection's user id. --- connection.cpp | 109 ++++++++++++++++++++++++++++++++++++++++++++------------- connection.h | 40 ++++++++++++++++++--- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/connection.cpp b/connection.cpp index 27f0a86f..785e0e43 100644 --- a/connection.cpp +++ b/connection.cpp @@ -35,6 +35,9 @@ #include #include #include +#include +#include +#include using namespace QMatrixClient; @@ -60,6 +63,8 @@ class Connection::Private QString userId; SyncJob* syncJob; + + bool cacheState = true; }; Connection::Connection(const QUrl& server, QObject* parent) @@ -329,47 +334,101 @@ QByteArray Connection::generateTxnId() return d->data->generateTxnId(); } -void Connection::saveState(const QUrl &toFile) { - QJsonObject rooms; +void Connection::saveState(const QUrl &toFile) const +{ + if (!d->cacheState) + return; + + QElapsedTimer et; et.start(); - for (auto i : this->roomMap()) { - rooms[i->id()] = i->toJson(); + 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; - roomObj.insert("leave", QJsonObject()); - roomObj.insert("join", rooms); - roomObj.insert("invite", QJsonObject()); + { + 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("presence", QJsonObject()); rootObj.insert("rooms", roomObj); - QJsonDocument doc { rootObj }; - QByteArray data = doc.toJson(); + QByteArray data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); - QFileInfo stateFile { toFile.toLocalFile() }; - QFile outfile { stateFile.absoluteFilePath() }; - if (!stateFile.dir().exists()) stateFile.dir().mkpath("."); + 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"; +} - if (outfile.open(QFile::WriteOnly)) { - qCDebug(MAIN) << "Writing state to file=" << outfile.fileName(); - outfile.write(data.data(), data.size()); +void Connection::loadState(const QUrl &fromFile) +{ + if (!d->cacheState) + return; - } else { - qCWarning(MAIN) << outfile.errorString(); + QElapsedTimer et; et.start(); + QFile file { + fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() + }; + if (!file.exists()) + { + qCDebug(MAIN) << "No state cache file found"; + return; } -} - -void Connection::loadState(const QUrl &fromFile) { - QFile file { fromFile.toLocalFile() }; - if (!file.exists()) return; file.open(QFile::ReadOnly); QByteArray data = file.readAll(); - QJsonDocument doc { QJsonDocument::fromJson(data) }; SyncData sync; - sync.parseJson(doc); + 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 ad161d7c..bf50d7d3 100644 --- a/connection.h +++ b/connection.h @@ -39,6 +39,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(); @@ -86,10 +91,35 @@ namespace QMatrixClient /** * Call this before first sync to load from previously saved file. - * Uses QUrl to be QML-friendly. - */ - Q_INVOKABLE void loadState(const QUrl &fromFile); - Q_INVOKABLE void saveState(const QUrl &toFile); + * + * \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); template JobT* callApi(JobArgTs... jobArgs) const @@ -120,6 +150,8 @@ namespace QMatrixClient void syncError(QString error); //void jobError(BaseJob* job); + void cacheStateChanged(); + protected: /** * @brief Access the underlying ConnectionData class -- cgit v1.2.3 From c5c26ff4a09eecaa6d8e1507087566ccf0fd96b4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 10:54:07 +0900 Subject: Room: cache last read event and unread messages flag with the room state Since there's no such thing as "unread messages flag" in the CS API spec, there's now a non-standard key-value in cached m.read receipts for that. --- events/receiptevent.cpp | 3 +- events/receiptevent.h | 2 + room.cpp | 115 +++++++++++++++++++++++++++--------------------- 3 files changed, 70 insertions(+), 50 deletions(-) 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(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/room.cpp b/room.cpp index 1393e145..241a885d 100644 --- a/room.cpp +++ b/room.cpp @@ -789,6 +789,8 @@ void Room::processEphemeralEvent(Event* event) d->setLastReadEvent(m, p.evtId); } } + if (receiptEvent->unreadMessages()) + d->unreadMessages = true; break; } default: @@ -877,67 +879,81 @@ void Room::Private::updateDisplayname() emit q->displaynameChanged(q); } -QJsonObject Room::Private::toJson() const { - QJsonValue nowTimestamp { QDateTime::currentMSecsSinceEpoch() }; - QJsonArray stateEvents; - - QJsonObject nameEvent; - nameEvent.insert("type", QStringLiteral("m.room.name")); +QJsonObject stateEventToJson(const QString& type, const QString& name, + const QJsonValue& content) +{ + QJsonObject contentObj; + contentObj.insert(name, content); - QJsonObject nameEventContent; - nameEventContent.insert("name", this->name); + QJsonObject eventObj; + eventObj.insert("type", type); + eventObj.insert("content", contentObj); - nameEvent.insert("content", nameEventContent); - stateEvents.append(nameEvent); + return eventObj; +} - for (const auto &i : this->membersMap) { - QJsonObject content; - content.insert("membership", QStringLiteral("join")); - content.insert("displayname", i->displayname()); - // avatar URL is not available +QJsonObject Room::Private::toJson() const +{ + QJsonObject result; + { + QJsonArray stateEvents; - QJsonObject memberEvent; - memberEvent.insert("type", QStringLiteral("m.room.member")); - memberEvent.insert("sender", i->id()); - memberEvent.insert("state_key", i->id()); - memberEvent.insert("content", content); - memberEvent.insert("membership", QStringLiteral("join")); - memberEvent.insert("origin_server_ts", nowTimestamp); - stateEvents.append(memberEvent); - } + 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)); - { - QJsonArray aliases; - for (const auto &i : this->aliases) { - aliases.append(QJsonValue(i)); + 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 content; - content.insert("aliases", aliases); + QJsonObject roomStateObj; + roomStateObj.insert("events", stateEvents); - QJsonObject aliasEvent; - aliasEvent.insert("type", QStringLiteral("m.room.aliases")); - aliasEvent.insert("origin_server_ts", nowTimestamp); - aliasEvent.insert("content", content); - - stateEvents.append(aliasEvent); + result.insert("state", roomStateObj); } + if (!q->readMarkerEventId().isEmpty()) { - QJsonObject content; - content.insert("alias", this->canonicalAlias); - - QJsonObject canonicalAliasEvent; - canonicalAliasEvent.insert("type", QStringLiteral("m.room.canonical_alias")); - canonicalAliasEvent.insert("origin_server_ts", nowTimestamp); - stateEvents.append(canonicalAliasEvent); - } + 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 roomStateObj; - roomStateObj.insert("events", stateEvents); + QJsonObject ephemeralObj; + ephemeralObj.insert("events", ephemeralEvents); - QJsonObject result; - result.insert("state", roomStateObj); + result.insert("ephemeral", ephemeralObj); + } QJsonObject unreadNotificationsObj; unreadNotificationsObj.insert("highlight_count", highlightCount); @@ -947,7 +963,8 @@ QJsonObject Room::Private::toJson() const { return result; } -QJsonObject Room::toJson() const { +QJsonObject Room::toJson() const +{ return d->toJson(); } -- cgit v1.2.3 From 68c3727db0e2fd4cc6d08d3969f3494a906ef4d4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Sep 2017 18:59:49 +0900 Subject: Room: Fixed a special case with invalid-read-marker-becoming-valid It's a case when the last-read-event id refers to an event that was outside the loaded timeline and has just arrived. Depending on what messages follow the discovered last-read one, we might need to promote the read marker and update unreadMessages flag. The latter is especially relevant in our current situation when empty timelines upon the application startup are a norm. --- room.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/room.cpp b/room.cpp index 1a6b055d..7eb0dc59 100644 --- a/room.cpp +++ b/room.cpp @@ -117,7 +117,8 @@ class Room::Private void dropDuplicateEvents(RoomEvents* events) const; void setLastReadEvent(User* u, const QString& eventId); - rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); + rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); QJsonObject toJson() const; @@ -208,13 +209,14 @@ void Room::Private::setLastReadEvent(User* u, const QString& eventId) } Room::Private::rev_iter_pair_t -Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker) +Room::Private::promoteReadMarker(User* u, Room::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 (prevMarker <= newMarker) // Remember, we deal with reverse iterators + if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators return { prevMarker, prevMarker }; Q_ASSERT(newMarker < timeline.crend()); @@ -687,9 +689,22 @@ void Room::addHistoricalMessageEvents(RoomEvents events) void Room::doAddHistoricalMessageEvents(const RoomEvents& events) { Q_ASSERT(!events.empty()); + + const bool thereWasNoReadMarker = readMarker() == timelineEdge(); // Historical messages arrive in newest-to-oldest order for (auto e: events) d->prependEvent(e); + + // Catch a special case when the last read event id refers to an event + // that was outside the loaded timeline and has just arrived. Depending on + // other messages next to the last read one, we might need to promote + // the read marker and update unreadMessages flag. + const auto curReadMarker = readMarker(); + if (thereWasNoReadMarker && curReadMarker != timelineEdge()) + { + qCDebug(MAIN) << "Discovered last read event in a historical batch"; + d->promoteReadMarker(localUser(), curReadMarker, true); + } qCDebug(MAIN) << "Room" << displayName() << "received" << events.size() << "past events; the oldest event is now" << d->timeline.front(); } -- cgit v1.2.3 From f42a3090d40343166d2abd198e66fd13e4c7ccd1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Sep 2017 12:31:19 +0900 Subject: Room::updateData(): Don't profile empty structures --- room.cpp | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/room.cpp b/room.cpp index 7eb0dc59..94b5cd29 100644 --- a/room.cpp +++ b/room.cpp @@ -532,27 +532,35 @@ void Room::updateData(SyncRoomData&& data) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); - QElapsedTimer et; et.start(); - - processStateEvents(data.state); - qCDebug(PROFILER) << "*** Room::processStateEvents(state):" - << et.elapsed() << "ms," << data.state.size() << "events"; - - et.restart(); - // State changes can arrive in a timeline event; so check those. - processStateEvents(data.timeline); - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" - << et.elapsed() << "ms," << data.timeline.size() << "events"; - et.restart(); - addNewMessageEvents(data.timeline.release()); - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et.elapsed() << "ms"; - - et.restart(); - for( auto ephemeralEvent: data.ephemeral ) + QElapsedTimer et; + if (!data.state.empty()) { - processEphemeralEvent(ephemeralEvent); + et.start(); + processStateEvents(data.state); + qCDebug(PROFILER) << "*** Room::processStateEvents(state):" + << et.elapsed() << "ms," << data.state.size() << "events"; + } + if (!data.timeline.empty()) + { + et.restart(); + // State changes can arrive in a timeline event; so check those. + processStateEvents(data.timeline); + qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + << et.elapsed() << "ms," << data.timeline.size() << "events"; + + et.restart(); + addNewMessageEvents(data.timeline.release()); + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" + << et.elapsed() << "ms"; + } + if (!data.ephemeral.empty()) + { + et.restart(); + for( auto ephemeralEvent: data.ephemeral ) + processEphemeralEvent(ephemeralEvent); + qCDebug(PROFILER) << "*** Room::processEphemeralEvents():" + << et.elapsed() << "ms"; } - qCDebug(PROFILER) << "*** Room::processEphemeralEvents():" << et.elapsed() << "ms"; if( data.highlightCount != d->highlightCount ) { -- cgit v1.2.3 From 7e8c2ee1d00e43aab90030493c31aef0b4467f71 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Sep 2017 13:00:21 +0900 Subject: Minor optimisations in sync data parsing --- jobs/syncjob.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 062f1b15..78a9e93f 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -59,27 +59,30 @@ BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) return d.parseJson(data); } -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) { +BaseJob::Status SyncData::parseJson(const QJsonDocument &data) +{ QElapsedTimer et; et.start(); + QJsonObject json = data.object(); nextBatch_ = json.value("next_batch").toString(); // TODO: presence // TODO: account_data QJsonObject rooms = json.value("rooms").toObject(); - const struct { QString jsonKey; JoinState enumVal; } roomStates[] + static const struct { QString jsonKey; JoinState enumVal; } roomStates[] { { "join", JoinState::Join }, { "invite", JoinState::Invite }, { "leave", JoinState::Leave } }; - for (auto roomState: roomStates) + for (const auto& roomState: roomStates) { const QJsonObject rs = rooms.value(roomState.jsonKey).toObject(); // We have a Qt container on the right and an STL one on the left roomData.reserve(static_cast(rs.size())); - for( auto rkey: rs.keys() ) - roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject()); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + roomData.emplace_back(roomIt.key(), roomState.enumVal, + roomIt.value().toObject()); } qCDebug(PROFILER) << "*** SyncData::parseJson():" << et.elapsed() << "ms"; return BaseJob::Success; -- cgit v1.2.3 From b01591bddbcc4bcf3957feeb6b4b2875a9a2d978 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Sep 2017 14:05:16 +0900 Subject: BaseJob: track the outcome of sendRequest() in the logs Also: no reason to start the job timer if the request is not running, so don't even bother. --- jobs/basejob.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 3057ed75..6b2ebc58 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -164,8 +164,14 @@ void BaseJob::start() d->sendRequest(); connect( d->reply.data(), &QNetworkReply::sslErrors, this, &BaseJob::sslErrors ); connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); - d->timer.start(getCurrentTimeout()); - emit started(); + if (d->reply->isRunning()) + { + d->timer.start(getCurrentTimeout()); + qCDebug(d->logCat) << this << "request has been sent"; + emit started(); + } + else + qCWarning(d->logCat) << this << "request could not start"; } void BaseJob::gotReply() -- cgit v1.2.3 From 81c04033fa32ed0fa45c44db22ce11ff0636669b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Sep 2017 20:47:28 +0900 Subject: User: no more croppedAvatar(); added avatarUrl() accessor avatarUrl() is not yet invokable from QML; I'm considering to make all the simple things in User Q_PROPERTies instead. --- user.cpp | 49 +++++++++++++++++++++++-------------------------- user.h | 3 ++- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/user.cpp b/user.cpp index f9f48539..12eb2e0b 100644 --- a/user.cpp +++ b/user.cpp @@ -28,7 +28,6 @@ #include #include #include -#include using namespace QMatrixClient; @@ -52,7 +51,9 @@ class User::Private QSize requestedSize; bool avatarValid; bool avatarOngoingRequest; - QVector scaledAvatars; + /// Map of requested size to the actual pixmap used for it + /// (it's a shame that QSize has no predefined qHash()). + QHash, QPixmap> scaledAvatars; QString bridged; void requestAvatar(); @@ -91,25 +92,22 @@ QString User::bridged() const { } QPixmap User::avatar(int width, int height) -{ - return croppedAvatar(width, height); // FIXME: Return an uncropped avatar; -} - -QPixmap User::croppedAvatar(int width, int height) { QSize size(width, height); - if( !d->avatarValid + // FIXME: Alternating between longer-width and longer-height requests + // is a sure way to trick the below code into constantly getting another + // image from the server because the existing one is alleged unsatisfactory. + // This is plain abuse by the client, though; so not critical for now. + if( (!d->avatarValid && d->avatarUrl.isValid() && !d->avatarOngoingRequest) || width > d->requestedSize.width() || height > d->requestedSize.height() ) { - if( !d->avatarOngoingRequest && d->avatarUrl.isValid() ) - { - qCDebug(MAIN) << "Getting avatar for" << id(); - d->requestedSize = size; - d->avatarOngoingRequest = true; - QTimer::singleShot(0, this, SLOT(requestAvatar())); - } + qCDebug(MAIN) << "Getting avatar for" << id() + << "from" << d->avatarUrl.toString(); + d->requestedSize = size; + d->avatarOngoingRequest = true; + QTimer::singleShot(0, this, SLOT(requestAvatar())); } if( d->avatar.isNull() ) @@ -120,19 +118,18 @@ QPixmap User::croppedAvatar(int width, int height) d->avatar = d->defaultIcon.pixmap(size); } - for (const QPixmap& p: d->scaledAvatars) + auto& pixmap = d->scaledAvatars[{width, height}]; // Create the entry if needed + if (pixmap.isNull()) { - if (p.size() == size) - return p; + pixmap = d->avatar.scaled(width, height, + Qt::KeepAspectRatio, Qt::SmoothTransformation); } - QPixmap newlyScaled = d->avatar.scaled(size, - Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - QPixmap scaledAndCroped = newlyScaled.copy( - std::max((newlyScaled.width() - width)/2, 0), - std::max((newlyScaled.height() - height)/2, 0), - width, height); - d->scaledAvatars.push_back(scaledAndCroped); - return scaledAndCroped; + return pixmap; +} + +const QUrl& User::avatarUrl() const +{ + return d->avatarUrl; } void User::processEvent(Event* event) diff --git a/user.h b/user.h index ff81305b..a2d58908 100644 --- a/user.h +++ b/user.h @@ -53,7 +53,8 @@ namespace QMatrixClient Q_INVOKABLE QString bridged() const; QPixmap avatar(int requestedWidth, int requestedHeight); - QPixmap croppedAvatar(int requestedWidth, int requestedHeight); + + const QUrl& avatarUrl() const; void processEvent(Event* event); -- cgit v1.2.3 From f936182135d166e5dea734775e24cabd4b763c64 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Sep 2017 20:48:40 +0900 Subject: Room::Private::toJson(): save user avatar URLs as well Otherwise, users are doomed to stay avatarless upon restoration, until they update avatars again. --- room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/room.cpp b/room.cpp index 94b5cd29..332d6fa7 100644 --- a/room.cpp +++ b/room.cpp @@ -944,7 +944,7 @@ QJsonObject Room::Private::toJson() const QJsonObject content; content.insert("membership", QStringLiteral("join")); content.insert("displayname", i->displayname()); - // avatar URL is not available + content.insert("avatar_url", i->avatarUrl().toString()); QJsonObject memberEvent; memberEvent.insert("type", QStringLiteral("m.room.member")); -- cgit v1.2.3