aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--avatar.cpp12
-rw-r--r--avatar.h1
-rw-r--r--connection.cpp50
-rw-r--r--events/event.h28
-rw-r--r--events/eventcontent.cpp15
-rw-r--r--events/eventcontent.h75
-rw-r--r--events/roommessageevent.cpp24
-rw-r--r--events/roommessageevent.h29
-rw-r--r--jobs/basejob.cpp7
-rw-r--r--jobs/downloadfilejob.cpp5
-rw-r--r--room.cpp209
-rw-r--r--room.h47
-rw-r--r--user.cpp12
-rw-r--r--user.h8
14 files changed, 406 insertions, 116 deletions
diff --git a/avatar.cpp b/avatar.cpp
index a13507fb..040bf9bb 100644
--- a/avatar.cpp
+++ b/avatar.cpp
@@ -62,6 +62,11 @@ QImage Avatar::get(int width, int height, notifier_t notifier) const
return d->get({width, height}, notifier);
}
+QString Avatar::mediaId() const
+{
+ return d->_url.authority() + d->_url.path();
+}
+
QImage Avatar::Private::get(QSize size, Avatar::notifier_t notifier) const
{
// FIXME: Alternating between longer-width and longer-height requests
@@ -113,6 +118,13 @@ bool Avatar::updateUrl(const QUrl& newUrl)
if (newUrl == d->_url)
return false;
+ // FIXME: Make it a library-wide constant and maybe even make the URL checker
+ // a Connection(?) method.
+ if (newUrl.scheme() != "mxc" || newUrl.path().count('/') != 1)
+ {
+ qCWarning(MAIN) << "Malformed avatar URL:" << newUrl.toDisplayString();
+ return false;
+ }
d->_url = newUrl;
d->_valid = false;
return true;
diff --git a/avatar.h b/avatar.h
index 28c16e4d..4d476ea5 100644
--- a/avatar.h
+++ b/avatar.h
@@ -38,6 +38,7 @@ namespace QMatrixClient
QImage get(int dimension, notifier_t notifier) const;
QImage get(int w, int h, notifier_t notifier) const;
+ QString mediaId() const;
QUrl url() const;
bool updateUrl(const QUrl& newUrl);
diff --git a/connection.cpp b/connection.cpp
index f41f6c10..e46d08aa 100644
--- a/connection.cpp
+++ b/connection.cpp
@@ -59,6 +59,7 @@ class Connection::Private
// separately so we should, e.g., keep objects for Invite and
// Leave state of the same room.
QHash<QPair<QString, bool>, Room*> roomMap;
+ QVector<QString> roomIdsToForget;
QHash<QString, User*> userMap;
QString userId;
@@ -150,7 +151,7 @@ void Connection::connectToServer(const QString& user, const QString& password,
const QString& deviceId)
{
checkAndConnect(user,
- [&] {
+ [=] {
doConnectToServer(user, password, initialDeviceName, deviceId);
});
}
@@ -177,7 +178,7 @@ void Connection::connectWithToken(const QString& userId,
const QString& deviceId)
{
checkAndConnect(userId,
- [&] { d->connectWithToken(userId, accessToken, deviceId); });
+ [=] { d->connectWithToken(userId, accessToken, deviceId); });
}
void Connection::Private::connectWithToken(const QString& user,
@@ -255,6 +256,21 @@ void Connection::onSyncSuccess(SyncData &&data) {
d->data->setLastEvent(data.nextBatch());
for( auto&& roomData: data.takeRoomData() )
{
+ const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId);
+ if (forgetIdx != -1)
+ {
+ d->roomIdsToForget.removeAt(forgetIdx);
+ if (roomData.joinState == JoinState::Leave)
+ {
+ qDebug(MAIN) << "Room" << roomData.roomId
+ << "has been forgotten, ignoring /sync response for it";
+ continue;
+ }
+ qWarning(MAIN) << "Room" << roomData.roomId
+ << "has just been forgotten but /sync returned it in"
+ << toCString(roomData.joinState)
+ << "state - suspiciously fast turnaround";
+ }
if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
r->updateData(std::move(roomData));
}
@@ -376,20 +392,26 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
// a ForgetRoomJob is created in advance and can be returned in a probably
// not-yet-started state (it will start once /leave completes).
auto forgetJob = new ForgetRoomJob(id);
- auto joinedRoom = d->roomMap.value({id, false});
- if (joinedRoom && joinedRoom->joinState() == JoinState::Join)
+ auto room = d->roomMap.value({id, false});
+ if (!room)
+ room = d->roomMap.value({id, true});
+ if (room && room->joinState() != JoinState::Leave)
{
- auto leaveJob = joinedRoom->leaveRoom();
- connect(leaveJob, &BaseJob::success,
- this, [this, forgetJob] { forgetJob->start(connectionData()); });
+ auto leaveJob = room->leaveRoom();
+ connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] {
+ forgetJob->start(connectionData());
+ // If the matching /sync response hasn't arrived yet, mark the room
+ // for explicit deletion
+ if (room->joinState() != JoinState::Leave)
+ d->roomIdsToForget.push_back(room->id());
+ });
connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon);
}
else
forgetJob->start(connectionData());
- connect(forgetJob, &BaseJob::success, this, [this, &id]
+ connect(forgetJob, &BaseJob::success, this, [this, id]
{
- // If the room happens to be in the map (possible in both forms),
- // delete the found object(s).
+ // If the room is in the map (possibly in both forms), delete all forms.
for (auto f: {false, true})
if (auto r = d->roomMap.take({ id, f }))
{
@@ -476,11 +498,7 @@ const ConnectionData* Connection::connectionData() const
Room* Connection::provideRoom(const QString& id, JoinState joinState)
{
// TODO: This whole function is a strong case for a RoomManager class.
- if (id.isEmpty())
- {
- qCDebug(MAIN) << "Connection::provideRoom() with empty id, doing nothing";
- return nullptr;
- }
+ Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
auto* room = d->roomMap.value(roomKey, nullptr);
@@ -550,7 +568,7 @@ void Connection::setHomeserver(const QUrl& url)
emit homeserverChanged(homeserver());
}
-static constexpr int CACHE_VERSION_MAJOR = 1;
+static constexpr int CACHE_VERSION_MAJOR = 2;
static constexpr int CACHE_VERSION_MINOR = 0;
void Connection::saveState(const QUrl &toFile) const
diff --git a/events/event.h b/events/event.h
index 6ed5ba49..b5a4d94e 100644
--- a/events/event.h
+++ b/events/event.h
@@ -256,6 +256,21 @@ namespace QMatrixClient
};
template <typename ContentT>
+ struct Prev
+ {
+ template <typename... ContentParamTs>
+ explicit Prev(const QJsonObject& unsignedJson,
+ ContentParamTs&&... contentParams)
+ : senderId(unsignedJson.value("prev_sender").toString())
+ , content(unsignedJson.value("prev_content").toObject(),
+ std::forward<ContentParamTs>(contentParams)...)
+ { }
+
+ QString senderId;
+ ContentT content;
+ };
+
+ template <typename ContentT>
class StateEvent: public StateEventBase
{
public:
@@ -270,9 +285,8 @@ namespace QMatrixClient
{
auto unsignedData = obj.value("unsigned").toObject();
if (unsignedData.contains("prev_content"))
- _prev.reset(new ContentT(
- unsignedData.value("prev_content").toObject(),
- std::forward<ContentParamTs>(contentParams)...));
+ _prev = std::make_unique<Prev<ContentT>>(unsignedData,
+ std::forward<ContentParamTs>(contentParams)...);
}
template <typename... ContentParamTs>
explicit StateEvent(Type type, ContentParamTs&&... contentParams)
@@ -283,11 +297,15 @@ namespace QMatrixClient
QJsonObject toJson() const { return _content.toJson(); }
ContentT content() const { return _content; }
- ContentT* prev_content() const { return _prev.data(); }
+ /** @deprecated Use prevContent instead */
+ ContentT* prev_content() const { return prevContent(); }
+ ContentT* prevContent() const
+ { return _prev ? &_prev->content : nullptr; }
+ QString prevSenderId() const { return _prev ? _prev->senderId : ""; }
protected:
ContentT _content;
- QScopedPointer<ContentT> _prev;
+ std::unique_ptr<Prev<ContentT>> _prev;
};
} // namespace QMatrixClient
Q_DECLARE_METATYPE(QMatrixClient::Event*)
diff --git a/events/eventcontent.cpp b/events/eventcontent.cpp
index 271669e2..f5974b46 100644
--- a/events/eventcontent.cpp
+++ b/events/eventcontent.cpp
@@ -44,7 +44,6 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
, payloadSize(infoJson["size"].toInt())
, originalName(originalFilename)
{
- originalInfoJson.insert("mediaId", url.authority() + url.path());
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
@@ -74,15 +73,13 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert("h", imageSize.height());
}
-WithThumbnail::WithThumbnail(const QJsonObject& infoJson)
- : thumbnail(infoJson["thumbnail_url"].toString(),
- infoJson["thumbnail_info"].toObject())
+Thumbnail::Thumbnail(const QJsonObject& infoJson)
+ : ImageInfo(infoJson["thumbnail_url"].toString(),
+ infoJson["thumbnail_info"].toObject())
{ }
-void WithThumbnail::fillInfoJson(QJsonObject* infoJson) const
+void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
{
- infoJson->insert("thumbnail_url", thumbnail.url.toString());
- QJsonObject thumbnailInfoJson;
- thumbnail.fillInfoJson(&thumbnailInfoJson);
- infoJson->insert("thumbnail_info", thumbnailInfoJson);
+ infoJson->insert("thumbnail_url", url.toString());
+ infoJson->insert("thumbnail_info", toInfoJson<ImageInfo>(*this));
}
diff --git a/events/eventcontent.h b/events/eventcontent.h
index b37dc923..4afbaff3 100644
--- a/events/eventcontent.h
+++ b/events/eventcontent.h
@@ -27,6 +27,8 @@
#include <QtCore/QUrl>
#include <QtCore/QSize>
+#include <functional>
+
namespace QMatrixClient
{
namespace EventContent
@@ -144,6 +146,14 @@ namespace QMatrixClient
QString originalName;
};
+ template <typename InfoT>
+ QJsonObject toInfoJson(const InfoT& info)
+ {
+ QJsonObject infoJson;
+ info.fillInfoJson(&infoJson);
+ return infoJson;
+ }
+
/**
* A content info class for image content types: image, thumbnail, video
*/
@@ -163,18 +173,18 @@ namespace QMatrixClient
};
/**
- * A mixin class for an info type that carries a thumbnail
+ * An auxiliary class for an info type that carries a thumbnail
*
* This class saves/loads a thumbnail to/from "info" subobject of
* the JSON representation of event content; namely,
* "info/thumbnail_url" and "info/thumbnail_info" fields are used.
*/
- class WithThumbnail
+ class Thumbnail : public ImageInfo
{
public:
- WithThumbnail(const QJsonObject& infoJson);
- WithThumbnail(const ImageInfo& info)
- : thumbnail(info)
+ Thumbnail(const QJsonObject& infoJson);
+ Thumbnail(const ImageInfo& info)
+ : ImageInfo(info)
{ }
/**
@@ -182,9 +192,6 @@ namespace QMatrixClient
* and thumbnail URL to "thumbnail_url" node inside "info".
*/
void fillInfoJson(QJsonObject* infoJson) const;
-
- public:
- ImageInfo thumbnail;
};
class TypedBase: public Base
@@ -204,19 +211,22 @@ namespace QMatrixClient
* the parameter type.
*
* \tparam InfoT base info class
- * \tparam InfoMixinTs... additional info mixin classes (e.g. WithThumbnail)
*/
- template <class InfoT, class... InfoMixinTs>
- class UrlBasedContent :
- public TypedBase, public InfoT, public InfoMixinTs...
+ template <class InfoT>
+ class UrlBasedContent : public TypedBase, public InfoT
{
public:
+ UrlBasedContent(QUrl url, InfoT&& info, QString filename = {})
+ : InfoT(url, std::forward<InfoT>(info), filename)
+ { }
explicit UrlBasedContent(const QJsonObject& json)
: TypedBase(json)
, InfoT(json["url"].toString(), json["info"].toObject(),
json["filename"].toString())
- , InfoMixinTs(InfoT::originalInfoJson)...
- { }
+ {
+ // A small hack to facilitate links creation in QML.
+ originalJson.insert("mediaId", InfoT::mediaId());
+ }
QMimeType type() const override { return InfoT::mimeType; }
const FileInfo* fileInfo() const override { return this; }
@@ -228,12 +238,33 @@ namespace QMatrixClient
json->insert("url", InfoT::url.toString());
if (!InfoT::originalName.isEmpty())
json->insert("filename", InfoT::originalName);
- QJsonObject infoJson;
- InfoT::fillInfoJson(&infoJson);
- // http://en.cppreference.com/w/cpp/language/parameter_pack#Brace-enclosed_initializers
- // Looking forward to C++17 and its folding awesomeness.
- int d[] = { (InfoMixinTs::fillInfoJson(&infoJson), 0)... };
- Q_UNUSED(d);
+ json->insert("info", toInfoJson<InfoT>(*this));
+ }
+ };
+
+ template <typename InfoT>
+ class UrlWithThumbnailContent : public UrlBasedContent<InfoT>
+ {
+ public:
+ // TODO: POD constructor
+ explicit UrlWithThumbnailContent(const QJsonObject& json)
+ : UrlBasedContent<InfoT>(json)
+ , thumbnail(InfoT::originalInfoJson)
+ {
+ // Another small hack, to simplify making a thumbnail link
+ UrlBasedContent<InfoT>::originalJson.insert(
+ "thumbnailMediaId", thumbnail.mediaId());
+ }
+
+ public:
+ Thumbnail thumbnail;
+
+ protected:
+ void fillJson(QJsonObject* json) const override
+ {
+ UrlBasedContent<InfoT>::fillJson(json);
+ auto infoJson = json->take("info").toObject();
+ thumbnail.fillInfoJson(&infoJson);
json->insert("info", infoJson);
}
};
@@ -256,7 +287,7 @@ namespace QMatrixClient
* - mimeType
* - imageSize
*/
- using ImageContent = UrlBasedContent<ImageInfo, WithThumbnail>;
+ using ImageContent = UrlWithThumbnailContent<ImageInfo>;
/**
* Content class for m.file
@@ -274,6 +305,6 @@ namespace QMatrixClient
* - thumbnail.mimeType
* - thumbnail.imageSize (QSize for "h" and "w" in JSON)
*/
- using FileContent = UrlBasedContent<FileInfo, WithThumbnail>;
+ using FileContent = UrlWithThumbnailContent<FileInfo>;
} // namespace EventContent
} // namespace QMatrixClient
diff --git a/events/roommessageevent.cpp b/events/roommessageevent.cpp
index 20e81564..8c088f21 100644
--- a/events/roommessageevent.cpp
+++ b/events/roommessageevent.cpp
@@ -116,6 +116,13 @@ QMimeType RoomMessageEvent::mimeType() const
QMimeDatabase().mimeTypeForName("text/plain");
}
+bool RoomMessageEvent::hasTextContent() const
+{
+ return content() &&
+ (msgtype() == MsgType::Text || msgtype() == MsgType::Emote ||
+ msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes
+}
+
bool RoomMessageEvent::hasFileContent() const
{
return content() && content()->fileInfo();
@@ -159,13 +166,13 @@ void TextContent::fillJson(QJsonObject* json) const
}
LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail)
- : WithThumbnail(thumbnail), geoUri(geoUri)
+ : geoUri(geoUri), thumbnail(thumbnail)
{ }
LocationContent::LocationContent(const QJsonObject& json)
: TypedBase(json)
- , WithThumbnail(json["info"].toObject())
, geoUri(json["geo_uri"].toString())
+ , thumbnail(json["info"].toObject())
{ }
QMimeType LocationContent::type() const
@@ -177,16 +184,5 @@ void LocationContent::fillJson(QJsonObject* o) const
{
Q_ASSERT(o);
o->insert("geo_uri", geoUri);
- QJsonObject infoJson;
- WithThumbnail::fillInfoJson(&infoJson);
- o->insert("info", infoJson);
-}
-
-WithDuration::WithDuration(const QJsonObject& infoJson)
- : duration(infoJson["duration"].toInt())
-{ }
-
-void WithDuration::fillInfoJson(QJsonObject* infoJson) const
-{
- infoJson->insert("duration", duration);
+ o->insert("info", toInfoJson(thumbnail));
}
diff --git a/events/roommessageevent.h b/events/roommessageevent.h
index 6b551b76..2a5eeb7e 100644
--- a/events/roommessageevent.h
+++ b/events/roommessageevent.h
@@ -59,6 +59,7 @@ namespace QMatrixClient
EventContent::TypedBase* content() const
{ return _content.data(); }
QMimeType mimeType() const;
+ bool hasTextContent() const;
bool hasFileContent() const;
QJsonObject toJson() const;
@@ -112,7 +113,7 @@ namespace QMatrixClient
* - thumbnail.mimeType
* - thumbnail.imageSize
*/
- class LocationContent: public TypedBase, public WithThumbnail
+ class LocationContent: public TypedBase
{
public:
LocationContent(const QString& geoUri,
@@ -123,21 +124,32 @@ namespace QMatrixClient
public:
QString geoUri;
+ Thumbnail thumbnail;
protected:
void fillJson(QJsonObject* o) const override;
};
/**
- * A mixin class for info types that include duration: audio and video
+ * A base class for info types that include duration: audio and video
*/
- class WithDuration
+ template <typename ContentT>
+ class PlayableContent : public ContentT
{
public:
- explicit WithDuration(int duration) : duration(duration) { }
- WithDuration(const QJsonObject& infoJson);
+ PlayableContent(const QJsonObject& json)
+ : ContentT(json)
+ , duration(ContentT::originalInfoJson["duration"].toInt())
+ { }
- void fillInfoJson(QJsonObject* infoJson) const;
+ protected:
+ void fillJson(QJsonObject* json) const override
+ {
+ ContentT::fillJson(json);
+ auto infoJson = json->take("info").toObject();
+ infoJson.insert("duration", duration);
+ json->insert("info", infoJson);
+ }
public:
int duration;
@@ -162,8 +174,7 @@ namespace QMatrixClient
* - mimeType
* - imageSize
*/
- using VideoContent =
- UrlBasedContent<ImageInfo, WithThumbnail, WithDuration>;
+ using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>;
/**
* Content class for m.audio
@@ -177,6 +188,6 @@ namespace QMatrixClient
* - mimeType ("mimetype" in JSON)
* - duration
*/
- using AudioContent = UrlBasedContent<FileInfo, WithDuration>;
+ using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>;
} // namespace EventContent
} // namespace QMatrixClient
diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp
index 7fc56287..22ce5bd5 100644
--- a/jobs/basejob.cpp
+++ b/jobs/basejob.cpp
@@ -290,9 +290,10 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const
{
- qCDebug(d->logCat) << this << "returned from" << reply->url().toDisplayString();
- if (reply->error() != QNetworkReply::NoError)
- qCDebug(d->logCat) << this << "returned" << reply->error();
+ qCDebug(d->logCat) << this << "returned"
+ << (reply->error() == QNetworkReply::NoError ?
+ "Success" : reply->errorString())
+ << "from" << reply->url().toDisplayString();
switch( reply->error() )
{
case QNetworkReply::NoError:
diff --git a/jobs/downloadfilejob.cpp b/jobs/downloadfilejob.cpp
index 2530e259..06fa3b48 100644
--- a/jobs/downloadfilejob.cpp
+++ b/jobs/downloadfilejob.cpp
@@ -36,14 +36,15 @@ QString DownloadFileJob::targetFileName() const
void DownloadFileJob::beforeStart(const ConnectionData*)
{
- if (d->targetFile && !d->targetFile->open(QIODevice::WriteOnly))
+ if (d->targetFile && !d->targetFile->isReadable() &&
+ !d->targetFile->open(QIODevice::WriteOnly))
{
qCWarning(JOBS) << "Couldn't open the file"
<< d->targetFile->fileName() << "for writing";
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->open(QIODevice::WriteOnly))
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly))
{
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
diff --git a/room.cpp b/room.cpp
index 0ef78c9f..bc7c083e 100644
--- a/room.cpp
+++ b/room.cpp
@@ -43,9 +43,11 @@
#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
#include <QtCore/QDir>
+#include <QtCore/QRegularExpression>
#include <array>
#include <functional>
+#include <cmath>
using namespace QMatrixClient;
using namespace std::placeholders;
@@ -61,8 +63,7 @@ class Room::Private
Private(Connection* c, QString id_, JoinState initialJoinState)
: q(nullptr), connection(c), id(std::move(id_))
- , avatar(c), joinState(initialJoinState), unreadMessages(false)
- , highlightCount(0), notificationCount(0), roomMessagesJob(nullptr)
+ , avatar(c), joinState(initialJoinState)
{ }
Room* q;
@@ -83,18 +84,27 @@ class Room::Private
QString topic;
Avatar avatar;
JoinState joinState;
- bool unreadMessages;
- int highlightCount;
- int notificationCount;
+ int highlightCount = 0;
+ int notificationCount = 0;
members_map_t membersMap;
QList<User*> usersTyping;
QList<User*> membersLeft;
+ bool unreadMessages = false;
+ bool displayed = false;
+ QString firstDisplayedEventId;
+ QString lastDisplayedEventId;
QHash<const User*, QString> lastReadEventIds;
QString prevBatch;
- RoomMessagesJob* roomMessagesJob;
+ QPointer<RoomMessagesJob> roomMessagesJob;
struct FileTransferPrivateInfo
{
+#if defined(_MSC_VER) && _MSC_VER < 1910
+ FileTransferPrivateInfo() = default;
+ FileTransferPrivateInfo(BaseJob* j, QString fileName)
+ : job(j), localFileInfo(fileName)
+ { }
+#endif
QPointer<BaseJob> job = nullptr;
QFileInfo localFileInfo { };
FileTransferInfo::Status status = FileTransferInfo::Started;
@@ -103,13 +113,13 @@ class Room::Private
void update(qint64 p, qint64 t)
{
- progress = p; total = t;
if (t == 0)
{
t = -1;
if (p == 0)
p = -1;
}
+ progress = p; total = t;
}
};
void failedTransfer(const QString& tid, const QString& errorMessage = {})
@@ -203,9 +213,13 @@ RoomEventPtr TimelineItem::replaceEvent(RoomEventPtr&& other)
Room::Room(Connection* connection, QString id, JoinState initialJoinState)
: QObject(connection), d(new Private(connection, id, initialJoinState))
{
+ setObjectName(id);
// See "Accessing the Public Class" section in
// https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
d->q = this;
+ connect(this, &Room::userAdded, this, &Room::memberListChanged);
+ connect(this, &Room::userRemoved, this, &Room::memberListChanged);
+ connect(this, &Room::memberRenamed, this, &Room::memberListChanged);
qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id;
}
@@ -249,6 +263,16 @@ QString Room::topic() const
return d->topic;
}
+QString Room::avatarMediaId() const
+{
+ return d->avatar.mediaId();
+}
+
+QUrl Room::avatarUrl() const
+{
+ return d->avatar.url();
+}
+
QImage Room::avatar(int dimension)
{
return avatar(dimension, dimension);
@@ -265,8 +289,8 @@ QImage Room::avatar(int width, int height)
auto theOtherOneIt = d->membersMap.begin();
if (theOtherOneIt.value() == localUser())
++theOtherOneIt;
- return theOtherOneIt.value()->avatarObject()
- .get(width, height, [=] { emit avatarChanged(); });
+ return (*theOtherOneIt)->avatarObject()
+ .get(width, height, [=] { emit avatarChanged(); });
}
return {};
}
@@ -289,7 +313,10 @@ void Room::setJoinState(JoinState state)
void Room::Private::setLastReadEvent(User* u, const QString& eventId)
{
- lastReadEventIds.insert(u, eventId);
+ auto& storedId = lastReadEventIds[u];
+ if (storedId == eventId)
+ return;
+ storedId = eventId;
emit q->lastReadEventChanged(u);
if (isLocalUser(u))
emit q->readMarkerMoved();
@@ -405,6 +432,75 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
return timelineEdge();
}
+bool Room::displayed() const
+{
+ return d->displayed;
+}
+
+void Room::setDisplayed(bool displayed)
+{
+ if (d->displayed == displayed)
+ return;
+
+ d->displayed = displayed;
+ emit displayedChanged(displayed);
+ if( displayed )
+ {
+ resetHighlightCount();
+ resetNotificationCount();
+ }
+}
+
+QString Room::firstDisplayedEventId() const
+{
+ return d->firstDisplayedEventId;
+}
+
+Room::rev_iter_t Room::firstDisplayedMarker() const
+{
+ return findInTimeline(firstDisplayedEventId());
+}
+
+void Room::setFirstDisplayedEventId(const QString& eventId)
+{
+ if (d->firstDisplayedEventId == eventId)
+ return;
+
+ d->firstDisplayedEventId = eventId;
+ emit firstDisplayedEventChanged();
+}
+
+void Room::setFirstDisplayedEvent(TimelineItem::index_t index)
+{
+ Q_ASSERT(isValidIndex(index));
+ setFirstDisplayedEventId(findInTimeline(index)->event()->id());
+}
+
+QString Room::lastDisplayedEventId() const
+{
+ return d->lastDisplayedEventId;
+}
+
+Room::rev_iter_t Room::lastDisplayedMarker() const
+{
+ return findInTimeline(lastDisplayedEventId());
+}
+
+void Room::setLastDisplayedEventId(const QString& eventId)
+{
+ if (d->lastDisplayedEventId == eventId)
+ return;
+
+ d->lastDisplayedEventId = eventId;
+ emit lastDisplayedEventChanged();
+}
+
+void Room::setLastDisplayedEvent(TimelineItem::index_t index)
+{
+ Q_ASSERT(isValidIndex(index));
+ setLastDisplayedEventId(findInTimeline(index)->event()->id());
+}
+
Room::rev_iter_t Room::readMarker(const User* user) const
{
Q_ASSERT(user);
@@ -465,10 +561,64 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
total = INT_MAX;
}
+#if defined(_MSC_VER) && _MSC_VER < 1910
+ // A workaround for MSVC 2015 that fails with "error C2440: 'return':
+ // cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'"
+ FileTransferInfo fti;
+ fti.status = infoIt->status;
+ fti.progress = int(progress);
+ fti.total = int(total);
+ fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath());
+ fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
+ return fti;
+#else
return { infoIt->status, int(progress), int(total),
QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath())
};
+#endif
+}
+
+static const auto RegExpOptions =
+ QRegularExpression::CaseInsensitiveOption
+ | QRegularExpression::OptimizeOnFirstUsageOption
+ | QRegularExpression::UseUnicodePropertiesOption;
+
+// regexp is originally taken from Konsole (https://github.com/KDE/konsole)
+// full url:
+// protocolname:// or www. followed by anything other than whitespaces,
+// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :,
+// comma or dot
+// Note: outer parentheses are a part of C++ raw string delimiters, not of
+// the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
+static const QRegularExpression FullUrlRegExp(QStringLiteral(
+ R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
+ ), RegExpOptions);
+// email address:
+// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
+static const QRegularExpression EmailAddressRegExp(QStringLiteral(
+ R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
+ ), RegExpOptions);
+
+/** Converts all that looks like a URL into HTML links */
+static void linkifyUrls(QString& htmlEscapedText)
+{
+ // NOTE: htmlEscapedText is already HTML-escaped (no literal <,>,&)!
+
+ htmlEscapedText.replace(EmailAddressRegExp,
+ QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
+ htmlEscapedText.replace(FullUrlRegExp,
+ QStringLiteral(R"(<a href="\1">\1</a>)"));
+}
+
+QString Room::prettyPrint(const QString& plainText) const
+{
+ auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") +
+ plainText.toHtmlEscaped() + QStringLiteral("</span>");
+ pt.replace('\n', "<br/>");
+
+ linkifyUrls(pt);
+ return pt;
}
QList< User* > Room::usersTyping() const
@@ -613,7 +763,7 @@ void Room::Private::removeMember(User* u)
}
}
-QString Room::roomMembername(User *u) const
+QString Room::roomMembername(const User* u) const
{
// See the CS spec, section 11.2.2.3
@@ -725,17 +875,13 @@ void Room::getPreviousContent(int limit)
void Room::Private::getPreviousContent(int limit)
{
- if( !roomMessagesJob )
+ if( !isJobRunning(roomMessagesJob) )
{
roomMessagesJob =
connection->callApi<RoomMessagesJob>(id, prevBatch, limit);
- connect( roomMessagesJob, &RoomMessagesJob::result, [=] {
- if( !roomMessagesJob->error() )
- {
- addHistoricalMessageEvents(roomMessagesJob->releaseEvents());
- prevBatch = roomMessagesJob->end();
- }
- roomMessagesJob = nullptr;
+ connect( roomMessagesJob, &RoomMessagesJob::success, [=] {
+ prevBatch = roomMessagesJob->end();
+ addHistoricalMessageEvents(roomMessagesJob->releaseEvents());
});
}
}
@@ -818,11 +964,14 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
return;
}
auto* fileInfo = event->content()->fileInfo();
+ auto safeTempPrefix = eventId;
+ safeTempPrefix.replace(':', '_');
+ safeTempPrefix = QDir::tempPath() + '/' + safeTempPrefix + '#';
auto fileName = !localFilename.isEmpty() ? localFilename.toLocalFile() :
!fileInfo->originalName.isEmpty() ?
- (QDir::tempPath() + '/' + fileInfo->originalName) :
+ (safeTempPrefix + fileInfo->originalName) :
!event->plainBody().isEmpty() ?
- (QDir::tempPath() + '/' + event->plainBody()) : QString();
+ (safeTempPrefix + event->plainBody()) : QString();
auto job = connection()->downloadFile(fileInfo->url, fileName);
if (isJobRunning(job))
{
@@ -1003,8 +1152,8 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
processRedaction(move(r));
if (insertedSize > 0)
{
- checkUnreadMessages(timeline.cend() - insertedSize);
emit q->addedMessages();
+ checkUnreadMessages(timeline.cend() - insertedSize);
}
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
@@ -1016,7 +1165,7 @@ void Room::Private::checkUnreadMessages(timeline_iter_t from)
const auto newUnreadMessages = count_if(from, timeline.cend(),
std::bind(&Room::Private::isEventNotable, this, _1));
- // The first event in the just-added batch (referred to by upTo.base())
+ // The first event in the just-added batch (referred to by `from`)
// defines whose read marker can possibly be promoted any further over
// the same author's events newly arrived. Others will need explicit
// read receipts from the server (or, for the local user,
@@ -1096,6 +1245,7 @@ void Room::processStateEvents(const RoomEvents& events)
case EventType::RoomCanonicalAlias: {
auto aliasEvent = static_cast<RoomCanonicalAliasEvent*>(event);
d->canonicalAlias = aliasEvent->alias();
+ setObjectName(d->canonicalAlias);
qCDebug(MAIN) << "Room canonical alias updated:" << d->canonicalAlias;
emitNamesChanged = true;
break;
@@ -1329,7 +1479,9 @@ QJsonObject Room::Private::toJson() const
QJsonObject roomStateObj;
roomStateObj.insert("events", stateEvents);
- result.insert("state", roomStateObj);
+ result.insert(
+ joinState == JoinState::Invite ? "invite_state" : "state",
+ roomStateObj);
}
if (!q->readMarkerEventId().isEmpty())
@@ -1361,9 +1513,12 @@ QJsonObject Room::Private::toJson() const
}
QJsonObject unreadNotificationsObj;
- unreadNotificationsObj.insert("highlight_count", highlightCount);
- unreadNotificationsObj.insert("notification_count", notificationCount);
- result.insert("unread_notifications", unreadNotificationsObj);
+ if (highlightCount > 0)
+ unreadNotificationsObj.insert("highlight_count", highlightCount);
+ if (notificationCount > 0)
+ unreadNotificationsObj.insert("notification_count", notificationCount);
+ if (!unreadNotificationsObj.isEmpty())
+ result.insert("unread_notifications", unreadNotificationsObj);
return result;
}
diff --git a/room.h b/room.h
index f5bf0839..b908a763 100644
--- a/room.h
+++ b/room.h
@@ -40,7 +40,6 @@ namespace QMatrixClient
class MemberSorter;
class LeaveRoomJob;
class RedactEventJob;
- class Room;
class TimelineItem
{
@@ -105,6 +104,17 @@ namespace QMatrixClient
Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
+ Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
+ Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
+
+ Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
+ Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged)
+ Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
+
+ Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged)
+ Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged)
+ Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged)
+
Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved)
public:
using Timeline = std::deque<TimelineItem>;
@@ -114,6 +124,8 @@ namespace QMatrixClient
Room(Connection* connection, QString id, JoinState initialJoinState);
~Room() override;
+ // Property accessors
+
Connection* connection() const;
User* localUser() const;
const QString& id() const;
@@ -122,14 +134,16 @@ namespace QMatrixClient
QString canonicalAlias() const;
QString displayName() const;
QString topic() const;
+ QString avatarMediaId() const;
+ QUrl avatarUrl() const;
Q_INVOKABLE JoinState joinState() const;
Q_INVOKABLE QList<User*> usersTyping() const;
QList<User*> membersLeft() const;
Q_INVOKABLE QList<User*> users() const;
- Q_INVOKABLE QStringList memberNames() const;
- Q_INVOKABLE int memberCount() const;
- Q_INVOKABLE int timelineSize() const;
+ QStringList memberNames() const;
+ int memberCount() const;
+ int timelineSize() const;
/**
* Returns a square room avatar with the given size and requests it
@@ -150,7 +164,7 @@ namespace QMatrixClient
* @brief Produces a disambiguated name for a given user in
* the context of the room
*/
- Q_INVOKABLE QString roomMembername(User* u) const;
+ Q_INVOKABLE QString roomMembername(const User* u) const;
/**
* @brief Produces a disambiguated name for a user with this id in
* the context of the room
@@ -170,6 +184,17 @@ namespace QMatrixClient
rev_iter_t findInTimeline(TimelineItem::index_t index) const;
rev_iter_t findInTimeline(const QString& evtId) const;
+ bool displayed() const;
+ void setDisplayed(bool displayed = true);
+ QString firstDisplayedEventId() const;
+ rev_iter_t firstDisplayedMarker() const;
+ void setFirstDisplayedEventId(const QString& eventId);
+ void setFirstDisplayedEvent(TimelineItem::index_t index);
+ QString lastDisplayedEventId() const;
+ rev_iter_t lastDisplayedMarker() const;
+ void setLastDisplayedEventId(const QString& eventId);
+ void setLastDisplayedEvent(TimelineItem::index_t index);
+
rev_iter_t readMarker(const User* user) const;
rev_iter_t readMarker() const;
QString readMarkerEventId() const;
@@ -192,6 +217,11 @@ namespace QMatrixClient
Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+ /** Pretty-prints plain text into HTML
+ * This includes HTML escaping of <,>,",& and URLs linkification.
+ */
+ QString prettyPrint(const QString& plainText) const;
+
MemberSorter memberSorter() const;
QJsonObject toJson() const;
@@ -245,10 +275,17 @@ namespace QMatrixClient
void userAdded(User* user);
void userRemoved(User* user);
void memberRenamed(User* user);
+ void memberListChanged();
+
void joinStateChanged(JoinState oldState, JoinState newState);
void typingChanged();
+
void highlightCountChanged(Room* room);
void notificationCountChanged(Room* room);
+
+ void displayedChanged(bool displayed);
+ void firstDisplayedEventChanged();
+ void lastDisplayedEventChanged();
void lastReadEventChanged(User* user);
void readMarkerMoved();
void unreadMessagesChanged(Room* room);
diff --git a/user.cpp b/user.cpp
index baa7bc45..b0890b61 100644
--- a/user.cpp
+++ b/user.cpp
@@ -51,7 +51,9 @@ class User::Private
User::User(QString userId, Connection* connection)
: QObject(connection), d(new Private(std::move(userId), connection))
-{ }
+{
+ setObjectName(userId);
+}
User::~User()
{
@@ -74,6 +76,7 @@ void User::updateName(const QString& newName)
if (oldName != newName)
{
d->name = newName;
+ setObjectName(displayname());
emit nameChanged(newName, oldName);
}
}
@@ -127,7 +130,7 @@ QString User::bridged() const {
return d->bridged;
}
-const Avatar& User::avatarObject()
+const Avatar& User::avatarObject() const
{
return d->avatar;
}
@@ -142,6 +145,11 @@ QImage User::avatar(int width, int height)
return d->avatar.get(width, height, [=] { emit avatarChanged(this); });
}
+QString User::avatarMediaId() const
+{
+ return d->avatar.mediaId();
+}
+
QUrl User::avatarUrl() const
{
return d->avatar.url();
diff --git a/user.h b/user.h
index 91dfdc09..8a2c53d9 100644
--- a/user.h
+++ b/user.h
@@ -33,6 +33,8 @@ namespace QMatrixClient
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false)
Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false)
+ Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
+ Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
public:
User(QString userId, Connection* connection);
~User() override;
@@ -57,11 +59,12 @@ namespace QMatrixClient
*/
QString bridged() const;
- const Avatar& avatarObject();
+ const Avatar& avatarObject() const;
Q_INVOKABLE QImage avatar(int dimension);
Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight);
- Q_INVOKABLE QUrl avatarUrl() const;
+ QString avatarMediaId() const;
+ QUrl avatarUrl() const;
void processEvent(Event* event);
@@ -83,3 +86,4 @@ namespace QMatrixClient
Private* d;
};
}
+Q_DECLARE_METATYPE(QMatrixClient::User*)