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 da975f68f6a8503bf5466292dcdceed8c6f7fa6f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 5 Sep 2017 19:23:13 +0900 Subject: Initialize more properly to fix a warning --- jobs/basejob.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 26ceb268..157ac034 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -263,13 +263,13 @@ void BaseJob::finishJob() BaseJob::duration_t BaseJob::getCurrentTimeout() const { - static const std::array timeouts = { 90, 90, 120, 120 }; + static const std::array timeouts ({ 90, 90, 120, 120 }); return timeouts[std::min(d->retriesTaken, timeouts.size() - 1)] * 1000; } BaseJob::duration_t BaseJob::getNextRetryInterval() const { - static const std::array intervals = { 5, 10, 30 }; + static const std::array intervals ({ 5, 10, 30 }); return intervals[std::min(d->retriesTaken, intervals.size() - 1)] * 1000; } -- cgit v1.2.3 From 180d62e094a1a6b6fc69c99b291ef075dc649135 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 8 Sep 2017 16:51:03 +0900 Subject: Revert previous commit as it breaks building with VC 2015 This reverts commit da975f68f6a8503bf5466292dcdceed8c6f7fa6f. --- jobs/basejob.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 157ac034..26ceb268 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -263,13 +263,13 @@ void BaseJob::finishJob() BaseJob::duration_t BaseJob::getCurrentTimeout() const { - static const std::array timeouts ({ 90, 90, 120, 120 }); + static const std::array timeouts = { 90, 90, 120, 120 }; return timeouts[std::min(d->retriesTaken, timeouts.size() - 1)] * 1000; } BaseJob::duration_t BaseJob::getNextRetryInterval() const { - static const std::array intervals ({ 5, 10, 30 }); + static const std::array intervals = { 5, 10, 30 }; return intervals[std::min(d->retriesTaken, intervals.size() - 1)] * 1000; } -- cgit v1.2.3 From c4f690edf4cc2e11f19370f80ddd7428aba0b536 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 30 May 2017 08:56:59 +0900 Subject: Connection: Room and User factories are std::functions now Instead of createUser() and createRoom() virtual functions, use std::function<> to store predefined lambdas that would create respective descendants from User and Room, respectively. No more need QuaternionConnection just for the sake of creating a QuaternionRoom. --- connection.cpp | 16 ++++++---------- connection.h | 29 +++++++++++++++++++---------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/connection.cpp b/connection.cpp index 7920125d..2c9ee88a 100644 --- a/connection.cpp +++ b/connection.cpp @@ -238,7 +238,7 @@ User* Connection::user(const QString& userId) { if( d->userMap.contains(userId) ) return d->userMap.value(userId); - User* user = createUser(userId); + auto* user = createUser(this, userId); d->userMap.insert(userId, user); return user; } @@ -297,7 +297,7 @@ Room* Connection::provideRoom(const QString& id) return d->roomMap.value(id); // Not yet in the map, create a new one. - Room* room = createRoom(id); + auto* room = createRoom(this, id); if (room) { d->roomMap.insert( id, room ); @@ -309,15 +309,11 @@ Room* Connection::provideRoom(const QString& id) return room; } -User* Connection::createUser(const QString& userId) -{ - return new User(userId, this); -} +std::function Connection::createRoom = + [](Connection* c, const QString& id) { return new Room(c, id); }; -Room* Connection::createRoom(const QString& roomId) -{ - return new Room(this, roomId); -} +std::function Connection::createUser = + [](Connection* c, const QString& id) { return new User(id, c); }; QByteArray Connection::generateTxnId() { diff --git a/connection.h b/connection.h index 0b8500b9..4b0413e3 100644 --- a/connection.h +++ b/connection.h @@ -22,6 +22,8 @@ #include #include +#include + namespace QMatrixClient { class Room; @@ -96,6 +98,20 @@ namespace QMatrixClient */ Q_INVOKABLE QByteArray generateTxnId(); + template + static void setRoomType() + { + createRoom = + [](Connection* c, const QString& id) { return new T(c, id); }; + } + + template + static void setUserType() + { + createUser = + [](Connection* c, const QString& id) { return new T(id, c); }; + } + signals: void resolved(); void connected(); @@ -130,18 +146,11 @@ namespace QMatrixClient */ Room* provideRoom(const QString& roomId); - /** - * makes it possible for derived classes to have its own User class - */ - virtual User* createUser(const QString& userId); - - /** - * makes it possible for derived classes to have its own Room class - */ - virtual Room* createRoom(const QString& roomId); - private: class Private; Private* d; + + static std::function createRoom; + static std::function createUser; }; } // namespace QMatrixClient -- cgit v1.2.3 From d57485bb3a7d980642e7f68edd5785c144acc742 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 8 Sep 2017 18:36:44 +0900 Subject: converters.h: Facility methods for generated jobs A cherry-pick from the kitsune-apigen branch; a family of toJson() and fromJson<>() functions to unify conversion of data back and forth. --- jobs/converters.h | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 jobs/converters.h diff --git a/jobs/converters.h b/jobs/converters.h new file mode 100644 index 00000000..376dfeab --- /dev/null +++ b/jobs/converters.h @@ -0,0 +1,89 @@ +/****************************************************************************** +* Copyright (C) 2017 Kitsune Ral +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include +#include +#include + +namespace QMatrixClient +{ + template + inline QJsonValue toJson(T val) + { + return QJsonValue(val); + } + + template + inline QJsonValue toJson(const QVector& vals) + { + QJsonArray ar; + for (const auto& v: vals) + ar.push_back(toJson(v)); + return ar; + } + + inline QJsonValue toJson(const QStringList& strings) + { + return QJsonArray::fromStringList(strings); + } + + template + inline T fromJson(const QJsonValue& jv) + { + return QVariant(jv).value(); + } + + template <> + inline int fromJson(const QJsonValue& jv) + { + return jv.toInt(); + } + + template <> + inline qint64 fromJson(const QJsonValue& jv) + { + return static_cast(jv.toDouble()); + } + + template <> + inline double fromJson(const QJsonValue& jv) + { + return jv.toDouble(); + } + + template <> + inline QString fromJson(const QJsonValue& jv) + { + return jv.toString(); + } + + template <> + inline QDateTime fromJson(const QJsonValue& jv) + { + return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); + } + + template <> + inline QDate fromJson(const QJsonValue& jv) + { + return QDateTime::fromMSecsSinceEpoch( + fromJson(jv), Qt::UTC).date(); + } +} // namespace QMatrixClient \ No newline at end of file -- cgit v1.2.3 From 8ae1da50f8474a698ce83f61233b3a8975faeaee Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Sep 2017 20:14:08 +0900 Subject: First files made by api-generator Actual usage will come with the next commit. --- jobs/generated/inviting.cpp | 37 +++++++++++++++++++++++++++++++++++++ jobs/generated/inviting.h | 42 ++++++++++++++++++++++++++++++++++++++++++ jobs/generated/kicking.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ jobs/generated/kicking.h | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 jobs/generated/inviting.cpp create mode 100644 jobs/generated/inviting.h create mode 100644 jobs/generated/kicking.cpp create mode 100644 jobs/generated/kicking.h diff --git a/jobs/generated/inviting.cpp b/jobs/generated/inviting.cpp new file mode 100644 index 00000000..e5e7f410 --- /dev/null +++ b/jobs/generated/inviting.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#include "inviting.h" + + +#include "../converters.h" + +#include + +using namespace QMatrixClient; + + + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + + +InviteUserJob::InviteUserJob(const ConnectionData* connection, + QString roomId + , + QString user_id + ) + : BaseJob(connection, HttpVerb::Post, "InviteUserJob" + , basePath % "/rooms/" % roomId % "/invite" + , Query { } + , Data { + { "user_id", toJson(user_id) } + } + + ) +{ } + + + + diff --git a/jobs/generated/inviting.h b/jobs/generated/inviting.h new file mode 100644 index 00000000..af5a426d --- /dev/null +++ b/jobs/generated/inviting.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#pragma once + + +#include "../basejob.h" + + + +#include + + + + +namespace QMatrixClient +{ + + + // Operations + + /** + + */ + class InviteUserJob : public BaseJob + { + public: + InviteUserJob(const ConnectionData* connection + + , + QString roomId + + , + QString user_id + ); + + }; + + +} // namespace QMatrixClient diff --git a/jobs/generated/kicking.cpp b/jobs/generated/kicking.cpp new file mode 100644 index 00000000..726f6fb0 --- /dev/null +++ b/jobs/generated/kicking.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#include "kicking.h" + + +#include "../converters.h" + +#include + +using namespace QMatrixClient; + + + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + + +KickJob::KickJob(const ConnectionData* connection, + QString roomId + , + QString user_id + , + QString reason + ) + : BaseJob(connection, HttpVerb::Post, "KickJob" + , basePath % "/rooms/" % roomId % "/kick" + , Query { } + , Data { + { "user_id", toJson(user_id) }, + + { "reason", toJson(reason) } + } + + ) +{ } + + + + diff --git a/jobs/generated/kicking.h b/jobs/generated/kicking.h new file mode 100644 index 00000000..7b183b08 --- /dev/null +++ b/jobs/generated/kicking.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#pragma once + + +#include "../basejob.h" + + + +#include + + + + +namespace QMatrixClient +{ + + + // Operations + + /** + + */ + class KickJob : public BaseJob + { + public: + KickJob(const ConnectionData* connection + + , + QString roomId + + , + QString user_id + + , + QString reason + ); + + }; + + +} // namespace QMatrixClient -- cgit v1.2.3 From 24e3d967d7c9029147202e10385b0b8e8881e4d9 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Sep 2017 20:17:42 +0900 Subject: Kicking, inviting, exposing rooms in Invite state Kicking and inviting use generated job classes. Rooms in Invite state are stored separately in the hash from those in Join/Leave state because The Spec says so. For clients, this means that the same room may appear twice in the rooms map if it's been left and then the user was again invited to it. The code in Quaternion that properly processes this will arrive shortly. --- connection.cpp | 69 ++++++++++++++++++++++++++++++++++---------------------- connection.h | 18 +++++++++++---- jobs/syncjob.cpp | 5 ++-- jobs/syncjob.h | 1 - room.cpp | 23 +++++++++++++++---- room.h | 5 +++- 6 files changed, 80 insertions(+), 41 deletions(-) diff --git a/connection.cpp b/connection.cpp index 2c9ee88a..5d8a42e3 100644 --- a/connection.cpp +++ b/connection.cpp @@ -50,7 +50,11 @@ class Connection::Private Connection* q; ConnectionData* data; - QHash roomMap; + // A complex key below is a pair of room name and whether its + // state is Invited. The spec mandates to keep Invited room state + // separately so we should, e.g., keep objects for Invite and + // Leave state of the same room. + QHash, Room*> roomMap; QHash userMap; QString username; QString password; @@ -160,7 +164,7 @@ void Connection::sync(int timeout) d->data->setLastEvent(job->nextBatch()); for( auto&& roomData: job->takeRoomData() ) { - if ( auto* r = provideRoom(roomData.roomId) ) + if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) r->updateData(std::move(roomData)); } d->syncJob = nullptr; @@ -197,20 +201,12 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const JoinRoomJob* Connection::joinRoom(const QString& roomAlias) { - auto job = callApi(roomAlias); - connect( job, &BaseJob::success, [=] () { - if ( Room* r = provideRoom(job->roomId()) ) - emit joinedRoom(r); - }); - return job; + return callApi(roomAlias); } void Connection::leaveRoom(Room* room) { - auto job = callApi(room->id()); - connect( job, &BaseJob::success, [=] () { - emit leftRoom(room); - }); + callApi(room->id()); } RoomMessagesJob* Connection::getMessages(Room* room, const QString& from) const @@ -275,7 +271,7 @@ int Connection::millisToReconnect() const return d->syncJob ? d->syncJob->millisToRetry() : 0; } -QHash< QString, Room* > Connection::roomMap() const +const QHash< QPair, Room* >& Connection::roomMap() const { return d->roomMap; } @@ -285,36 +281,55 @@ const ConnectionData* Connection::connectionData() const return d->data; } -Room* Connection::provideRoom(const QString& id) +Room* Connection::provideRoom(const QString& id, JoinState joinState) { + // TODO: This whole function is a strong case for a RoomManager class. if (id.isEmpty()) { qCDebug(MAIN) << "Connection::provideRoom() with empty id, doing nothing"; return nullptr; } - if (d->roomMap.contains(id)) - return d->roomMap.value(id); - - // Not yet in the map, create a new one. - auto* room = createRoom(this, id); - if (room) + const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + auto* room = d->roomMap.value(roomKey, nullptr); + if (!room) { - d->roomMap.insert( id, room ); + room = createRoom(this, id, joinState); + if (!room) + { + qCritical() << "Failed to create a room!!!" << id; + return nullptr; + } + qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second; + + d->roomMap.insert(roomKey, room); emit newRoom(room); - } else { - qCritical() << "Failed to create a room!!!" << id; + } + else if (room->joinState() != joinState) + { + room->setJoinState(joinState); + if (joinState == JoinState::Leave) + emit leftRoom(room); + else if (joinState == JoinState::Join) + emit joinedRoom(room); + } + + if (joinState != JoinState::Invite && d->roomMap.contains({id, true})) + { + // Preempt the Invite room after it's been acted upon (joined or left). + qCDebug(MAIN) << "Deleting invited state"; + delete d->roomMap.take({id, true}); } return room; } -std::function Connection::createRoom = - [](Connection* c, const QString& id) { return new Room(c, id); }; +Connection::room_factory_t Connection::createRoom = + [](Connection* c, const QString& id, JoinState joinState) + { return new Room(c, id, joinState); }; -std::function Connection::createUser = +Connection::user_factory_t Connection::createUser = [](Connection* c, const QString& id) { return new User(id, c); }; - QByteArray Connection::generateTxnId() { return d->data->generateTxnId(); diff --git a/connection.h b/connection.h index 4b0413e3..b118ffb0 100644 --- a/connection.h +++ b/connection.h @@ -18,6 +18,8 @@ #pragma once +#include "joinstate.h" + #include #include #include @@ -41,11 +43,16 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT public: + using room_factory_t = + std::function; + using user_factory_t = + std::function; + explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); virtual ~Connection(); - QHash roomMap() const; + const QHash, Room*>& roomMap() const; Q_INVOKABLE virtual void resolveServer(const QString& domain); Q_INVOKABLE virtual void connectToServer(const QString& user, @@ -102,7 +109,8 @@ namespace QMatrixClient static void setRoomType() { createRoom = - [](Connection* c, const QString& id) { return new T(c, id); }; + [](Connection* c, const QString& id, JoinState joinState) + { return new T(c, id, joinState); }; } template @@ -144,13 +152,13 @@ namespace QMatrixClient * @return a pointer to a Room object with the specified id; nullptr * if roomId is empty if createRoom() failed to create a Room object. */ - Room* provideRoom(const QString& roomId); + Room* provideRoom(const QString& roomId, JoinState joinState); private: class Private; Private* d; - static std::function createRoom; - static std::function createUser; + static room_factory_t createRoom; + static user_factory_t createUser; }; } // namespace QMatrixClient diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 29ddc2e6..38cfcb2a 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -99,15 +99,14 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) - , state("state") + , state(joinState == JoinState::Invite ? "invite_state" : "state") , timeline("timeline") , ephemeral("ephemeral") , accountData("account_data") - , inviteState("invite_state") { switch (joinState) { case JoinState::Invite: - inviteState.fromJson(room_); + state.fromJson(room_); break; case JoinState::Join: state.fromJson(room_); diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 07824e23..57a87c9f 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -51,7 +51,6 @@ namespace QMatrixClient Batch timeline; Batch ephemeral; Batch accountData; - Batch inviteState; bool timelineLimited; QString timelinePrevBatch; diff --git a/room.cpp b/room.cpp index 547b74c4..78e5b80d 100644 --- a/room.cpp +++ b/room.cpp @@ -18,6 +18,9 @@ #include "room.h" +#include "jobs/generated/kicking.h" +#include "jobs/generated/inviting.h" + #include #include @@ -48,9 +51,9 @@ class Room::Private typedef QMultiHash members_map_t; typedef std::pair rev_iter_pair_t; - Private(Connection* c, QString id_) + Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(std::move(id_)) - , joinState(JoinState::Join), unreadMessages(false) + , joinState(initialJoinState), unreadMessages(false) , highlightCount(0), notificationCount(0), roomMessagesJob(nullptr) { } @@ -134,8 +137,8 @@ class Room::Private } }; -Room::Room(Connection* connection, QString id) - : QObject(connection), d(new Private(connection, id)) +Room::Room(Connection* connection, QString id, JoinState initialJoinState) + : QObject(connection), d(new Private(connection, id, initialJoinState)) { // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ @@ -194,6 +197,8 @@ void Room::setJoinState(JoinState state) if( state == oldState ) return; d->joinState = state; + qCDebug(MAIN) << "Room" << id() << "changed state: " + << int(oldState) << "->" << int(state); emit joinStateChanged(oldState, state); } @@ -601,11 +606,21 @@ void Room::Private::getPreviousContent(int limit) } } +void Room::inviteToRoom(const QString& memberId) const +{ + connection()->callApi(id(), memberId); +} + void Room::leaveRoom() const { connection()->callApi(id()); } +void Room::kickMember(const QString& memberId, const QString& reason) const +{ + connection()->callApi(id(), memberId, reason); +} + void Room::Private::dropDuplicateEvents(RoomEvents* events) const { // Collect all duplicate events at the end of the container diff --git a/room.h b/room.h index 23a1412d..9465a960 100644 --- a/room.h +++ b/room.h @@ -77,7 +77,7 @@ namespace QMatrixClient using Timeline = std::deque; using rev_iter_t = Timeline::const_reverse_iterator; - Room(Connection* connection, QString id); + Room(Connection* connection, QString id, JoinState initialJoinState); virtual ~Room(); Connection* connection() const; @@ -154,7 +154,10 @@ namespace QMatrixClient void getPreviousContent(int limit = 10); + void inviteToRoom(const QString& memberId) const; void leaveRoom() const; + void kickMember(const QString& memberId, const QString& reason) const; + void userRenamed(User* user, QString oldName); /** Mark all messages in the room as read */ -- cgit v1.2.3 From 6300d53663ea118a1f80ceed24648d8553766e17 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Sep 2017 21:34:18 +0900 Subject: Collect files from jobs/generated into the list of built sources --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a345e06..9e3abce1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,11 +81,13 @@ set(libqmatrixclient_SRCS jobs/syncjob.cpp jobs/mediathumbnailjob.cpp jobs/logoutjob.cpp - ) +) + +aux_source_directory(jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) -add_library(qmatrixclient ${libqmatrixclient_SRCS}) +add_library(qmatrixclient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) set_property(TARGET qmatrixclient PROPERTY VERSION "0.0.0") set_property(TARGET qmatrixclient PROPERTY SOVERSION 0 ) -- cgit v1.2.3 From 5ddfbf25f2657f232649a9fdd2ad481127f6c349 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 13 Sep 2017 21:09:20 +0900 Subject: Add a missing #include --- jobs/converters.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jobs/converters.h b/jobs/converters.h index 376dfeab..f9ab0269 100644 --- a/jobs/converters.h +++ b/jobs/converters.h @@ -21,6 +21,7 @@ #include #include #include +#include namespace QMatrixClient { @@ -83,7 +84,6 @@ namespace QMatrixClient template <> inline QDate fromJson(const QJsonValue& jv) { - return QDateTime::fromMSecsSinceEpoch( - fromJson(jv), Qt::UTC).date(); + return fromJson(jv).date(); } -} // namespace QMatrixClient \ No newline at end of file +} // namespace QMatrixClient -- cgit v1.2.3 From cb3848c1e7fe09c2e778d38139c399b9f0484ce5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 13 Sep 2017 21:10:36 +0900 Subject: MediaThumbnailJob: get rid of useless pimpl; add scaledThumbnail() Also use scaledThumbnail() in User::requestAvatar() --- jobs/mediathumbnailjob.cpp | 17 +++++------------ jobs/mediathumbnailjob.h | 5 ++--- user.cpp | 5 ++--- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/jobs/mediathumbnailjob.cpp b/jobs/mediathumbnailjob.cpp index 9bb731b9..9579f6b2 100644 --- a/jobs/mediathumbnailjob.cpp +++ b/jobs/mediathumbnailjob.cpp @@ -23,12 +23,6 @@ using namespace QMatrixClient; -class MediaThumbnailJob::Private -{ - public: - QPixmap thumbnail; -}; - MediaThumbnailJob::MediaThumbnailJob(const ConnectionData* data, QUrl url, QSize requestedSize, ThumbnailType thumbnailType) : BaseJob(data, HttpVerb::Get, "MediaThumbnailJob", @@ -39,22 +33,21 @@ MediaThumbnailJob::MediaThumbnailJob(const ConnectionData* data, QUrl url, QSize , { "method", thumbnailType == ThumbnailType::Scale ? "scale" : "crop" } })) - , d(new Private) { } -MediaThumbnailJob::~MediaThumbnailJob() +QPixmap MediaThumbnailJob::thumbnail() { - delete d; + return pixmap; } -QPixmap MediaThumbnailJob::thumbnail() +QPixmap MediaThumbnailJob::scaledThumbnail(QSize toSize) { - return d->thumbnail; + return pixmap.scaled(toSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } BaseJob::Status MediaThumbnailJob::parseReply(QByteArray data) { - if( !d->thumbnail.loadFromData(data) ) + if( !pixmap.loadFromData(data) ) { qCDebug(JOBS) << "MediaThumbnailJob: could not read image data"; } diff --git a/jobs/mediathumbnailjob.h b/jobs/mediathumbnailjob.h index 307d0a99..186da829 100644 --- a/jobs/mediathumbnailjob.h +++ b/jobs/mediathumbnailjob.h @@ -31,15 +31,14 @@ namespace QMatrixClient public: MediaThumbnailJob(const ConnectionData* data, QUrl url, QSize requestedSize, ThumbnailType thumbnailType=ThumbnailType::Scale); - virtual ~MediaThumbnailJob(); QPixmap thumbnail(); + QPixmap scaledThumbnail(QSize toSize); protected: Status parseReply(QByteArray data) override; private: - class Private; - Private* d; + QPixmap pixmap; }; } diff --git a/user.cpp b/user.cpp index 8d37eef6..f9f48539 100644 --- a/user.cpp +++ b/user.cpp @@ -170,12 +170,11 @@ void User::requestAvatar() void User::Private::requestAvatar() { - MediaThumbnailJob* job = connection->getThumbnail(avatarUrl, requestedSize); + auto* job = connection->callApi(avatarUrl, requestedSize); connect( job, &MediaThumbnailJob::success, [=]() { avatarOngoingRequest = false; avatarValid = true; - avatar = job->thumbnail().scaled(requestedSize, - Qt::KeepAspectRatio, Qt::SmoothTransformation); + avatar = job->scaledThumbnail(requestedSize); scaledAvatars.clear(); emit q->avatarChanged(q); }); -- cgit v1.2.3 From 6b40c313f8e4a0964e857973d8f9636a9f833f9d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 14 Sep 2017 20:19:41 +0900 Subject: Better API for clients to catch up on room list changes joinedRoom() and leftRoom() now pass the preempted Invite state of the room as well; roomMap() only returns Invite and Join rooms, not Leave. --- connection.cpp | 57 ++++++++++++++++++++++++++++++++++++++++++--------------- connection.h | 7 ++++--- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/connection.cpp b/connection.cpp index 5d8a42e3..fdf58e9e 100644 --- a/connection.cpp +++ b/connection.cpp @@ -271,9 +271,18 @@ int Connection::millisToReconnect() const return d->syncJob ? d->syncJob->millisToRetry() : 0; } -const QHash< QPair, Room* >& Connection::roomMap() const +QHash< QPair, Room* > Connection::roomMap() const { - return d->roomMap; + // Copy-on-write-and-remove-elements is faster than copying elements one by one. + QHash< QPair, Room* > roomMap = d->roomMap; + for (auto it = roomMap.begin(); it != roomMap.end(); ) + { + if (it.value()->joinState() == JoinState::Leave) + it = roomMap.erase(it); + else + ++it; + } + return roomMap; } const ConnectionData* Connection::connectionData() const @@ -290,6 +299,23 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) return nullptr; } + // Room transitions from the Connection standpoint: + // - none -> (new) Invite + // - none -> (new) Join + // - none -> (new) Leave + // - Invite -> (new) Join replaces Invite (deleted) + // - Invite -> (new) Leave (archived) replaces Invite (deleted) + // - Join -> (moves to) Leave + // - Leave -> (new) Invite, Leave + // - Leave -> (moves to) Join + // Room transitions from the user's standpoint (what's seen in signals): + // - none -> Invite: newRoom(Invite) + // - none -> Join: newRoom(Join) or Room::joinStateChanged(Join); joinedRoom + // - Invite -> Invite replaced with Join: + // newRoom(Join); joinedRoom; aboutToDeleteRoom(Invite) + // - Invite -> Invite replaced with Leave (none): + // newRoom(Leave); leftRoom; aboutToDeleteRoom(Invite) + // - Join -> Leave (none): leftRoom const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); if (!room) @@ -297,7 +323,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) room = createRoom(this, id, joinState); if (!room) { - qCritical() << "Failed to create a room!!!" << id; + qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; } qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second; @@ -305,20 +331,21 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) d->roomMap.insert(roomKey, room); emit newRoom(room); } - else if (room->joinState() != joinState) - { - room->setJoinState(joinState); - if (joinState == JoinState::Leave) - emit leftRoom(room); - else if (joinState == JoinState::Join) - emit joinedRoom(room); - } - if (joinState != JoinState::Invite && d->roomMap.contains({id, true})) + if (joinState != JoinState::Invite) { - // Preempt the Invite room after it's been acted upon (joined or left). - qCDebug(MAIN) << "Deleting invited state"; - delete d->roomMap.take({id, true}); + // Preempt the Invite room (if any) with a room in Join/Leave state. + auto prevInvite = d->roomMap.take({id, true}); + if (joinState == JoinState::Join) + joinedRoom(room, prevInvite); + else if (joinState == JoinState::Leave) + leftRoom(room, prevInvite); + if (prevInvite) + { + qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); + emit aboutToDeleteRoom(prevInvite); + delete prevInvite; + } } return room; diff --git a/connection.h b/connection.h index b118ffb0..361e0870 100644 --- a/connection.h +++ b/connection.h @@ -52,7 +52,7 @@ namespace QMatrixClient Connection(); virtual ~Connection(); - const QHash, Room*>& roomMap() const; + QHash, Room*> roomMap() const; Q_INVOKABLE virtual void resolveServer(const QString& domain); Q_INVOKABLE virtual void connectToServer(const QString& user, @@ -128,8 +128,9 @@ namespace QMatrixClient void syncDone(); void newRoom(Room* room); - void joinedRoom(Room* room); - void leftRoom(Room* room); + void joinedRoom(Room* room, Room* prevInvite); + void leftRoom(Room* room, Room* prevInvite); + void aboutToDeleteRoom(Room* room); void loginError(QString error); void networkError(size_t nextAttempt, int inMilliseconds); -- cgit v1.2.3 From 726f8d464f4b29f6fd3dc92fa5493e239970b209 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Sep 2017 20:39:36 +0900 Subject: provideRoom: Added invitedRoom() signal; fixed issues with some transitions Notably: * setJoinState() invocation has been missing from the previous code * processing invites did not take into account that a Leave state may already exist, thereby forcing clients that display left rooms to look through their records just in case they have to replace a Leave with Invite. * joinedRoom() was emitted even when the room is not newly joined. --- connection.cpp | 57 ++++++++++++++++++++++++++++++++------------------------- connection.h | 5 +++-- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/connection.cpp b/connection.cpp index fdf58e9e..513d9242 100644 --- a/connection.cpp +++ b/connection.cpp @@ -299,26 +299,28 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) return nullptr; } - // Room transitions from the Connection standpoint: - // - none -> (new) Invite - // - none -> (new) Join - // - none -> (new) Leave - // - Invite -> (new) Join replaces Invite (deleted) - // - Invite -> (new) Leave (archived) replaces Invite (deleted) - // - Join -> (moves to) Leave - // - Leave -> (new) Invite, Leave - // - Leave -> (moves to) Join - // Room transitions from the user's standpoint (what's seen in signals): - // - none -> Invite: newRoom(Invite) - // - none -> Join: newRoom(Join) or Room::joinStateChanged(Join); joinedRoom - // - Invite -> Invite replaced with Join: - // newRoom(Join); joinedRoom; aboutToDeleteRoom(Invite) - // - Invite -> Invite replaced with Leave (none): - // newRoom(Leave); leftRoom; aboutToDeleteRoom(Invite) - // - Join -> Leave (none): leftRoom + // Room transitions: + // 1. none -> Invite: r=createRoom, emit invitedRoom(r,null) + // 2. none -> Join: r=createRoom, emit joinedRoom(r,null) + // 3. none -> Leave: r=createRoom, emit leftRoom(r,null) + // 4. inv=Invite -> Join: r=createRoom, emit joinedRoom(r,inv), delete Invite + // 4a. Leave, inv=Invite -> Join: change state, emit joinedRoom(r,inv), delete Invite + // 5. inv=Invite -> Leave: r=createRoom, emit leftRoom(r,inv), delete Invite + // 5a. r=Leave, inv=Invite -> Leave: emit leftRoom(r,inv), delete Invite + // 6. Join -> Leave: change state + // 7. r=Leave -> Invite: inv=createRoom, emit invitedRoom(inv,r) + // 8. Leave -> (changes to) Join const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); - if (!room) + if (room) + { + // Leave is a special case because in transition (5a) above + // joinState == room->joinState but we still have to preempt the Invite + // and emit a signal. For Invite and Join, there's no such problem. + if (room->joinState() == joinState && joinState != JoinState::Leave) + return room; + } + else { room = createRoom(this, id, joinState); if (!room) @@ -326,20 +328,25 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; } - qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second; - d->roomMap.insert(roomKey, room); + qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second; emit newRoom(room); } - - if (joinState != JoinState::Invite) + if (joinState == JoinState::Invite) + { + // prev is either Leave or nullptr + auto* prev = d->roomMap.value({id, false}, nullptr); + emit invitedRoom(room, prev); + } + else { + room->setJoinState(joinState); // Preempt the Invite room (if any) with a room in Join/Leave state. - auto prevInvite = d->roomMap.take({id, true}); + auto* prevInvite = d->roomMap.take({id, true}); if (joinState == JoinState::Join) - joinedRoom(room, prevInvite); + emit joinedRoom(room, prevInvite); else if (joinState == JoinState::Leave) - leftRoom(room, prevInvite); + emit leftRoom(room, prevInvite); if (prevInvite) { qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); diff --git a/connection.h b/connection.h index 361e0870..0dcb8c3f 100644 --- a/connection.h +++ b/connection.h @@ -128,8 +128,9 @@ namespace QMatrixClient void syncDone(); void newRoom(Room* room); - void joinedRoom(Room* room, Room* prevInvite); - void leftRoom(Room* room, Room* prevInvite); + void invitedRoom(Room* room, Room* prev); + void joinedRoom(Room* room, Room* prev); + void leftRoom(Room* room, Room* prev); void aboutToDeleteRoom(Room* room); void loginError(QString error); -- 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 From d51e7a43736096eb2776acd99e1aab6deeb65667 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Sep 2017 18:43:29 +0900 Subject: jobs: SetRoomStateJob (with or without state key); setting room topic --- CMakeLists.txt | 1 + events/roomtopicevent.h | 12 +++++++++ jobs/setroomstatejob.cpp | 32 ++++++++++++++++++++++++ jobs/setroomstatejob.h | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ room.cpp | 8 +++++- room.h | 1 + 6 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 jobs/setroomstatejob.cpp create mode 100644 jobs/setroomstatejob.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e3abce1..e55335ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ set(libqmatrixclient_SRCS jobs/checkauthmethods.cpp jobs/passwordlogin.cpp jobs/sendeventjob.cpp + jobs/setroomstatejob.cpp jobs/postreceiptjob.cpp jobs/joinroomjob.cpp jobs/leaveroomjob.cpp diff --git a/events/roomtopicevent.h b/events/roomtopicevent.h index fb849afe..95ad0e04 100644 --- a/events/roomtopicevent.h +++ b/events/roomtopicevent.h @@ -25,6 +25,9 @@ namespace QMatrixClient class RoomTopicEvent: public RoomEvent { public: + explicit RoomTopicEvent(const QString& topic) + : RoomEvent(Type::RoomTopic), _topic(topic) + { } explicit RoomTopicEvent(const QJsonObject& obj) : RoomEvent(Type::RoomTopic, obj) , _topic(contentJson()["topic"].toString()) @@ -32,6 +35,15 @@ namespace QMatrixClient QString topic() const { return _topic; } + QJsonObject toJson() const + { + QJsonObject obj; + obj.insert("topic", _topic); + return obj; + } + + static constexpr const char* TypeId = "m.room.topic"; + private: QString _topic; }; diff --git a/jobs/setroomstatejob.cpp b/jobs/setroomstatejob.cpp new file mode 100644 index 00000000..c2beb87b --- /dev/null +++ b/jobs/setroomstatejob.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "setroomstatejob.h" + +using namespace QMatrixClient; + +BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) +{ + _eventId = data.object().value("event_id").toString(); + if (!_eventId.isEmpty()) + return Success; + + qCDebug(JOBS) << data; + return { UserDefinedError, "No event_id in the JSON response" }; +} + diff --git a/jobs/setroomstatejob.h b/jobs/setroomstatejob.h new file mode 100644 index 00000000..1c72f31c --- /dev/null +++ b/jobs/setroomstatejob.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * Copyright (C) 2015 Felix Rohrbach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "basejob.h" + +#include "connectiondata.h" + +namespace QMatrixClient +{ + class SetRoomStateJob: public BaseJob + { + public: + /** + * Constructs a job that sets a state using an arbitrary room event + * with a state key. + */ + template + SetRoomStateJob(const ConnectionData* connection, const QString& roomId, + const EvT* event, const QString& stateKey) + : BaseJob(connection, HttpVerb::Put, "SetRoomStateJob", + QStringLiteral("_matrix/client/r0/rooms/%1/state/%2/%3") + .arg(roomId, EvT::TypeId, stateKey), + Query(), + Data(event->toJson())) + { } + /** + * Constructs a job that sets a state using an arbitrary room event + * without a state key. + */ + template + SetRoomStateJob(const ConnectionData* connection, const QString& roomId, + const EvT* event) + : BaseJob(connection, HttpVerb::Put, "SetRoomStateJob", + QStringLiteral("_matrix/client/r0/rooms/%1/state/%2") + .arg(roomId, EvT::TypeId), + Query(), + Data(event->toJson())) + { } + + QString eventId() const { return _eventId; } + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + QString _eventId; + }; +} // namespace QMatrixClient diff --git a/room.cpp b/room.cpp index ae8c7e63..2ba3766a 100644 --- a/room.cpp +++ b/room.cpp @@ -26,6 +26,7 @@ #include #include // for efficient string concats (operator%) #include +#include #include "connection.h" #include "state.h" @@ -595,6 +596,12 @@ void Room::postMessage(RoomMessageEvent* event) connection()->callApi(id(), event); } +void Room::setTopic(const QString& newTopic) +{ + RoomTopicEvent evt(newTopic); + connection()->callApi(id(), &evt); +} + void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); @@ -1032,4 +1039,3 @@ bool MemberSorter::operator()(User *u1, User *u2) const n2.remove(0, 1); return n1.localeAwareCompare(n2) < 0; } - diff --git a/room.h b/room.h index 5ea89418..393dced3 100644 --- a/room.h +++ b/room.h @@ -153,6 +153,7 @@ namespace QMatrixClient void postMessage(RoomMessageEvent* event); /** @deprecated */ void postMessage(const QString& type, const QString& plainText); + void setTopic(const QString& newTopic); void getPreviousContent(int limit = 10); -- cgit v1.2.3 From 15feb65dbb17161ccfbda614e0635f2698beacd7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 22 Sep 2017 14:04:26 +0900 Subject: BaseJob: Log the sent request more nicely --- jobs/basejob.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 6b2ebc58..ea1a7158 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -159,8 +159,9 @@ 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(); + qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint; + if (!d->requestQuery.isEmpty()) + qCDebug(d->logCat) << " query:" << 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 ae59271da3a199eb936aa709893ef592cd51f172 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 22 Sep 2017 14:17:31 +0900 Subject: Use epoch milliseconds instead of std::rand() to seed the txn counter std::rand() on MinGW on Windows 7, at least in debug mode, nicely generates the same value across runs, reliably leading to messages loss as the server discards them. --- connectiondata.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connectiondata.cpp b/connectiondata.cpp index cd91ef27..6f15577e 100644 --- a/connectiondata.cpp +++ b/connectiondata.cpp @@ -21,7 +21,6 @@ #include "logging.h" #include -#include using namespace QMatrixClient; @@ -38,7 +37,7 @@ struct ConnectionData::Private QString lastEvent; mutable unsigned int txnCounter = 0; - const int id = std::rand(); // We don't really care about pure randomness + const qint64 id = QDateTime::currentMSecsSinceEpoch(); }; ConnectionData::ConnectionData(QUrl baseUrl) -- cgit v1.2.3