aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/avatar.cpp12
-rw-r--r--lib/connection.cpp133
-rw-r--r--lib/connection.h18
-rw-r--r--lib/converters.h2
-rw-r--r--lib/events/event.cpp5
-rw-r--r--lib/events/event.h12
-rw-r--r--lib/events/eventloader.h1
-rw-r--r--lib/events/simplestateevents.h14
-rw-r--r--lib/events/stateevent.cpp15
-rw-r--r--lib/events/stateevent.h20
-rw-r--r--lib/jobs/basejob.cpp19
-rw-r--r--lib/jobs/basejob.h14
-rw-r--r--lib/jobs/mediathumbnailjob.cpp8
-rw-r--r--lib/jobs/syncjob.cpp112
-rw-r--r--lib/jobs/syncjob.h48
-rw-r--r--lib/logging.h11
-rw-r--r--lib/room.cpp498
-rw-r--r--lib/room.h67
-rw-r--r--lib/syncdata.cpp180
-rw-r--r--lib/syncdata.h86
-rw-r--r--lib/user.cpp15
-rw-r--r--lib/user.h2
-rw-r--r--lib/util.cpp14
-rw-r--r--lib/util.h6
24 files changed, 804 insertions, 508 deletions
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
index b8e1096d..c0ef3cba 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -190,18 +190,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const
return _imageSource != Banned;
}
-QString cacheLocation() {
- const auto cachePath =
- QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
- + "/avatar/";
- QDir dir;
- if (!dir.exists(cachePath))
- dir.mkpath(cachePath);
- return cachePath;
-}
-
QString Avatar::Private::localFile() const {
- static const auto cachePath = cacheLocation();
+ static const auto cachePath = cacheLocation("avatars");
return cachePath % _url.authority() % '_' % _url.fileName() % ".png";
}
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 3d635a7e..9372acd5 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -39,7 +39,6 @@
#include <QtNetwork/QDnsLookup>
#include <QtCore/QFile>
#include <QtCore/QDir>
-#include <QtCore/QFileInfo>
#include <QtCore/QStandardPaths>
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
@@ -65,10 +64,6 @@ HashT erase_if(HashT& hashMap, Pred pred)
return removals;
}
-#ifndef TRIM_RAW_DATA
-#define TRIM_RAW_DATA 65535
-#endif
-
class Connection::Private
{
public:
@@ -228,8 +223,7 @@ void Connection::doConnectToServer(const QString& user, const QString& password,
});
connect(loginJob, &BaseJob::failure, this,
[this, loginJob] {
- emit loginError(loginJob->errorString(),
- loginJob->rawData(TRIM_RAW_DATA));
+ emit loginError(loginJob->errorString(), loginJob->rawDataSample());
});
}
@@ -306,7 +300,7 @@ void Connection::sync(int timeout)
connect( job, &SyncJob::retryScheduled, this,
[this,job] (int retriesTaken, int nextInMilliseconds)
{
- emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA),
+ emit networkError(job->errorString(), job->rawDataSample(),
retriesTaken, nextInMilliseconds);
});
connect( job, &SyncJob::failure, this, [this, job] {
@@ -315,14 +309,14 @@ void Connection::sync(int timeout)
{
qCWarning(SYNCJOB)
<< "Sync job failed with ContentAccessError - login expired?";
- emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA));
+ emit loginError(job->errorString(), job->rawDataSample());
}
else
- emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA));
+ emit syncError(job->errorString(), job->rawDataSample());
});
}
-void Connection::onSyncSuccess(SyncData &&data) {
+void Connection::onSyncSuccess(SyncData &&data, bool fromCache) {
d->data->setLastEvent(data.nextBatch());
for (auto&& roomData: data.takeRoomData())
{
@@ -343,7 +337,7 @@ void Connection::onSyncSuccess(SyncData &&data) {
}
if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
{
- r->updateData(std::move(roomData));
+ r->updateData(std::move(roomData), fromCache);
if (d->firstTimeRooms.removeOne(r))
emit loadedRoomState(r);
}
@@ -422,9 +416,10 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
}
-JoinRoomJob* Connection::joinRoom(const QString& roomAlias)
+JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
+ const QStringList& serverNames)
{
- auto job = callApi<JoinRoomJob>(roomAlias);
+ auto job = callApi<JoinRoomJob>(roomAlias, serverNames);
connect(job, &JoinRoomJob::success,
this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
return job;
@@ -1063,47 +1058,54 @@ void Connection::setHomeserver(const QUrl& url)
emit homeserverChanged(homeserver());
}
-static constexpr int CACHE_VERSION_MAJOR = 8;
-static constexpr int CACHE_VERSION_MINOR = 0;
+void Connection::saveRoomState(Room* r) const
+{
+ Q_ASSERT(r);
+ if (!d->cacheState)
+ return;
+
+ QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) };
+ if (outRoomFile.open(QFile::WriteOnly))
+ {
+ QJsonDocument json { r->toJson() };
+ auto data = d->cacheToBinary ? json.toBinaryData()
+ : json.toJson(QJsonDocument::Compact);
+ outRoomFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName();
+ } else {
+ qCWarning(MAIN) << "Error opening" << outRoomFile.fileName()
+ << ":" << outRoomFile.errorString();
+ }
+}
-void Connection::saveState(const QUrl &toFile) const
+void Connection::saveState() const
{
if (!d->cacheState)
return;
QElapsedTimer et; et.start();
- QFileInfo stateFile {
- toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile()
- };
- if (!stateFile.dir().exists())
- stateFile.dir().mkpath(".");
-
- QFile outfile { stateFile.absoluteFilePath() };
- if (!outfile.open(QFile::WriteOnly))
+ QFile outFile { stateCachePath() % "state.json" };
+ if (!outFile.open(QFile::WriteOnly))
{
- qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath()
- << ":" << outfile.errorString();
+ qCWarning(MAIN) << "Error opening" << outFile.fileName()
+ << ":" << outFile.errorString();
qCWarning(MAIN) << "Caching the rooms state disabled";
d->cacheState = false;
return;
}
- QJsonObject rootObj;
+ QJsonObject rootObj {
+ { QStringLiteral("cache_version"), QJsonObject {
+ { QStringLiteral("major"), SyncData::cacheVersion().first },
+ { QStringLiteral("minor"), SyncData::cacheVersion().second }
+ }}};
{
QJsonObject rooms;
QJsonObject inviteRooms;
for (const auto* i : roomMap()) // Pass on rooms in Leave state
- {
- if (i->joinState() == JoinState::Invite)
- inviteRooms.insert(i->id(), i->toJson());
- else
- rooms.insert(i->id(), i->toJson());
- QElapsedTimer et1; et1.start();
- QCoreApplication::processEvents();
- if (et1.elapsed() > 1)
- qCDebug(PROFILER) << "processEvents() borrowed" << et1;
- }
+ (i->joinState() == JoinState::Invite ? inviteRooms : rooms)
+ .insert(i->id(), QJsonValue::Null);
QJsonObject roomObj;
if (!rooms.isEmpty())
@@ -1126,63 +1128,35 @@ void Connection::saveState(const QUrl &toFile) const
QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
- QJsonObject versionObj;
- versionObj.insert("major", CACHE_VERSION_MAJOR);
- versionObj.insert("minor", CACHE_VERSION_MINOR);
- rootObj.insert("cache_version", versionObj);
-
QJsonDocument json { rootObj };
auto data = d->cacheToBinary ? json.toBinaryData() :
json.toJson(QJsonDocument::Compact);
qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
- outfile.write(data.data(), data.size());
- qCDebug(MAIN) << "State cache saved to" << outfile.fileName();
+ outFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "State cache saved to" << outFile.fileName();
}
-void Connection::loadState(const QUrl &fromFile)
+void Connection::loadState()
{
if (!d->cacheState)
return;
QElapsedTimer et; et.start();
- QFile file {
- fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile()
- };
- if (!file.exists())
- {
- qCDebug(MAIN) << "No state cache file found";
- return;
- }
- if(!file.open(QFile::ReadOnly))
- {
- qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read";
- return;
- }
- QByteArray data = file.readAll();
- auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) :
- QJsonDocument::fromJson(data);
- if (jsonDoc.isNull())
- {
- qCWarning(MAIN) << "Cache file broken, discarding";
+ SyncData sync { stateCachePath() % "state.json" };
+ if (sync.nextBatch().isEmpty()) // No token means no cache by definition
return;
- }
- auto actualCacheVersionMajor =
- jsonDoc.object()
- .value("cache_version").toObject()
- .value("major").toInt();
- if (actualCacheVersionMajor < CACHE_VERSION_MAJOR)
+
+ if (!sync.unresolvedRooms().isEmpty())
{
- qCWarning(MAIN)
- << "Major version of the cache file is" << actualCacheVersionMajor
- << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache";
+ qCWarning(MAIN) << "State cache incomplete, discarding";
return;
}
-
- SyncData sync;
- sync.parseJson(jsonDoc);
- onSyncSuccess(std::move(sync));
+ // TODO: to handle load failures, instead of the above block:
+ // 1. Do initial sync on failed rooms without saving the nextBatch token
+ // 2. Do the sync across all rooms as normal
+ onSyncSuccess(std::move(sync), true);
qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et;
}
@@ -1190,8 +1164,7 @@ QString Connection::stateCachePath() const
{
auto safeUserId = userId();
safeUserId.replace(':', '_');
- return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
- % '/' % safeUserId % "_state.json";
+ return cacheLocation(safeUserId);
}
bool Connection::cacheState() const
diff --git a/lib/connection.h b/lib/connection.h
index cfad8774..32533b6e 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -280,7 +280,7 @@ namespace QMatrixClient
* to be QML-friendly. Empty parameter means using a path
* defined by stateCachePath().
*/
- Q_INVOKABLE void loadState(const QUrl &fromFile = {});
+ Q_INVOKABLE void loadState();
/**
* 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
@@ -290,7 +290,10 @@ namespace QMatrixClient
* QML-friendly. Empty parameter means using a path defined by
* stateCachePath().
*/
- Q_INVOKABLE void saveState(const QUrl &toFile = {}) const;
+ Q_INVOKABLE void saveState() const;
+
+ /// This method saves the current state of a single room.
+ void saveRoomState(Room* r) const;
/**
* The default path to store the cached room state, defined as
@@ -461,7 +464,8 @@ namespace QMatrixClient
CreateRoomJob* createDirectChat(const QString& userId,
const QString& topic = {}, const QString& name = {});
- virtual JoinRoomJob* joinRoom(const QString& roomAlias);
+ virtual JoinRoomJob* joinRoom(const QString& roomAlias,
+ const QStringList& serverNames = {});
/** Sends /forget to the server and also deletes room locally.
* This method is in Connection, not in Room, since it's a
@@ -518,7 +522,7 @@ namespace QMatrixClient
* a successful login and logout and are constant at other times.
*/
void stateChanged();
- void loginError(QString message, QByteArray details);
+ void loginError(QString message, QString details);
/** A network request (job) failed
*
@@ -536,11 +540,11 @@ namespace QMatrixClient
* @param retriesTaken - how many retries have already been taken
* @param nextRetryInMilliseconds - when the job will retry again
*/
- void networkError(QString message, QByteArray details,
+ void networkError(QString message, QString details,
int retriesTaken, int nextRetryInMilliseconds);
void syncDone();
- void syncError(QString message, QByteArray details);
+ void syncError(QString message, QString details);
void newUser(User* user);
@@ -673,7 +677,7 @@ namespace QMatrixClient
/**
* Completes loading sync data.
*/
- void onSyncSuccess(SyncData &&data);
+ void onSyncSuccess(SyncData &&data, bool fromCache = false);
private:
class Private;
diff --git a/lib/converters.h b/lib/converters.h
index 70938ab9..53855a1f 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -61,7 +61,7 @@ namespace QMatrixClient
inline auto toJson(const QJsonValue& val) { return val; }
inline auto toJson(const QJsonObject& o) { return o; }
inline auto toJson(const QJsonArray& arr) { return arr; }
- // Special-case QStrings and bools to avoid ambiguity between QJsonValue
+ // Special-case QString to avoid ambiguity between QJsonValue
// and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below)
inline auto toJson(const QString& s) { return s; }
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index fd6e3939..c98dfbb6 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -77,3 +77,8 @@ const QJsonObject Event::unsignedJson() const
{
return fullJson()[UnsignedKeyL].toObject();
}
+
+void Event::dumpTo(QDebug dbg) const
+{
+ dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact);
+}
diff --git a/lib/events/event.h b/lib/events/event.h
index 5b33628f..c51afcc4 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -209,7 +209,7 @@ namespace QMatrixClient
inline auto registerEventType()
{
static const auto _ = setupFactory<EventT>();
- return _;
+ return _; // Only to facilitate usage in static initialisation
}
// === Event ===
@@ -257,8 +257,18 @@ namespace QMatrixClient
return fromJson<T>(contentJson()[key]);
}
+ friend QDebug operator<<(QDebug dbg, const Event& e)
+ {
+ QDebugStateSaver _dss { dbg };
+ dbg.noquote().nospace()
+ << e.matrixType() << '(' << e.type() << "): ";
+ e.dumpTo(dbg);
+ return dbg;
+ }
+
virtual bool isStateEvent() const { return false; }
virtual bool isCallEvent() const { return false; }
+ virtual void dumpTo(QDebug dbg) const;
protected:
QJsonObject& editJson() { return _json; }
diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h
index 3ee9a181..cd2f9149 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -19,7 +19,6 @@
#pragma once
#include "stateevent.h"
-#include "converters.h"
namespace QMatrixClient {
namespace _impl {
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index 56be947c..5aa24c15 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -59,21 +59,23 @@ namespace QMatrixClient
};
} // namespace EventContent
-#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \
- class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \
+#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \
+ class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \
{ \
public: \
- using content_type = _ContentType; \
+ using value_type = content_type::value_type; \
DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(const QJsonObject& obj) \
- : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \
- { } \
+ explicit _Name() : _Name(value_type()) { } \
template <typename T> \
explicit _Name(T&& value) \
: StateEvent(typeId(), matrixTypeId(), \
QStringLiteral(#_ContentKey), \
std::forward<T>(value)) \
{ } \
+ explicit _Name(QJsonObject obj) \
+ : StateEvent(typeId(), std::move(obj), \
+ QStringLiteral(#_ContentKey)) \
+ { } \
auto _ContentKey() const { return content().value; } \
}; \
REGISTER_EVENT_TYPE(_Name) \
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index fd5d2642..fd8079be 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -28,3 +28,18 @@ bool StateEventBase::repeatsState() const
const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
return fullJson().value(ContentKeyL) == prevContentJson;
}
+
+QString StateEventBase::replacedState() const
+{
+ return unsignedJson().value("replaces_state"_ls).toString();
+}
+
+void StateEventBase::dumpTo(QDebug dbg) const
+{
+ if (!stateKey().isEmpty())
+ dbg << '<' << stateKey() << "> ";
+ if (unsignedJson().contains(PrevContentKeyL))
+ dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject())
+ .toJson(QJsonDocument::Compact) << " -> ";
+ RoomEvent::dumpTo(dbg);
+}
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 6032132e..d4a7e8b3 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -30,11 +30,21 @@ namespace QMatrixClient {
~StateEventBase() override = default;
bool isStateEvent() const override { return true; }
+ QString replacedState() const;
+ void dumpTo(QDebug dbg) const override;
+
virtual bool repeatsState() const;
};
using StateEventPtr = event_ptr_tt<StateEventBase>;
using StateEvents = EventsArray<StateEventBase>;
+ /**
+ * A combination of event type and state key uniquely identifies a piece
+ * of state in Matrix.
+ * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events
+ */
+ using StateEventKey = std::pair<QString, QString>;
+
template <typename ContentT>
struct Prev
{
@@ -90,3 +100,13 @@ namespace QMatrixClient {
std::unique_ptr<Prev<ContentT>> _prev;
};
} // namespace QMatrixClient
+
+namespace std {
+ template <> struct hash<QMatrixClient::StateEventKey>
+ {
+ size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT
+ {
+ return qHash(k);
+ }
+ };
+}
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index b21173ae..4a7780b1 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -426,8 +426,8 @@ BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
if( error.error == QJsonParseError::NoError )
return parseJson(json);
- else
- return { IncorrectResponseError, error.errorString() };
+
+ return { IncorrectResponseError, error.errorString() };
}
BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
@@ -519,8 +519,19 @@ BaseJob::Status BaseJob::status() const
QByteArray BaseJob::rawData(int bytesAtMost) const
{
- return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ?
- d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse;
+ return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost
+ ? d->rawResponse.left(bytesAtMost) : d->rawResponse;
+}
+
+QString BaseJob::rawDataSample(int bytesAtMost) const
+{
+ auto data = rawData(bytesAtMost);
+ Q_ASSERT(data.size() <= d->rawResponse.size());
+ return data.size() == d->rawResponse.size()
+ ? data : data + tr("...(truncated, %Ln bytes in total)",
+ "Comes after trimmed raw network response",
+ d->rawResponse.size());
+
}
QString BaseJob::statusCaption() const
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 4ef25ab8..3d50344d 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -138,8 +138,20 @@ namespace QMatrixClient
Status status() const;
/** Short human-friendly message on the job status */
QString statusCaption() const;
- /** Raw response body as received from the server */
+ /** Get raw response body as received from the server
+ * \param bytesAtMost return this number of leftmost bytes, or -1
+ * to return the entire response
+ */
QByteArray rawData(int bytesAtMost = -1) const;
+ /** Get UI-friendly sample of raw data
+ *
+ * This is almost the same as rawData but appends the "truncated"
+ * suffix if not all data fit in bytesAtMost. This call is
+ * recommended to present a sample of raw data as "details" next to
+ * error messages. Note that the default \p bytesAtMost value is
+ * also tailored to UI cases.
+ */
+ QString rawDataSample(int bytesAtMost = 65535) const;
/** Error (more generally, status) code
* Equivalent to status().code
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index 8dfb6094..aeb49839 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -23,7 +23,8 @@ using namespace QMatrixClient;
QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl,
const QUrl& mxcUri, QSize requestedSize)
{
- return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1),
+ return makeRequestUrl(std::move(baseUrl),
+ mxcUri.authority(), mxcUri.path().mid(1),
requestedSize.width(), requestedSize.height());
}
@@ -34,9 +35,8 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName,
{ }
MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize)
- : GetContentThumbnailJob(mxcUri.authority(),
- mxcUri.path().mid(1), // sans leading '/'
- requestedSize.width(), requestedSize.height())
+ : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/'
+ requestedSize)
{ }
QImage MediaThumbnailJob::thumbnail() const
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
index 9cbac71b..ac0f6685 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -18,10 +18,6 @@
#include "syncjob.h"
-#include "events/eventloader.h"
-
-#include <QtCore/QElapsedTimer>
-
using namespace QMatrixClient;
static size_t jobId = 0;
@@ -46,110 +42,14 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
setMaxRetries(std::numeric_limits<int>::max());
}
-QString SyncData::nextBatch() const
-{
- return nextBatch_;
-}
-
-SyncDataList&& SyncData::takeRoomData()
-{
- return std::move(roomData);
-}
-
-Events&& SyncData::takePresenceData()
-{
- return std::move(presenceData);
-}
-
-Events&& SyncData::takeAccountData()
-{
- return std::move(accountData);
-}
-
-Events&&SyncData::takeToDeviceEvents()
-{
- return std::move(toDeviceEvents);
-}
-
-template <typename EventsArrayT, typename StrT>
-inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
-{
- return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
-}
-
BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
{
- return d.parseJson(data);
-}
-
-BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
-{
- QElapsedTimer et; et.start();
-
- auto json = data.object();
- nextBatch_ = json.value("next_batch"_ls).toString();
- presenceData = load<Events>(json, "presence"_ls);
- accountData = load<Events>(json, "account_data"_ls);
- toDeviceEvents = load<Events>(json, "to_device"_ls);
+ d.parseJson(data.object());
+ if (d.unresolvedRooms().isEmpty())
+ return BaseJob::Success;
- auto rooms = json.value("rooms"_ls).toObject();
- JoinStates::Int ii = 1; // ii is used to make a JoinState value
- auto totalRooms = 0;
- auto totalEvents = 0;
- for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
- {
- const auto rs = rooms.value(JoinStateStrings[i]).toObject();
- // We have a Qt container on the right and an STL one on the left
- roomData.reserve(static_cast<size_t>(rs.size()));
- for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
- {
- roomData.emplace_back(roomIt.key(), JoinState(ii),
- roomIt.value().toObject());
- const auto& r = roomData.back();
- totalEvents += r.state.size() + r.ephemeral.size() +
- r.accountData.size() + r.timeline.size();
- }
- totalRooms += roomData.size();
- }
- qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
- << totalRooms << "room(s),"
- << totalEvents << "event(s) in" << et;
- return BaseJob::Success;
+ qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:"
+ << d.unresolvedRooms().join(',');
+ return BaseJob::IncorrectResponseError;
}
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-qmatrixclient.unread_count");
-
-SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
- const QJsonObject& room_)
- : roomId(roomId_)
- , joinState(joinState_)
- , state(load<StateEvents>(room_,
- joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls))
-{
- switch (joinState) {
- case JoinState::Join:
- ephemeral = load<Events>(room_, "ephemeral"_ls);
- FALLTHROUGH;
- case JoinState::Leave:
- {
- accountData = load<Events>(room_, "account_data"_ls);
- timeline = load<RoomEvents>(room_, "timeline"_ls);
- const auto timelineJson = room_.value("timeline"_ls).toObject();
- timelineLimited = timelineJson.value("limited"_ls).toBool();
- timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
-
- break;
- }
- default: /* nothing on top of state */;
- }
-
- const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
- unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
- highlightCount = unreadJson.value("highlight_count"_ls).toInt();
- notificationCount = unreadJson.value("notification_count"_ls).toInt();
- if (highlightCount > 0 || notificationCount > 0)
- qCDebug(SYNCJOB) << "Room" << roomId_
- << "has highlights:" << highlightCount
- << "and notifications:" << notificationCount;
-}
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
index 6b9bedfa..a0a3c026 100644
--- a/lib/jobs/syncjob.h
+++ b/lib/jobs/syncjob.h
@@ -20,56 +20,10 @@
#include "basejob.h"
-#include "joinstate.h"
-#include "events/stateevent.h"
-#include "util.h"
+#include "../syncdata.h"
namespace QMatrixClient
{
- class SyncRoomData
- {
- public:
- QString roomId;
- JoinState joinState;
- StateEvents state;
- RoomEvents timeline;
- Events ephemeral;
- Events accountData;
-
- bool timelineLimited;
- QString timelinePrevBatch;
- int unreadCount;
- int highlightCount;
- int notificationCount;
-
- SyncRoomData(const QString& roomId, JoinState joinState_,
- const QJsonObject& room_);
- SyncRoomData(SyncRoomData&&) = default;
- SyncRoomData& operator=(SyncRoomData&&) = default;
-
- static const QString UnreadCountKey;
- };
- // QVector cannot work with non-copiable objects, std::vector can.
- using SyncDataList = std::vector<SyncRoomData>;
-
- class SyncData
- {
- public:
- BaseJob::Status parseJson(const QJsonDocument &data);
- Events&& takePresenceData();
- Events&& takeAccountData();
- Events&& takeToDeviceEvents();
- SyncDataList&& takeRoomData();
- QString nextBatch() const;
-
- private:
- QString nextBatch_;
- Events presenceData;
- Events accountData;
- Events toDeviceEvents;
- SyncDataList roomData;
- };
-
class SyncJob: public BaseJob
{
public:
diff --git a/lib/logging.h b/lib/logging.h
index 8dbfdf30..a3a65887 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -65,6 +65,17 @@ namespace QMatrixClient
{
return qdm(debug_object);
}
+
+ inline qint64 profilerMinNsecs()
+ {
+ return
+#ifdef PROFILER_LOG_USECS
+ PROFILER_LOG_USECS
+#else
+ 200
+#endif
+ * 1000;
+ }
}
inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et)
diff --git a/lib/room.cpp b/lib/room.cpp
index 7c45bf89..8b81bfb2 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -25,7 +25,6 @@
#include "csapi/receipts.h"
#include "csapi/redaction.h"
#include "csapi/account-data.h"
-#include "csapi/message_pagination.h"
#include "csapi/room_state.h"
#include "csapi/room_send.h"
#include "csapi/tags.h"
@@ -46,10 +45,10 @@
#include "connection.h"
#include "user.h"
#include "converters.h"
+#include "syncdata.h"
#include <QtCore/QHash>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
-#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
#include <QtCore/QDir>
#include <QtCore/QTemporaryFile>
@@ -92,18 +91,19 @@ class Room::Private
void updateDisplayname();
Connection* connection;
+ QString id;
+ JoinState joinState;
+ /// The state of the room at timeline position before-0
+ /// \sa timelineBase
+ std::unordered_map<StateEventKey, StateEventPtr> baseState;
+ /// The state of the room at timeline position after-maxTimelineIndex()
+ /// \sa Room::syncEdge
+ QHash<StateEventKey, const StateEventBase*> currentState;
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
- QString id;
- QStringList aliases;
- QString canonicalAlias;
- QString name;
QString displayname;
- QString topic;
- QString encryptionAlgorithm;
Avatar avatar;
- JoinState joinState;
int highlightCount = 0;
int notificationCount = 0;
members_map_t membersMap;
@@ -157,8 +157,8 @@ class Room::Private
fileTransfers[tid].status = FileTransferInfo::Failed;
emit q->fileTransferFailed(tid, errorMessage);
}
- // A map from event/txn ids to information about the long operation;
- // used for both download and upload operations
+ /// A map from event/txn ids to information about the long operation;
+ /// used for both download and upload operations
QHash<QString, FileTransferPrivateInfo> fileTransfers;
const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
@@ -169,8 +169,22 @@ class Room::Private
void renameMember(User* u, QString oldName);
void removeMemberFromMap(const QString& username, User* u);
+ /// A point in the timeline corresponding to baseState
+ rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
+
void getPreviousContent(int limit = 10);
+ template <typename EventT>
+ const EventT* getCurrentState(QString stateKey = {}) const
+ {
+ static const EventT empty;
+ const auto* evt =
+ currentState.value({EventT::matrixTypeId(), stateKey}, &empty);
+ Q_ASSERT(evt->type() == EventT::typeId() &&
+ evt->matrixType() == EventT::matrixTypeId());
+ return static_cast<const EventT*>(evt);
+ }
+
bool isEventNotable(const TimelineItem& ti) const
{
return !ti->isRedacted() &&
@@ -178,7 +192,7 @@ class Room::Private
is<RoomMessageEvent>(*ti);
}
- void addNewMessageEvents(RoomEvents&& events);
+ Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
/** Move events into the timeline
@@ -215,6 +229,8 @@ class Room::Private
QString doSendEvent(const RoomEvent* pEvent);
PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr);
+ void onEventSendingFailure(const RoomEvent* pEvent,
+ const QString& txnId, BaseJob* call = nullptr);
template <typename EvT>
auto requestSetState(const QString& stateKey, const EvT& event)
@@ -288,17 +304,17 @@ const Room::PendingEvents& Room::pendingEvents() const
QString Room::name() const
{
- return d->name;
+ return d->getCurrentState<RoomNameEvent>()->name();
}
QStringList Room::aliases() const
{
- return d->aliases;
+ return d->getCurrentState<RoomAliasesEvent>()->aliases();
}
QString Room::canonicalAlias() const
{
- return d->canonicalAlias;
+ return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
}
QString Room::displayName() const
@@ -308,7 +324,7 @@ QString Room::displayName() const
QString Room::topic() const
{
- return d->topic;
+ return d->getCurrentState<RoomTopicEvent>()->topic();
}
QString Room::avatarMediaId() const
@@ -366,6 +382,7 @@ void Room::setJoinState(JoinState state)
d->joinState = state;
qCDebug(MAIN) << "Room" << id() << "changed state: "
<< int(oldState) << "->" << int(state);
+ emit changed(Change::JoinStateChange);
emit joinStateChanged(oldState, state);
}
@@ -384,6 +401,7 @@ void Room::Private::setLastReadEvent(User* u, QString eventId)
if (storedId != serverReadMarker)
connection->callApi<PostReadMarkersJob>(id, storedId);
emit q->readMarkerMoved(eventId, storedId);
+ connection->saveRoomState(q);
}
}
@@ -408,7 +426,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
QElapsedTimer et; et.start();
const auto newUnreadMessages = count_if(from, to,
std::bind(&Room::Private::isEventNotable, this, _1));
- if (et.nsecsElapsed() > 10000)
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Counting gained unread messages took" << et;
if(newUnreadMessages > 0)
@@ -418,7 +436,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
unreadMessages = 0;
unreadMessages += newUnreadMessages;
- qCDebug(MAIN) << "Room" << displayname << "has gained"
+ qCDebug(MAIN) << "Room" << q->objectName() << "has gained"
<< newUnreadMessages << "unread message(s),"
<< (q->readMarker() == timeline.crend() ?
"in total at least" : "in total")
@@ -450,7 +468,7 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
QElapsedTimer et; et.start();
unreadMessages = count_if(eagerMarker, timeline.cend(),
std::bind(&Room::Private::isEventNotable, this, _1));
- if (et.nsecsElapsed() > 10000)
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
// See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
@@ -513,11 +531,21 @@ int Room::unreadCount() const
return d->unreadMessages;
}
-Room::rev_iter_t Room::timelineEdge() const
+Room::rev_iter_t Room::historyEdge() const
{
return d->timeline.crend();
}
+Room::Timeline::const_iterator Room::syncEdge() const
+{
+ return d->timeline.cend();
+}
+
+Room::rev_iter_t Room::timelineEdge() const
+{
+ return historyEdge();
+}
+
TimelineItem::index_t Room::minTimelineIndex() const
{
return d->timeline.empty() ? 0 : d->timeline.front().index();
@@ -945,7 +973,12 @@ int Room::timelineSize() const
bool Room::usesEncryption() const
{
- return !d->encryptionAlgorithm.isEmpty();
+ return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
+}
+
+GetRoomEventsJob* Room::eventsHistoryJob() const
+{
+ return d->eventsHistoryJob;
}
void Room::Private::insertMemberIntoMap(User *u)
@@ -1006,8 +1039,9 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
{
Q_ASSERT(!events.empty());
// Historical messages arrive in newest-to-oldest order, so the process for
- // them is symmetric to the one for new messages.
- auto index = timeline.empty() ? -int(placement) :
+ // them is almost symmetric to the one for new messages. New messages get
+ // appended from index 0; old messages go backwards from index -1.
+ auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ :
placement == Older ? timeline.front().index() :
timeline.back().index();
auto baseIndex = index;
@@ -1028,7 +1062,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
eventsIndex.insert(eId, index);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
- const auto insertedSize = (index - baseIndex) * int(placement);
+ const auto insertedSize = (index - baseIndex) * placement;
Q_ASSERT(insertedSize == int(events.size()));
return insertedSize;
}
@@ -1074,45 +1108,48 @@ QString Room::roomMembername(const QString& userId) const
return roomMembername(user(userId));
}
-void Room::updateData(SyncRoomData&& data)
+void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if( d->prevBatch.isEmpty() )
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
+ Changes roomChanges = Change::NoChange;
QElapsedTimer et; et.start();
for (auto&& event: data.accountData)
- processAccountDataEvent(move(event));
+ roomChanges |= processAccountDataEvent(move(event));
- bool emitNamesChanged = false;
if (!data.state.empty())
{
et.restart();
- for (const auto& e: data.state)
- emitNamesChanged |= processStateEvent(*e);
+ for (auto&& eptr: data.state)
+ {
+ const auto& evt = *eptr;
+ Q_ASSERT(evt.isStateEvent());
+ d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr);
+ roomChanges |= processStateEvent(evt);
+ }
- qCDebug(PROFILER) << "*** Room::processStateEvents():"
- << data.state.size() << "event(s)," << et;
+ if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** Room::processStateEvents():"
+ << data.state.size() << "event(s)," << et;
}
if (!data.timeline.empty())
{
et.restart();
- // State changes can arrive in a timeline event; so check those.
- for (const auto& e: data.timeline)
- emitNamesChanged |= processStateEvent(*e);
- qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):"
- << data.timeline.size() << "event(s)," << et;
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
+ if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** Room::addNewMessageEvents():"
+ << data.timeline.size() << "event(s)," << et;
}
- if (emitNamesChanged)
+ if (roomChanges&TopicChange)
+ emit topicChanged();
+
+ if (roomChanges&NameChange)
emit namesChanged(this);
+
d->updateDisplayname();
- if (!data.timeline.empty())
- {
- et.restart();
- d->addNewMessageEvents(move(data.timeline));
- qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et;
- }
for( auto&& ephemeralEvent: data.ephemeral )
processEphemeralEvent(move(ephemeralEvent));
@@ -1134,6 +1171,12 @@ void Room::updateData(SyncRoomData&& data)
d->notificationCount = data.notificationCount;
emit notificationCountChanged(this);
}
+ if (roomChanges != Change::NoChange)
+ {
+ emit changed(roomChanges);
+ if (!fromCache)
+ connection()->saveRoomState(this);
+ }
}
QString Room::Private::sendEvent(RoomEventPtr&& event)
@@ -1150,50 +1193,42 @@ QString Room::Private::sendEvent(RoomEventPtr&& event)
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
{
auto txnId = pEvent->transactionId();
- // TODO: Enqueue the job rather than immediately trigger it
- auto call = connection->callApi<SendMessageJob>(BackgroundRequest,
- id, pEvent->matrixType(), txnId, pEvent->contentJson());
- Room::connect(call, &BaseJob::started, q,
- [this,pEvent,txnId] {
- auto it = findAsPending(pEvent);
- if (it == unsyncedEvents.end())
- {
- qWarning(EVENTS) << "Pending event for transaction" << txnId
- << "not found - got synced so soon?";
- return;
- }
- it->setDeparted();
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
- });
- Room::connect(call, &BaseJob::failure, q,
- [this,pEvent,txnId,call] {
- auto it = findAsPending(pEvent);
- if (it == unsyncedEvents.end())
- {
- qCritical(EVENTS) << "Pending event for transaction" << txnId
- << "got lost without successful sending";
- return;
- }
- it->setSendingFailed(
- call->statusCaption() % ": " % call->errorString());
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
-
- });
- Room::connect(call, &BaseJob::success, q,
- [this,call,pEvent,txnId] {
- // Find an event by the pointer saved in the lambda (the pointer
- // may be dangling by now but we can still search by it).
- auto it = findAsPending(pEvent);
- if (it == unsyncedEvents.end())
- {
- qDebug(EVENTS) << "Pending event for transaction" << txnId
- << "already merged";
- return;
- }
+ // TODO, #133: Enqueue the job rather than immediately trigger it.
+ if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest,
+ id, pEvent->matrixType(), txnId, pEvent->contentJson()))
+ {
+ Room::connect(call, &BaseJob::started, q,
+ [this,pEvent,txnId] {
+ auto it = findAsPending(pEvent);
+ if (it == unsyncedEvents.end())
+ {
+ qWarning(EVENTS) << "Pending event for transaction" << txnId
+ << "not found - got synced so soon?";
+ return;
+ }
+ it->setDeparted();
+ emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ });
+ Room::connect(call, &BaseJob::failure, q,
+ std::bind(&Room::Private::onEventSendingFailure,
+ this, pEvent, txnId, call));
+ Room::connect(call, &BaseJob::success, q,
+ [this,call,pEvent,txnId] {
+ // Find an event by the pointer saved in the lambda (the pointer
+ // may be dangling by now but we can still search by it).
+ auto it = findAsPending(pEvent);
+ if (it == unsyncedEvents.end())
+ {
+ qDebug(EVENTS) << "Pending event for transaction" << txnId
+ << "already merged";
+ return;
+ }
- it->setReachedServer(call->eventId());
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
- });
+ it->setReachedServer(call->eventId());
+ emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ });
+ } else
+ onEventSendingFailure(pEvent, txnId);
return txnId;
}
@@ -1206,6 +1241,22 @@ Room::PendingEvents::iterator Room::Private::findAsPending(
return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp);
}
+void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
+ const QString& txnId, BaseJob* call)
+{
+ auto it = findAsPending(pEvent);
+ if (it == unsyncedEvents.end())
+ {
+ qCritical(EVENTS) << "Pending event for transaction" << txnId
+ << "could not be sent";
+ return;
+ }
+ it->setSendingFailed(call
+ ? call->statusCaption() % ": " % call->errorString()
+ : tr("The call could not be started"));
+ emit q->pendingEventChanged(it - unsyncedEvents.begin());
+}
+
QString Room::retryMessage(const QString& txnId)
{
auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
@@ -1285,15 +1336,15 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
if (le->type() != re->type())
return false;
- if (!le->id().isEmpty())
+ if (!re->id().isEmpty())
return le->id() == re->id();
- if (!le->transactionId().isEmpty())
+ if (!re->transactionId().isEmpty())
return le->transactionId() == re->transactionId();
// This one is not reliable (there can be two unsynced
// events with the same type, sender and state key) but
// it's the best we have for state events.
- if (le->isStateEvent())
+ if (re->isStateEvent())
return le->stateKey() == re->stateKey();
// Empty id and no state key, hmm... (shrug)
@@ -1349,10 +1400,13 @@ void Room::Private::getPreviousContent(int limit)
{
eventsHistoryJob =
connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
+ emit q->eventsHistoryJobChanged();
connect( eventsHistoryJob, &BaseJob::success, q, [=] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
+ connect( eventsHistoryJob, &QObject::destroyed,
+ q, &Room::eventsHistoryJobChanged);
}
}
@@ -1587,11 +1641,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
return true;
}
- // Make a new event from the redacted JSON, exchange events,
- // notify everyone and delete the old event
+ // Make a new event from the redacted JSON and put it in the timeline
+ // instead of the redacted one. oldEvent will be deleted on return.
auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
- q->onRedaction(*oldEvent, *ti.event());
qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id();
+ if (oldEvent->isStateEvent())
+ {
+ const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() };
+ Q_ASSERT(currentState.contains(evtKey));
+ if (currentState[evtKey] == oldEvent.get())
+ {
+ Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState
+ qCDebug(MAIN).nospace() << "Reverting state "
+ << oldEvent->matrixType() << "/" << oldEvent->stateKey();
+ // Retarget the current state to the newly made event.
+ if (q->processStateEvent(*ti))
+ emit q->namesChanged(q);
+ updateDisplayname();
+ }
+ }
+ q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
}
@@ -1613,48 +1682,49 @@ inline bool isRedaction(const RoomEventPtr& ep)
return is<RedactionEvent>(*ep);
}
-void Room::Private::addNewMessageEvents(RoomEvents&& events)
+Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
+ if (events.empty())
+ return Change::NoChange;
// Pre-process redactions so that events that get redacted in the same
// batch landed in the timeline already redacted.
- // XXX: The code below is written (and commented) so that it could be
- // quickly converted to not-saving redaction events in the timeline.
- // See #220 for details.
- auto newEnd = std::find_if(events.begin(), events.end(), isRedaction);
- // Either process the redaction, or shift the non-redaction event
- // overwriting redactions in a remove_if fashion.
- for(const auto& eptr: RoomEventsRange(newEnd, events.end()))
+ // NB: We have to store redaction events to the timeline too - see #220.
+ auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction);
+ for(const auto& eptr: RoomEventsRange(redactionIt, events.end()))
if (auto* r = eventCast<RedactionEvent>(eptr))
{
// Try to find the target in the timeline, then in the batch.
if (processRedaction(*r))
continue;
- auto targetIt = std::find_if(events.begin(), newEnd,
+ auto targetIt = std::find_if(events.begin(), redactionIt,
[id=r->redactedEvent()] (const RoomEventPtr& ep) {
return ep->id() == id;
});
- if (targetIt != newEnd)
+ if (targetIt != redactionIt)
*targetIt = makeRedacted(**targetIt, *r);
else
qCDebug(MAIN) << "Redaction" << r->id()
<< "ignored: target event" << r->redactedEvent()
<< "is not found";
- // If the target events comes later, it comes already redacted.
+ // If the target event comes later, it comes already redacted.
}
-// else // This should be uncommented once we stop adding redactions to the timeline
-// *newEnd++ = std::move(eptr);
- newEnd = events.end(); // This line should go if/when we stop adding redactions to the timeline
- if (events.begin() == newEnd)
- return;
+ // State changes arrive as a part of timeline; the current room state gets
+ // updated before merging events to the timeline because that's what
+ // clients historically expect. This may eventually change though if we
+ // postulate that the current state is only current between syncs but not
+ // within a sync.
+ Changes stateChanges = Change::NoChange;
+ for (const auto& eptr: events)
+ stateChanges |= q->processStateEvent(*eptr);
auto timelineSize = timeline.size();
auto totalInserted = 0;
- for (auto it = events.begin(); it != newEnd;)
+ for (auto it = events.begin(); it != events.end();)
{
- auto nextPendingPair = findFirstOf(it, newEnd,
+ auto nextPendingPair = findFirstOf(it, events.end(),
unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent);
auto nextPending = nextPendingPair.first;
@@ -1668,7 +1738,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
q->onAddNewTimelineEvents(firstInserted);
emit q->addedMessages(firstInserted->index(), timeline.back().index());
}
- if (nextPending == newEnd)
+ if (nextPending == events.end())
break;
it = nextPending + 1;
@@ -1696,7 +1766,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
if (totalInserted > 0)
{
qCDebug(MAIN)
- << "Room" << displayname << "received" << totalInserted
+ << "Room" << q->objectName() << "received" << totalInserted
<< "new events; the last event is now" << timeline.back();
// The first event in the just-added batch (referred to by `from`)
@@ -1717,21 +1787,34 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ return stateChanges;
}
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
{
+ QElapsedTimer et; et.start();
const auto timelineSize = timeline.size();
dropDuplicateEvents(events);
- RoomEventsRange normalEvents {
- events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction)
- };
- if (normalEvents.empty())
+ if (events.empty())
return;
- emit q->aboutToAddHistoricalMessages(normalEvents);
- const auto insertedSize = moveEventsToTimeline(normalEvents, Older);
+ // In case of lazy-loading new members may be loaded with historical
+ // messages. Also, the cache doesn't store events with empty content;
+ // so when such events show up in the timeline they should be properly
+ // incorporated.
+ for (const auto& eptr: events)
+ {
+ const auto& e = *eptr;
+ if (e.isStateEvent() &&
+ !currentState.contains({e.matrixType(), e.stateKey()}))
+ {
+ q->processStateEvent(e);
+ }
+ }
+
+ emit q->aboutToAddHistoricalMessages(events);
+ const auto insertedSize = moveEventsToTimeline(events, Older);
const auto from = timeline.crend() - insertedSize;
qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize
@@ -1743,51 +1826,46 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
updateUnreadCount(from, timeline.crend());
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
+ if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():"
+ << insertedSize << "event(s)," << et;
}
-bool Room::processStateEvent(const RoomEvent& e)
+Room::Changes Room::processStateEvent(const RoomEvent& e)
{
+ if (!e.isStateEvent())
+ return Change::NoChange;
+
+ d->currentState[{e.matrixType(),e.stateKey()}] =
+ static_cast<const StateEventBase*>(&e);
+ if (!is<RoomMemberEvent>(e))
+ qCDebug(EVENTS) << "Room state event:" << e;
+
return visit(e
- , [this] (const RoomNameEvent& evt) {
- d->name = evt.name();
- qCDebug(MAIN) << "Room name updated:" << d->name;
- return true;
+ , [] (const RoomNameEvent&) {
+ return NameChange;
}
- , [this] (const RoomAliasesEvent& evt) {
- d->aliases = evt.aliases();
- qCDebug(MAIN) << "Room aliases updated:" << d->aliases;
- return true;
+ , [] (const RoomAliasesEvent&) {
+ return OtherChange;
}
, [this] (const RoomCanonicalAliasEvent& evt) {
- d->canonicalAlias = evt.alias();
- if (!d->canonicalAlias.isEmpty())
- setObjectName(d->canonicalAlias);
- qCDebug(MAIN) << "Room canonical alias updated:"
- << d->canonicalAlias;
- return true;
+ setObjectName(evt.alias().isEmpty() ? d->id : evt.alias());
+ return CanonicalAliasChange;
}
- , [this] (const RoomTopicEvent& evt) {
- d->topic = evt.topic();
- qCDebug(MAIN) << "Room topic updated:" << d->topic;
- emit topicChanged();
- return false;
+ , [] (const RoomTopicEvent&) {
+ return TopicChange;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
- {
- qCDebug(MAIN) << "Room avatar URL updated:"
- << evt.url().toString();
emit avatarChanged();
- }
- return false;
+ return AvatarChange;
}
, [this] (const RoomMemberEvent& evt) {
auto* u = user(evt.userId());
u->processEvent(evt, this);
if (u == localUser() && memberJoinState(u) == JoinState::Invite
&& evt.isDirect())
- connection()->addToDirectChats(this,
- user(evt.senderId()));
+ connection()->addToDirectChats(this, user(evt.senderId()));
if( evt.membership() == MembershipType::Join )
{
@@ -1807,24 +1885,23 @@ bool Room::processStateEvent(const RoomEvent& e)
emit userAdded(u);
}
}
- else if( evt.membership() == MembershipType::Leave )
+ else if( evt.membership() != MembershipType::Join )
{
if (memberJoinState(u) == JoinState::Join)
{
+ if (evt.membership() == MembershipType::Invite)
+ qCWarning(MAIN) << "Invalid membership change:" << evt;
if (!d->membersLeft.contains(u))
d->membersLeft.append(u);
d->removeMemberFromMap(u->name(this), u);
emit userRemoved(u);
}
}
- return false;
+ return MembersChange;
}
- , [this] (const EncryptionEvent& evt) {
- d->encryptionAlgorithm = evt.algorithm();
- qCDebug(MAIN) << "Encryption switched on in room" << id()
- << "with algorithm" << d->encryptionAlgorithm;
- emit encryption();
- return false;
+ , [this] (const EncryptionEvent&) {
+ emit encryption(); // It can only be done once, so emit it here.
+ return EncryptionOn;
}
);
}
@@ -1841,15 +1918,17 @@ void Room::processEphemeralEvent(EventPtr&& event)
if (memberJoinState(u) == JoinState::Join)
d->usersTyping.append(u);
}
- if (!evt->users().isEmpty())
+ if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):"
<< evt->users().size() << "users," << et;
emit typingChanged();
}
if (auto* evt = eventCast<ReceiptEvent>(event))
{
+ int totalReceipts = 0;
for( const auto &p: qAsConst(evt->eventsWithReceipts()) )
{
+ totalReceipts += p.receipts.size();
{
if (p.receipts.size() == 1)
qCDebug(EPHEMERAL) << "Marking" << p.evtId
@@ -1888,14 +1967,15 @@ void Room::processEphemeralEvent(EventPtr&& event)
}
}
}
- if (!evt->eventsWithReceipts().isEmpty())
+ if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 ||
+ et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):"
<< evt->eventsWithReceipts().size()
- << "events with receipts," << et;
+ << "event(s) with" << totalReceipts << "receipt(s)," << et;
}
}
-void Room::processAccountDataEvent(EventPtr&& event)
+Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
if (auto* evt = eventCast<TagEvent>(event))
d->setTags(evt->tags());
@@ -1922,7 +2002,9 @@ void Room::processAccountDataEvent(EventPtr&& event)
qCDebug(MAIN) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
+ return Change::AccountDataChange;
}
+ return Change::NoChange;
}
QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const
@@ -1963,9 +2045,8 @@ QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) co
// iii. More users.
if (userlist.size() > 3)
- return tr("%1 and %L2 others")
- .arg(q->roomMembername(first_two[0]))
- .arg(userlist.size() - 3);
+ return tr("%1 and %Ln other(s)", "", userlist.size() - 3)
+ .arg(q->roomMembername(first_two[0]));
// userlist.size() < 2 - apparently, there's only current user in the room
return QString();
@@ -1977,27 +2058,29 @@ QString Room::Private::calculateDisplayname() const
// Numbers below refer to respective parts in the spec.
// 1. Name (from m.room.name)
- if (!name.isEmpty()) {
- return name;
+ auto dispName = q->name();
+ if (!dispName.isEmpty()) {
+ return dispName;
}
// 2. Canonical alias
- if (!canonicalAlias.isEmpty())
- return canonicalAlias;
+ dispName = q->canonicalAlias();
+ if (!dispName.isEmpty())
+ return dispName;
// Using m.room.aliases in naming is explicitly discouraged by the spec
- //if (!aliases.empty() && !aliases.at(0).isEmpty())
- // return aliases.at(0);
+ //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty())
+ // return q->aliases().at(0);
// 3. Room members
- QString topMemberNames = roomNameFromMemberNames(membersMap.values());
- if (!topMemberNames.isEmpty())
- return topMemberNames;
+ dispName = roomNameFromMemberNames(membersMap.values());
+ if (!dispName.isEmpty())
+ return dispName;
// 4. Users that previously left the room
- topMemberNames = roomNameFromMemberNames(membersLeft);
- if (!topMemberNames.isEmpty())
- return tr("Empty room (was: %1)").arg(topMemberNames);
+ dispName = roomNameFromMemberNames(membersLeft);
+ if (!dispName.isEmpty())
+ return tr("Empty room (was: %1)").arg(dispName);
// 5. Fail miserably
return tr("Empty room (%1)").arg(id);
@@ -2016,34 +2099,6 @@ void Room::Private::updateDisplayname()
}
}
-void appendStateEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content, const QString& stateKey = {})
-{
- if (!content.isEmpty() || !stateKey.isEmpty())
- {
- auto json = basicEventJson(type, content);
- json.insert(QStringLiteral("state_key"), stateKey);
- events.append(json);
- }
-}
-
-#define ADD_STATE_EVENT(events, type, name, content) \
- appendStateEvent((events), QStringLiteral(type), \
- {{ QStringLiteral(name), content }});
-
-void appendEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content)
-{
- if (!content.isEmpty())
- events.append(basicEventJson(type, content));
-}
-
-template <typename EvtT>
-void appendEvent(QJsonArray& events, const EvtT& event)
-{
- appendEvent(events, EvtT::matrixTypeId(), event.toJson());
-}
-
QJsonObject Room::Private::toJson() const
{
QElapsedTimer et; et.start();
@@ -2051,23 +2106,19 @@ QJsonObject Room::Private::toJson() const
{
QJsonArray stateEvents;
- ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name);
- ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic);
- ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url",
- avatar.url().toString());
- ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases",
- QJsonArray::fromStringList(aliases));
- ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias",
- canonicalAlias);
- ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm",
- encryptionAlgorithm);
-
- for (const auto *m : membersMap)
- appendStateEvent(stateEvents, QStringLiteral("m.room.member"),
- { { QStringLiteral("membership"), QStringLiteral("join") }
- , { QStringLiteral("displayname"), m->rawName(q) }
- , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() }
- }, m->id());
+ for (const auto* evt: currentState)
+ {
+ Q_ASSERT(evt->isStateEvent());
+ if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) ||
+ evt->contentJson().isEmpty())
+ continue;
+
+ auto json = evt->fullJson();
+ auto unsignedJson = evt->unsignedJson();
+ unsignedJson.remove(QStringLiteral("prev_content"));
+ json[UnsignedKeyL] = unsignedJson;
+ stateEvents.append(json);
+ }
const auto stateObjName = joinState == JoinState::Invite ?
QStringLiteral("invite_state") : QStringLiteral("state");
@@ -2075,14 +2126,17 @@ QJsonObject Room::Private::toJson() const
QJsonObject {{ QStringLiteral("events"), stateEvents }});
}
- QJsonArray accountDataEvents;
if (!accountData.empty())
{
+ QJsonArray accountDataEvents;
for (const auto& e: accountData)
- appendEvent(accountDataEvents, e.first, e.second->contentJson());
+ {
+ if (!e.second->contentJson().isEmpty())
+ accountDataEvents.append(e.second->fullJson());
+ }
+ result.insert(QStringLiteral("account_data"),
+ QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
- result.insert(QStringLiteral("account_data"),
- QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
QJsonObject unreadNotifObj
{ { SyncRoomData::UnreadCountKey, unreadMessages } };
@@ -2094,7 +2148,7 @@ QJsonObject Room::Private::toJson() const
result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
- if (et.elapsed() > 50)
+ if (et.elapsed() > 30)
qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
return result;
diff --git a/lib/room.h b/lib/room.h
index f1566ac5..9d4561e5 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -18,7 +18,7 @@
#pragma once
-#include "jobs/syncjob.h"
+#include "csapi/message_pagination.h"
#include "events/roommessageevent.h"
#include "events/accountdataevents.h"
#include "eventitem.h"
@@ -33,6 +33,7 @@
namespace QMatrixClient
{
class Event;
+ class SyncRoomData;
class RoomMemberEvent;
class Connection;
class User;
@@ -95,12 +96,32 @@ namespace QMatrixClient
Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged)
Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged)
+ Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged)
+
public:
using Timeline = std::deque<TimelineItem>;
using PendingEvents = std::vector<PendingEventItem>;
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
+ enum Change : uint {
+ NoChange = 0x0,
+ NameChange = 0x1,
+ CanonicalAliasChange = 0x2,
+ TopicChange = 0x4,
+ UnreadNotifsChange = 0x8,
+ AvatarChange = 0x10,
+ JoinStateChange = 0x20,
+ TagsChange = 0x40,
+ MembersChange = 0x80,
+ EncryptionOn = 0x100,
+ AccountDataChange = 0x200,
+ OtherChange = 0x1000,
+ AnyChange = 0x1FFF
+ };
+ Q_DECLARE_FLAGS(Changes, Change)
+ Q_FLAG(Changes)
+
Room(Connection* connection, QString id, JoinState initialJoinState);
~Room() override;
@@ -126,6 +147,8 @@ namespace QMatrixClient
int timelineSize() const;
bool usesEncryption() const;
+ GetRoomEventsJob* eventsHistoryJob() const;
+
/**
* Returns a square room avatar with the given size and requests it
* from the network if needed
@@ -177,9 +200,16 @@ namespace QMatrixClient
const Timeline& messageEvents() const;
const PendingEvents& pendingEvents() const;
/**
- * A convenience method returning the read marker to
- * the before-oldest message
+ * A convenience method returning the read marker to the position
+ * before the "oldest" event; same as messageEvents().crend()
+ */
+ rev_iter_t historyEdge() const;
+ /**
+ * A convenience method returning the iterator beyond the latest
+ * arrived event; same as messageEvents().cend()
*/
+ Timeline::const_iterator syncEdge() const;
+ /// \deprecated Use historyEdge instead
rev_iter_t timelineEdge() const;
Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const;
Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const;
@@ -323,6 +353,7 @@ namespace QMatrixClient
*
* Takes ownership of the event, deleting it once the matching one
* arrives with the sync
+ * \return transaction id associated with the event.
*/
QString postEvent(RoomEvent* event);
QString postJson(const QString& matrixType,
@@ -356,6 +387,7 @@ namespace QMatrixClient
void markAllMessagesAsRead();
signals:
+ void eventsHistoryJobChanged();
void aboutToAddHistoricalMessages(RoomEventsRange events);
void aboutToAddNewMessages(RoomEventsRange events);
void addedMessages(int fromIndex, int toIndex);
@@ -368,6 +400,13 @@ namespace QMatrixClient
void pendingEventDiscarded();
void pendingEventChanged(int pendingEventIndex);
+ /** A common signal for various kinds of changes in the room
+ * Aside from all changes in the room state
+ * @param changes a set of flags describing what changes occured
+ * upon the last sync
+ * \sa StateChange
+ */
+ void changed(Changes changes);
/**
* \brief The room name, the canonical alias or other aliases changed
*
@@ -417,27 +456,28 @@ namespace QMatrixClient
/// The room is about to be deleted
void beforeDestruction(Room*);
- public: // Used by Connection - not a part of the client API
- QJsonObject toJson() const;
- void updateData(SyncRoomData&& data );
-
- // Clients should use Connection::joinRoom() and Room::leaveRoom()
- // to change the room state
- void setJoinState( JoinState state );
-
protected:
/// Returns true if any of room names/aliases has changed
- virtual bool processStateEvent(const RoomEvent& e);
+ virtual Changes processStateEvent(const RoomEvent& e);
virtual void processEphemeralEvent(EventPtr&& event);
- virtual void processAccountDataEvent(EventPtr&& event);
+ virtual Changes processAccountDataEvent(EventPtr&& event);
virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { }
virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { }
virtual void onRedaction(const RoomEvent& /*prevEvent*/,
const RoomEvent& /*after*/) { }
+ virtual QJsonObject toJson() const;
+ virtual void updateData(SyncRoomData&& data, bool fromCache = false);
private:
+ friend class Connection;
+
class Private;
Private* d;
+
+ // This is called from Connection, reflecting a state change that
+ // arrived from the server. Clients should use
+ // Connection::joinRoom() and Room::leaveRoom() to change the state.
+ void setJoinState(JoinState state);
};
class MemberSorter
@@ -460,3 +500,4 @@ namespace QMatrixClient
};
} // namespace QMatrixClient
Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo)
+Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes)
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
new file mode 100644
index 00000000..1023ed6a
--- /dev/null
+++ b/lib/syncdata.cpp
@@ -0,0 +1,180 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "syncdata.h"
+
+#include "events/eventloader.h"
+
+#include <QtCore/QFile>
+#include <QtCore/QFileInfo>
+
+using namespace QMatrixClient;
+
+const QString SyncRoomData::UnreadCountKey =
+ QStringLiteral("x-qmatrixclient.unread_count");
+
+template <typename EventsArrayT, typename StrT>
+inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
+{
+ return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
+}
+
+SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
+ const QJsonObject& room_)
+ : roomId(roomId_)
+ , joinState(joinState_)
+ , state(load<StateEvents>(room_, joinState == JoinState::Invite
+ ? "invite_state"_ls : "state"_ls))
+{
+ switch (joinState) {
+ case JoinState::Join:
+ ephemeral = load<Events>(room_, "ephemeral"_ls);
+ FALLTHROUGH;
+ case JoinState::Leave:
+ {
+ accountData = load<Events>(room_, "account_data"_ls);
+ timeline = load<RoomEvents>(room_, "timeline"_ls);
+ const auto timelineJson = room_.value("timeline"_ls).toObject();
+ timelineLimited = timelineJson.value("limited"_ls).toBool();
+ timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
+
+ break;
+ }
+ default: /* nothing on top of state */;
+ }
+
+ const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
+ unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
+ highlightCount = unreadJson.value("highlight_count"_ls).toInt();
+ notificationCount = unreadJson.value("notification_count"_ls).toInt();
+ if (highlightCount > 0 || notificationCount > 0)
+ qCDebug(SYNCJOB) << "Room" << roomId_
+ << "has highlights:" << highlightCount
+ << "and notifications:" << notificationCount;
+}
+
+SyncData::SyncData(const QString& cacheFileName)
+{
+ QFileInfo cacheFileInfo { cacheFileName };
+ auto json = loadJson(cacheFileName);
+ auto requiredVersion = std::get<0>(cacheVersion());
+ auto actualVersion = json.value("cache_version").toObject()
+ .value("major").toInt();
+ if (actualVersion == requiredVersion)
+ parseJson(json, cacheFileInfo.absolutePath() + '/');
+ else
+ qCWarning(MAIN)
+ << "Major version of the cache file is" << actualVersion << "but"
+ << requiredVersion << "is required; discarding the cache";
+}
+
+SyncDataList&& SyncData::takeRoomData()
+{
+ return move(roomData);
+}
+
+QString SyncData::fileNameForRoom(QString roomId)
+{
+ roomId.replace(':', '_');
+ return roomId + ".json";
+}
+
+Events&& SyncData::takePresenceData()
+{
+ return std::move(presenceData);
+}
+
+Events&& SyncData::takeAccountData()
+{
+ return std::move(accountData);
+}
+
+Events&& SyncData::takeToDeviceEvents()
+{
+ return std::move(toDeviceEvents);
+}
+
+QJsonObject SyncData::loadJson(const QString& fileName)
+{
+ QFile roomFile { fileName };
+ if (!roomFile.exists())
+ {
+ qCWarning(MAIN) << "No state cache file" << fileName;
+ return {};
+ }
+ if(!roomFile.open(QIODevice::ReadOnly))
+ {
+ qCWarning(MAIN) << "Failed to open state cache file"
+ << roomFile.fileName();
+ return {};
+ }
+ auto data = roomFile.readAll();
+
+ const auto json =
+ (data.startsWith('{') ? QJsonDocument::fromJson(data)
+ : QJsonDocument::fromBinaryData(data)).object();
+ if (json.isEmpty())
+ {
+ qCWarning(MAIN) << "State cache in" << fileName
+ << "is broken or empty, discarding";
+ }
+ return json;
+}
+
+void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
+{
+ QElapsedTimer et; et.start();
+
+ nextBatch_ = json.value("next_batch"_ls).toString();
+ presenceData = load<Events>(json, "presence"_ls);
+ accountData = load<Events>(json, "account_data"_ls);
+ toDeviceEvents = load<Events>(json, "to_device"_ls);
+
+ auto rooms = json.value("rooms"_ls).toObject();
+ JoinStates::Int ii = 1; // ii is used to make a JoinState value
+ auto totalRooms = 0;
+ auto totalEvents = 0;
+ for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
+ {
+ const auto rs = rooms.value(JoinStateStrings[i]).toObject();
+ // We have a Qt container on the right and an STL one on the left
+ roomData.reserve(static_cast<size_t>(rs.size()));
+ for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
+ {
+ auto roomJson = roomIt->isObject()
+ ? roomIt->toObject()
+ : loadJson(baseDir + fileNameForRoom(roomIt.key()));
+ if (roomJson.isEmpty())
+ {
+ unresolvedRoomIds.push_back(roomIt.key());
+ continue;
+ }
+ roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson);
+ const auto& r = roomData.back();
+ totalEvents += r.state.size() + r.ephemeral.size() +
+ r.accountData.size() + r.timeline.size();
+ }
+ totalRooms += rs.size();
+ }
+ if (!unresolvedRoomIds.empty())
+ qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(',');
+ if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
+ << totalRooms << "room(s),"
+ << totalEvents << "event(s) in" << et;
+}
diff --git a/lib/syncdata.h b/lib/syncdata.h
new file mode 100644
index 00000000..aa8948bc
--- /dev/null
+++ b/lib/syncdata.h
@@ -0,0 +1,86 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "joinstate.h"
+#include "events/stateevent.h"
+
+namespace QMatrixClient {
+ class SyncRoomData
+ {
+ public:
+ QString roomId;
+ JoinState joinState;
+ StateEvents state;
+ RoomEvents timeline;
+ Events ephemeral;
+ Events accountData;
+
+ bool timelineLimited;
+ QString timelinePrevBatch;
+ int unreadCount;
+ int highlightCount;
+ int notificationCount;
+
+ SyncRoomData(const QString& roomId, JoinState joinState_,
+ const QJsonObject& room_);
+ SyncRoomData(SyncRoomData&&) = default;
+ SyncRoomData& operator=(SyncRoomData&&) = default;
+
+ static const QString UnreadCountKey;
+ };
+
+ // QVector cannot work with non-copiable objects, std::vector can.
+ using SyncDataList = std::vector<SyncRoomData>;
+
+ class SyncData
+ {
+ public:
+ SyncData() = default;
+ explicit SyncData(const QString& cacheFileName);
+ /** Parse sync response into room events
+ * \param json response from /sync or a room state cache
+ * \return the list of rooms with missing cache files; always
+ * empty when parsing response from /sync
+ */
+ void parseJson(const QJsonObject& json, const QString& baseDir = {});
+
+ Events&& takePresenceData();
+ Events&& takeAccountData();
+ Events&& takeToDeviceEvents();
+ SyncDataList&& takeRoomData();
+
+ QString nextBatch() const { return nextBatch_; }
+
+ QStringList unresolvedRooms() const { return unresolvedRoomIds; }
+
+ static std::pair<int, int> cacheVersion() { return { 9, 0 }; }
+ static QString fileNameForRoom(QString roomId);
+
+ private:
+ QString nextBatch_;
+ Events presenceData;
+ Events accountData;
+ Events toDeviceEvents;
+ SyncDataList roomData;
+ QStringList unresolvedRoomIds;
+
+ static QJsonObject loadJson(const QString& fileName);
+ };
+} // namespace QMatrixClient
diff --git a/lib/user.cpp b/lib/user.cpp
index eec08ad9..eec41957 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -312,6 +312,11 @@ void User::unmarkIgnore()
connection()->removeFromIgnoredUsers(this);
}
+bool User::isIgnored() const
+{
+ return connection()->isIgnored(this);
+}
+
void User::Private::setAvatarOnServer(QString contentUri, User* q)
{
auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri);
@@ -321,14 +326,16 @@ void User::Private::setAvatarOnServer(QString contentUri, User* q)
QString User::displayname(const Room* room) const
{
- auto name = d->nameForRoom(room);
- return name.isEmpty() ? d->userId :
- room ? room->roomMembername(this) : name;
+ if (room)
+ return room->roomMembername(this);
+
+ const auto name = d->nameForRoom(nullptr);
+ return name.isEmpty() ? d->userId : name;
}
QString User::fullName(const Room* room) const
{
- auto name = d->nameForRoom(room);
+ const auto name = d->nameForRoom(room);
return name.isEmpty() ? d->userId : name % " (" % d->userId % ')';
}
diff --git a/lib/user.h b/lib/user.h
index 17f5625f..0023b44a 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -125,6 +125,8 @@ namespace QMatrixClient
void ignore();
/** Remove the user from the ignore list */
void unmarkIgnore();
+ /** Check whether the user is in ignore list */
+ bool isIgnored() const;
signals:
void nameAboutToChange(QString newName, QString oldName,
diff --git a/lib/util.cpp b/lib/util.cpp
index 1773fcfe..af06013c 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -19,6 +19,9 @@
#include "util.h"
#include <QtCore/QRegularExpression>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QDir>
+#include <QtCore/QStringBuilder>
static const auto RegExpOptions =
QRegularExpression::CaseInsensitiveOption
@@ -61,3 +64,14 @@ QString QMatrixClient::prettyPrint(const QString& plainText)
linkifyUrls(pt);
return pt;
}
+
+QString QMatrixClient::cacheLocation(const QString& dirName)
+{
+ const QString cachePath =
+ QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ % '/' % dirName % '/';
+ QDir dir;
+ if (!dir.exists(cachePath))
+ dir.mkpath(cachePath);
+ return cachePath;
+}
diff --git a/lib/util.h b/lib/util.h
index 13eec143..88c756a1 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -240,5 +240,11 @@ namespace QMatrixClient
* This includes HTML escaping of <,>,",& and URLs linkification.
*/
QString prettyPrint(const QString& plainText);
+
+ /** Return a path to cache directory after making sure that it exists
+ * The returned path has a trailing slash, clients don't need to append it.
+ * \param dir path to cache directory relative to the standard cache path
+ */
+ QString cacheLocation(const QString& dirName);
} // namespace QMatrixClient