aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt5
-rw-r--r--connection.cpp192
-rw-r--r--connection.h63
-rw-r--r--connectiondata.cpp3
-rw-r--r--events/event.cpp35
-rw-r--r--events/event.h1
-rw-r--r--events/receiptevent.cpp3
-rw-r--r--events/receiptevent.h2
-rw-r--r--events/roomtopicevent.h12
-rw-r--r--jobs/basejob.cpp35
-rw-r--r--jobs/converters.h6
-rw-r--r--jobs/mediathumbnailjob.cpp17
-rw-r--r--jobs/mediathumbnailjob.h5
-rw-r--r--jobs/setroomstatejob.cpp32
-rw-r--r--jobs/setroomstatejob.h65
-rw-r--r--jobs/syncjob.cpp41
-rw-r--r--jobs/syncjob.h20
-rw-r--r--room.cpp173
-rw-r--r--room.h3
-rw-r--r--user.cpp54
-rw-r--r--user.h3
21 files changed, 610 insertions, 160 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 257c5ee5..f4358521 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
@@ -81,7 +82,9 @@ set(libqmatrixclient_SRCS
jobs/syncjob.cpp
jobs/mediathumbnailjob.cpp
jobs/logoutjob.cpp
- )
+)
+
+aux_source_directory(jobs/generated libqmatrixclient_job_SRCS)
if (MATRIX_DOC_PATH AND APIGEN_PATH)
add_custom_target(update-api
diff --git a/connection.cpp b/connection.cpp
index 5d8a42e3..e20f843f 100644
--- a/connection.cpp
+++ b/connection.cpp
@@ -32,6 +32,12 @@
#include "jobs/mediathumbnailjob.h"
#include <QtNetwork/QDnsLookup>
+#include <QtCore/QFile>
+#include <QtCore/QDir>
+#include <QtCore/QFileInfo>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QStringBuilder>
+#include <QtCore/QElapsedTimer>
using namespace QMatrixClient;
@@ -61,6 +67,8 @@ class Connection::Private
QString userId;
SyncJob* syncJob;
+
+ bool cacheState = true;
};
Connection::Connection(const QUrl& server, QObject* parent)
@@ -161,12 +169,7 @@ void Connection::sync(int timeout)
auto job = d->syncJob =
callApi<SyncJob>(d->data->lastEvent(), filter, timeout);
connect( job, &SyncJob::success, [=] () {
- d->data->setLastEvent(job->nextBatch());
- for( auto&& roomData: job->takeRoomData() )
- {
- if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
- r->updateData(std::move(roomData));
- }
+ onSyncSuccess(job->takeData());
d->syncJob = nullptr;
emit syncDone();
});
@@ -180,6 +183,16 @@ void Connection::sync(int timeout)
});
}
+void Connection::onSyncSuccess(SyncData &&data) {
+ d->data->setLastEvent(data.nextBatch());
+ for( auto&& roomData: data.takeRoomData() )
+ {
+ if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
+ r->updateData(std::move(roomData));
+ }
+
+}
+
void Connection::stopSync()
{
if (d->syncJob)
@@ -271,9 +284,18 @@ int Connection::millisToReconnect() const
return d->syncJob ? d->syncJob->millisToRetry() : 0;
}
-const QHash< QPair<QString, bool>, Room* >& Connection::roomMap() const
+QHash< QPair<QString, bool>, Room* > Connection::roomMap() const
{
- return d->roomMap;
+ // Copy-on-write-and-remove-elements is faster than copying elements one by one.
+ QHash< QPair<QString, bool>, 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,35 +312,60 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState)
return nullptr;
}
+ // 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)
{
- 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;
-
d->roomMap.insert(roomKey, room);
+ qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second;
emit newRoom(room);
}
- else if (room->joinState() != joinState)
+ if (joinState == JoinState::Invite)
{
- room->setJoinState(joinState);
- if (joinState == JoinState::Leave)
- emit leftRoom(room);
- else if (joinState == JoinState::Join)
- emit joinedRoom(room);
+ // prev is either Leave or nullptr
+ auto* prev = d->roomMap.value({id, false}, nullptr);
+ emit invitedRoom(room, prev);
}
-
- if (joinState != JoinState::Invite && d->roomMap.contains({id, true}))
+ else
{
- // Preempt the Invite room after it's been acted upon (joined or left).
- qCDebug(MAIN) << "Deleting invited state";
- delete d->roomMap.take({id, true});
+ room->setJoinState(joinState);
+ // Preempt the Invite room (if any) with a room in Join/Leave state.
+ auto* prevInvite = d->roomMap.take({id, true});
+ if (joinState == JoinState::Join)
+ emit joinedRoom(room, prevInvite);
+ else if (joinState == JoinState::Leave)
+ emit leftRoom(room, prevInvite);
+ if (prevInvite)
+ {
+ qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id();
+ emit aboutToDeleteRoom(prevInvite);
+ delete prevInvite;
+ }
}
return room;
@@ -334,3 +381,102 @@ QByteArray Connection::generateTxnId()
{
return d->data->generateTxnId();
}
+
+void Connection::saveState(const QUrl &toFile) const
+{
+ if (!d->cacheState)
+ return;
+
+ QElapsedTimer et; et.start();
+
+ QFileInfo stateFile {
+ toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile()
+ };
+ if (!stateFile.dir().exists())
+ stateFile.dir().mkpath(".");
+
+ QFile outfile { stateFile.absoluteFilePath() };
+ if (!outfile.open(QFile::WriteOnly))
+ {
+ qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath()
+ << ":" << outfile.errorString();
+ qCWarning(MAIN) << "Caching the rooms state disabled";
+ d->cacheState = false;
+ return;
+ }
+
+ QJsonObject roomObj;
+ {
+ QJsonObject rooms;
+ QJsonObject inviteRooms;
+ for (auto i : roomMap()) // Pass on rooms in Leave state
+ {
+ if (i->joinState() == JoinState::Invite)
+ inviteRooms.insert(i->id(), i->toJson());
+ else
+ rooms.insert(i->id(), i->toJson());
+ }
+
+ if (!rooms.isEmpty())
+ roomObj.insert("join", rooms);
+ if (!inviteRooms.isEmpty())
+ roomObj.insert("invite", inviteRooms);
+ }
+
+ QJsonObject rootObj;
+ rootObj.insert("next_batch", d->data->lastEvent());
+ rootObj.insert("rooms", roomObj);
+
+ QByteArray data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact);
+
+ qCDebug(MAIN) << "Writing state to file" << outfile.fileName();
+ outfile.write(data.data(), data.size());
+ qCDebug(PROFILER) << "*** Cached state for" << userId()
+ << "saved in" << et.elapsed() << "ms";
+}
+
+void Connection::loadState(const QUrl &fromFile)
+{
+ if (!d->cacheState)
+ return;
+
+ QElapsedTimer et; et.start();
+ QFile file {
+ fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile()
+ };
+ if (!file.exists())
+ {
+ qCDebug(MAIN) << "No state cache file found";
+ return;
+ }
+ file.open(QFile::ReadOnly);
+ QByteArray data = file.readAll();
+
+ SyncData sync;
+ sync.parseJson(QJsonDocument::fromJson(data));
+ onSyncSuccess(std::move(sync));
+ qCDebug(PROFILER) << "*** Cached state for" << userId()
+ << "loaded in" << et.elapsed() << "ms";
+}
+
+QString Connection::stateCachePath() const
+{
+ auto safeUserId = userId();
+ safeUserId.replace(':', '_');
+ return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ % '/' % safeUserId % "_state.json";
+}
+
+bool Connection::cacheState() const
+{
+ return d->cacheState;
+}
+
+void Connection::setCacheState(bool newValue)
+{
+ if (d->cacheState != newValue)
+ {
+ d->cacheState = newValue;
+ emit cacheStateChanged();
+ }
+}
diff --git a/connection.h b/connection.h
index b118ffb0..4ca6fbc5 100644
--- a/connection.h
+++ b/connection.h
@@ -35,6 +35,7 @@ namespace QMatrixClient
class ConnectionData;
class SyncJob;
+ class SyncData;
class RoomMessagesJob;
class PostReceiptJob;
class MediaThumbnailJob;
@@ -42,6 +43,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:
using room_factory_t =
std::function<Room*(Connection*, const QString&, JoinState joinState)>;
@@ -52,7 +58,7 @@ namespace QMatrixClient
Connection();
virtual ~Connection();
- const QHash<QPair<QString, bool>, Room*>& roomMap() const;
+ QHash<QPair<QString, bool>, Room*> roomMap() const;
Q_INVOKABLE virtual void resolveServer(const QString& domain);
Q_INVOKABLE virtual void connectToServer(const QString& user,
@@ -72,13 +78,16 @@ namespace QMatrixClient
/** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */
Q_INVOKABLE virtual PostReceiptJob* postReceipt(Room* room,
RoomEvent* event) const;
+ /** @deprecated Use callApi<JoinRoomJob>() instead */
Q_INVOKABLE virtual JoinRoomJob* joinRoom(const QString& roomAlias);
/** @deprecated Use callApi<LeaveRoomJob>() 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<MediaThumbnailJob>() instead */
virtual MediaThumbnailJob* getThumbnail(const QUrl& url,
QSize requestedSize) const;
+ /** @deprecated Use callApi<MediaThumbnailJob>() instead */
MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth,
int requestedHeight) const;
@@ -92,6 +101,44 @@ namespace QMatrixClient
Q_INVOKABLE SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
+ /**
+ * Call this before first sync to load from previously saved file.
+ *
+ * \param fromFile A local path to read the state from. Uses QUrl
+ * to be QML-friendly. Empty parameter means using a path
+ * defined by stateCachePath().
+ */
+ Q_INVOKABLE void loadState(const QUrl &fromFile = {});
+ /**
+ * This method saves the current state of rooms (but not messages
+ * in them) to a local cache file, so that it could be loaded by
+ * loadState() on a next run of the client.
+ *
+ * \param toFile A local path to save the state to. Uses QUrl to be
+ * QML-friendly. Empty parameter means using a path defined by
+ * stateCachePath().
+ */
+ Q_INVOKABLE void saveState(const QUrl &toFile = {}) const;
+
+ /**
+ * The default path to store the cached room state, defined as
+ * follows:
+ * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json"
+ * where `_safeUserId` is userId() with `:` (colon) replaced with
+ * `_` (underscore)
+ * /see loadState(), saveState()
+ */
+ Q_INVOKABLE QString stateCachePath() const;
+
+ bool cacheState() const;
+ void setCacheState(bool newValue);
+
+ /**
+ * This is a universal method to start a job of a type passed
+ * as a template parameter. Arguments to callApi() are arguments
+ * to the job constructor _except_ the first ConnectionData*
+ * argument - callApi() will pass it automatically.
+ */
template <typename JobT, typename... JobArgTs>
JobT* callApi(JobArgTs... jobArgs) const
{
@@ -128,8 +175,10 @@ namespace QMatrixClient
void syncDone();
void newRoom(Room* room);
- void joinedRoom(Room* room);
- void leftRoom(Room* room);
+ 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);
void networkError(size_t nextAttempt, int inMilliseconds);
@@ -137,6 +186,8 @@ namespace QMatrixClient
void syncError(QString error);
//void jobError(BaseJob* job);
+ void cacheStateChanged();
+
protected:
/**
* @brief Access the underlying ConnectionData class
@@ -154,6 +205,12 @@ namespace QMatrixClient
*/
Room* provideRoom(const QString& roomId, JoinState joinState);
+
+ /**
+ * Completes loading sync data.
+ */
+ void onSyncSuccess(SyncData &&data);
+
private:
class Private;
Private* d;
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 <QtNetwork/QNetworkAccessManager>
-#include <cstdlib>
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)
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
diff --git a/events/receiptevent.cpp b/events/receiptevent.cpp
index e3478cf1..3d6be9f1 100644
--- a/events/receiptevent.cpp
+++ b/events/receiptevent.cpp
@@ -46,7 +46,7 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj)
{
Q_ASSERT(obj["type"].toString() == jsonType);
- const QJsonObject contents = obj["content"].toObject();
+ const QJsonObject contents = contentJson();
_eventsWithReceipts.reserve(static_cast<size_t>(contents.size()));
for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt )
{
@@ -66,5 +66,6 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj)
}
_eventsWithReceipts.push_back({eventIt.key(), receipts});
}
+ _unreadMessages = obj["x-qmatrixclient.unread_messages"].toBool();
}
diff --git a/events/receiptevent.h b/events/receiptevent.h
index 1d280822..cbe36b10 100644
--- a/events/receiptevent.h
+++ b/events/receiptevent.h
@@ -41,9 +41,11 @@ namespace QMatrixClient
EventsWithReceipts eventsWithReceipts() const
{ return _eventsWithReceipts; }
+ bool unreadMessages() const { return _unreadMessages; }
private:
EventsWithReceipts _eventsWithReceipts;
+ bool _unreadMessages; // Spec extension for caching purposes
static constexpr const char * jsonType = "m.receipt";
};
diff --git a/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/basejob.cpp b/jobs/basejob.cpp
index 26ceb268..ea1a7158 100644
--- a/jobs/basejob.cpp
+++ b/jobs/basejob.cpp
@@ -24,6 +24,7 @@
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QtCore/QTimer>
+#include <QtCore/QStringBuilder>
#include <array>
@@ -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,11 +159,20 @@ 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;
+ 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 );
- 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()
@@ -219,16 +228,17 @@ BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
void BaseJob::stop()
{
d->timer.stop();
- if (!d->reply)
- {
- qCWarning(d->logCat) << this << "stopped with empty network reply";
- }
- else if (d->reply->isRunning())
+ if (d->reply)
{
- 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 +330,9 @@ void BaseJob::setStatus(int code, QString message)
void BaseJob::abandon()
{
+ this->disconnect();
+ if (d->reply)
+ d->reply->disconnect(this);
deleteLater();
}
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 <QtCore/QJsonValue>
#include <QtCore/QJsonArray>
#include <QtCore/QDate>
+#include <QtCore/QVariant>
namespace QMatrixClient
{
@@ -83,7 +84,6 @@ namespace QMatrixClient
template <>
inline QDate fromJson<QDate>(const QJsonValue& jv)
{
- return QDateTime::fromMSecsSinceEpoch(
- fromJson<qint64>(jv), Qt::UTC).date();
+ return fromJson<QDateTime>(jv).date();
}
-} // namespace QMatrixClient \ No newline at end of file
+} // namespace QMatrixClient
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/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 <kde@fxrh.de>
+ *
+ * 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 <kde@fxrh.de>
+ *
+ * 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 <typename EvT>
+ 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 <typename EvT>
+ 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/jobs/syncjob.cpp b/jobs/syncjob.cpp
index 38cfcb2a..f679e6f4 100644
--- a/jobs/syncjob.cpp
+++ b/jobs/syncjob.cpp
@@ -22,20 +22,12 @@
using namespace QMatrixClient;
-class SyncJob::Private
-{
- public:
- QString nextBatch;
- SyncData roomData;
-};
-
static size_t jobId = 0;
SyncJob::SyncJob(const ConnectionData* connection, const QString& since,
const QString& filter, int timeout, const QString& presence)
: BaseJob(connection, HttpVerb::Get, QString("SyncJob-%1").arg(++jobId),
"_matrix/client/r0/sync")
- , d(new Private)
{
setLoggingCategory(SYNCJOB);
QUrlQuery query;
@@ -52,47 +44,48 @@ SyncJob::SyncJob(const ConnectionData* connection, const QString& since,
setMaxRetries(std::numeric_limits<int>::max());
}
-SyncJob::~SyncJob()
+QString SyncData::nextBatch() const
{
- delete d;
+ return nextBatch_;
}
-QString SyncJob::nextBatch() const
+SyncDataList&& SyncData::takeRoomData()
{
- return d->nextBatch;
+ return std::move(roomData);
}
-SyncData&& SyncJob::takeRoomData()
+BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
{
- return std::move(d->roomData);
+ return d.parseJson(data);
}
-BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
+BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
{
QElapsedTimer et; et.start();
+
QJsonObject json = data.object();
- d->nextBatch = json.value("next_batch").toString();
+ nextBatch_ = json.value("next_batch").toString();
// TODO: presence
// TODO: account_data
QJsonObject rooms = json.value("rooms").toObject();
- 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
- d->roomData.reserve(static_cast<size_t>(rs.size()));
- for( auto rkey: rs.keys() )
- d->roomData.emplace_back(rkey, roomState.enumVal, rs[rkey].toObject());
+ roomData.reserve(static_cast<size_t>(rs.size()));
+ for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
+ roomData.emplace_back(roomIt.key(), roomState.enumVal,
+ roomIt.value().toObject());
}
- qCDebug(PROFILER) << "*** SyncJob::parseJson():" << et.elapsed() << "ms";
-
- return Success;
+ qCDebug(PROFILER) << "*** SyncData::parseJson():" << et.elapsed() << "ms";
+ return BaseJob::Success;
}
SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
diff --git a/jobs/syncjob.h b/jobs/syncjob.h
index 57a87c9f..6697a265 100644
--- a/jobs/syncjob.h
+++ b/jobs/syncjob.h
@@ -66,7 +66,18 @@ Q_DECLARE_TYPEINFO(QMatrixClient::SyncRoomData, Q_MOVABLE_TYPE);
namespace QMatrixClient
{
// QVector cannot work with non-copiable objects, std::vector can.
- using SyncData = std::vector<SyncRoomData>;
+ using SyncDataList = std::vector<SyncRoomData>;
+
+ class SyncData {
+ public:
+ BaseJob::Status parseJson(const QJsonDocument &data);
+ SyncDataList&& takeRoomData();
+ QString nextBatch() const;
+
+ private:
+ QString nextBatch_;
+ SyncDataList roomData;
+ };
class SyncJob: public BaseJob
{
@@ -74,16 +85,13 @@ namespace QMatrixClient
explicit SyncJob(const ConnectionData* connection, const QString& since = {},
const QString& filter = {},
int timeout = -1, const QString& presence = {});
- virtual ~SyncJob();
- SyncData&& takeRoomData();
- QString nextBatch() const;
+ SyncData &&takeData() { return std::move(d); }
protected:
Status parseJson(const QJsonDocument& data) override;
private:
- class Private;
- Private* d;
+ SyncData d;
};
} // namespace QMatrixClient
diff --git a/room.cpp b/room.cpp
index 78e5b80d..2ba3766a 100644
--- a/room.cpp
+++ b/room.cpp
@@ -26,6 +26,7 @@
#include <QtCore/QHash>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
#include <QtCore/QElapsedTimer>
+#include <jobs/setroomstatejob.h>
#include "connection.h"
#include "state.h"
@@ -120,7 +121,10 @@ 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;
private:
QString calculateDisplayname() const;
@@ -211,13 +215,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());
@@ -411,10 +416,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,
@@ -534,27 +538,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())
+ {
+ et.start();
+ processStateEvents(data.state);
+ qCDebug(PROFILER) << "*** Room::processStateEvents(state):"
+ << et.elapsed() << "ms," << data.state.size() << "events";
+ }
+ if (!data.timeline.empty())
{
- processEphemeralEvent(ephemeralEvent);
+ 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 )
{
@@ -584,6 +596,12 @@ void Room::postMessage(RoomMessageEvent* event)
connection()->callApi<SendEventJob>(id(), event);
}
+void Room::setTopic(const QString& newTopic)
+{
+ RoomTopicEvent evt(newTopic);
+ connection()->callApi<SetRoomStateJob>(id(), &evt);
+}
+
void Room::getPreviousContent(int limit)
{
d->getPreviousContent(limit);
@@ -701,9 +719,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();
}
@@ -815,6 +846,8 @@ void Room::processEphemeralEvent(Event* event)
d->setLastReadEvent(m, p.evtId);
}
}
+ if (receiptEvent->unreadMessages())
+ d->unreadMessages = true;
break;
}
default:
@@ -902,6 +935,95 @@ void Room::Private::updateDisplayname()
emit q->displaynameChanged(q);
}
+QJsonObject stateEventToJson(const QString& type, const QString& name,
+ const QJsonValue& content)
+{
+ QJsonObject contentObj;
+ contentObj.insert(name, content);
+
+ QJsonObject eventObj;
+ eventObj.insert("type", type);
+ eventObj.insert("content", contentObj);
+
+ return eventObj;
+}
+
+QJsonObject Room::Private::toJson() const
+{
+ QJsonObject result;
+ {
+ QJsonArray stateEvents;
+
+ stateEvents.append(stateEventToJson("m.room.name", "name", name));
+ stateEvents.append(stateEventToJson("m.room.topic", "topic", topic));
+ stateEvents.append(stateEventToJson("m.room.aliases", "aliases",
+ QJsonArray::fromStringList(aliases)));
+ stateEvents.append(stateEventToJson("m.room.canonical_alias", "alias",
+ canonicalAlias));
+
+ for (const auto &i : membersMap)
+ {
+ QJsonObject content;
+ content.insert("membership", QStringLiteral("join"));
+ content.insert("displayname", i->displayname());
+ content.insert("avatar_url", i->avatarUrl().toString());
+
+ QJsonObject memberEvent;
+ memberEvent.insert("type", QStringLiteral("m.room.member"));
+ memberEvent.insert("state_key", i->id());
+ memberEvent.insert("content", content);
+ stateEvents.append(memberEvent);
+ }
+
+ QJsonObject roomStateObj;
+ roomStateObj.insert("events", stateEvents);
+
+ result.insert("state", roomStateObj);
+ }
+
+ if (!q->readMarkerEventId().isEmpty())
+ {
+ QJsonArray ephemeralEvents;
+ {
+ // Don't dump the timestamp because it's useless in the cache.
+ QJsonObject user;
+ user.insert(connection->userId(), {});
+
+ QJsonObject receipt;
+ receipt.insert("m.read", user);
+
+ QJsonObject lastReadEvent;
+ lastReadEvent.insert(q->readMarkerEventId(), receipt);
+
+ QJsonObject receiptsObj;
+ receiptsObj.insert("type", QStringLiteral("m.receipt"));
+ receiptsObj.insert("content", lastReadEvent);
+ // In extension of the spec we add a hint to the receipt event
+ // to allow setting the unread indicator without downloading
+ // and analysing the timeline.
+ receiptsObj.insert("x-qmatrixclient.unread_messages", unreadMessages);
+ ephemeralEvents.append(receiptsObj);
+ }
+
+ QJsonObject ephemeralObj;
+ ephemeralObj.insert("events", ephemeralEvents);
+
+ result.insert("ephemeral", ephemeralObj);
+ }
+
+ QJsonObject unreadNotificationsObj;
+ unreadNotificationsObj.insert("highlight_count", highlightCount);
+ unreadNotificationsObj.insert("notification_count", notificationCount);
+ result.insert("unread_notifications", unreadNotificationsObj);
+
+ return result;
+}
+
+QJsonObject Room::toJson() const
+{
+ return d->toJson();
+}
+
MemberSorter Room::memberSorter() const
{
return MemberSorter(this);
@@ -917,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 9465a960..393dced3 100644
--- a/room.h
+++ b/room.h
@@ -145,12 +145,15 @@ namespace QMatrixClient
MemberSorter memberSorter() const;
+ QJsonObject toJson() const;
+
public slots:
void postMessage(const QString& plainText,
MessageEventType type = MessageEventType::Text);
void postMessage(RoomMessageEvent* event);
/** @deprecated */
void postMessage(const QString& type, const QString& plainText);
+ void setTopic(const QString& newTopic);
void getPreviousContent(int limit = 10);
diff --git a/user.cpp b/user.cpp
index 8d37eef6..12eb2e0b 100644
--- a/user.cpp
+++ b/user.cpp
@@ -28,7 +28,6 @@
#include <QtCore/QDebug>
#include <QtGui/QIcon>
#include <QtCore/QRegularExpression>
-#include <algorithm>
using namespace QMatrixClient;
@@ -52,7 +51,9 @@ class User::Private
QSize requestedSize;
bool avatarValid;
bool avatarOngoingRequest;
- QVector<QPixmap> scaledAvatars;
+ /// Map of requested size to the actual pixmap used for it
+ /// (it's a shame that QSize has no predefined qHash()).
+ QHash<QPair<int,int>, QPixmap> scaledAvatars;
QString bridged;
void requestAvatar();
@@ -92,24 +93,21 @@ 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)
@@ -170,12 +167,11 @@ void User::requestAvatar()
void User::Private::requestAvatar()
{
- MediaThumbnailJob* job = connection->getThumbnail(avatarUrl, requestedSize);
+ auto* job = connection->callApi<MediaThumbnailJob>(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);
});
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);