aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2019-01-06 00:12:05 +0900
committerGitHub <noreply@github.com>2019-01-06 00:12:05 +0900
commit9c08bbce341081a8ccbe0fccf48658b3e75e02cf (patch)
tree171ff3fcf35acb2c4a707488e6f43a2e35fff56f
parentf545d181ade8736dfda93e8abb34ab93ac34e931 (diff)
parent27555e44dfbaae26a0e030cb3c22eb00ba8371f0 (diff)
downloadlibquotient-9c08bbce341081a8ccbe0fccf48658b3e75e02cf.tar.gz
libquotient-9c08bbce341081a8ccbe0fccf48658b3e75e02cf.zip
Merge pull request #272 from QMatrixClient/kitsune-upload-attachments
Support of attachments uploading
-rw-r--r--.travis.yml1
-rw-r--r--CMakeLists.txt8
-rw-r--r--examples/CMakeLists.txt2
-rw-r--r--examples/qmc-example.cpp151
-rw-r--r--lib/connection.cpp15
-rw-r--r--lib/connection.h6
-rw-r--r--lib/eventitem.cpp26
-rw-r--r--lib/eventitem.h15
-rw-r--r--lib/events/eventcontent.cpp32
-rw-r--r--lib/events/eventcontent.h11
-rw-r--r--lib/events/roommessageevent.cpp65
-rw-r--r--lib/events/roommessageevent.h21
-rw-r--r--lib/events/stateevent.h6
-rw-r--r--lib/room.cpp187
-rw-r--r--lib/room.h25
-rw-r--r--libqmatrixclient.pri2
16 files changed, 496 insertions, 77 deletions
diff --git a/.travis.yml b/.travis.yml
index b515b8fb..fc143a62 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ addons:
packages:
- g++-5
- qt57base
+ - qt57multimedia
- valgrind
homebrew:
packages:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8a3193a4..c48a7ba9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,7 +40,9 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu
endif ()
endforeach ()
-find_package(Qt5 5.4.1 REQUIRED Network Gui)
+# Qt 5.6+ is the formal requirement but for the sake of supporting UBPorts
+# upstream Qt 5.4 is required.
+find_package(Qt5 5.4.1 REQUIRED Network Gui Multimedia)
get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE)
if (GTAD_PATH)
@@ -140,7 +142,7 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS}
${libqmatrixclient_job_SRCS} ${libqmatrixclient_csdef_SRCS}
${libqmatrixclient_cswellknown_SRCS}
${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS})
-set(API_VERSION "0.4")
+set(API_VERSION "0.5")
set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0")
set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} )
set_property(TARGET QMatrixClient PROPERTY
@@ -152,7 +154,7 @@ target_include_directories(QMatrixClient PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
-target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui)
+target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia)
add_executable(qmc-example ${example_SRCS})
target_link_libraries(qmc-example Qt5::Core QMatrixClient)
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 49e0089a..cd5e15ed 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -45,7 +45,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu
endif ()
endforeach ()
-find_package(Qt5 5.6 REQUIRED Network Gui)
+find_package(Qt5 5.6 REQUIRED Network Gui Multimedia)
get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE)
find_package(QMatrixClient REQUIRED)
diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
index 894167a9..8fbf4824 100644
--- a/examples/qmc-example.cpp
+++ b/examples/qmc-example.cpp
@@ -9,6 +9,8 @@
#include <QtCore/QCoreApplication>
#include <QtCore/QStringBuilder>
#include <QtCore/QTimer>
+#include <QtCore/QTemporaryFile>
+#include <QtCore/QFileInfo>
#include <iostream>
#include <functional>
@@ -20,7 +22,7 @@ using namespace std::placeholders;
class QMCTest : public QObject
{
public:
- QMCTest(Connection* conn, QString targetRoomName, QString source);
+ QMCTest(Connection* conn, QString testRoomName, QString source);
private slots:
void setupAndRun();
@@ -29,6 +31,9 @@ class QMCTest : public QObject
void doTests();
void loadMembers();
void sendMessage();
+ void sendFile();
+ void checkFileSendingOutcome(const QString& txnId,
+ const QString& fileName);
void addAndRemoveTag();
void sendAndRedact();
void checkRedactionOutcome(const QString& evtIdToRedact,
@@ -47,6 +52,8 @@ class QMCTest : public QObject
QString origin;
QString targetRoomName;
Room* targetRoom = nullptr;
+
+ bool validatePendingEvent(const QString& txnId);
};
#define QMC_CHECK(description, condition) \
@@ -69,6 +76,14 @@ class QMCTest : public QObject
} \
}
+bool QMCTest::validatePendingEvent(const QString& txnId)
+{
+ auto it = targetRoom->findPendingEvent(txnId);
+ return it != targetRoom->pendingEvents().end() &&
+ it->deliveryStatus() == EventStatus::Submitted &&
+ (*it)->transactionId() == txnId;
+}
+
QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source)
: c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName))
{
@@ -142,7 +157,8 @@ void QMCTest::run()
{
// TODO: Waiting for proper futures to come so that it could be:
// targetRoom->postPlainText(origin % ": All tests finished")
-// .then(this, &QMCTest::leave);
+// .then(this, &QMCTest::leave); // Qt-style
+// .then([this] { leave(); }); // STL-style
auto txnId =
targetRoom->postPlainText(origin % ": All tests finished");
connect(targetRoom, &Room::messageSent, this,
@@ -166,6 +182,7 @@ void QMCTest::doTests()
return;
sendMessage();
+ sendFile();
addAndRemoveTag();
sendAndRedact();
markDirectChat();
@@ -206,19 +223,133 @@ void QMCTest::sendMessage()
running.push_back("Message sending");
cout << "Sending a message" << endl;
auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here");
- auto& pending = targetRoom->pendingEvents();
- if (pending.empty())
+ if (!validatePendingEvent(txnId))
{
+ cout << "Invalid pending event right after submitting" << endl;
QMC_CHECK("Message sending", false);
return;
}
- auto it = std::find_if(pending.begin(), pending.end(),
- [&txnId] (const auto& e) {
- return e->transactionId() == txnId;
+
+ QMetaObject::Connection sc;
+ sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this,
+ [this,sc,txnId] (const RoomEvent* evt, int pendingIdx) {
+ const auto& pendingEvents = targetRoom->pendingEvents();
+ Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
+
+ if (evt->transactionId() != txnId)
+ return;
+
+ disconnect(sc);
+
+ QMC_CHECK("Message sending",
+ is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() &&
+ pendingEvents[size_t(pendingIdx)]->transactionId()
+ == evt->transactionId());
+ });
+}
+
+void QMCTest::sendFile()
+{
+ running.push_back("File sending");
+ cout << "Sending a file" << endl;
+ auto* tf = new QTemporaryFile;
+ if (!tf->open())
+ {
+ cout << "Failed to create a temporary file" << endl;
+ QMC_CHECK("File sending", false);
+ return;
+ }
+ tf->write("Test");
+ tf->close();
+ // QFileInfo::fileName brings only the file name; QFile::fileName brings
+ // the full path
+ const auto tfName = QFileInfo(*tf).fileName();
+ cout << "Sending file" << tfName.toStdString() << endl;
+ const auto txnId = targetRoom->postFile("Test file",
+ QUrl::fromLocalFile(tf->fileName()));
+ if (!validatePendingEvent(txnId))
+ {
+ cout << "Invalid pending event right after submitting" << endl;
+ QMC_CHECK("File sending", false);
+ delete tf;
+ return;
+ }
+
+ QMetaObject::Connection scCompleted, scFailed;
+ scCompleted = connect(targetRoom, &Room::fileTransferCompleted, this,
+ [this,txnId,tf,tfName,scCompleted,scFailed] (const QString& id) {
+ auto fti = targetRoom->fileTransferInfo(id);
+ Q_ASSERT(fti.status == FileTransferInfo::Completed);
+
+ if (id != txnId)
+ return;
+
+ disconnect(scCompleted);
+ disconnect(scFailed);
+ delete tf;
+
+ checkFileSendingOutcome(txnId, tfName);
+ });
+ scFailed = connect(targetRoom, &Room::fileTransferFailed, this,
+ [this,txnId,tf,scCompleted,scFailed]
+ (const QString& id, const QString& error) {
+ if (id != txnId)
+ return;
+
+ targetRoom->postPlainText(origin % ": File upload failed: " % error);
+ disconnect(scCompleted);
+ disconnect(scFailed);
+ delete tf;
+
+ QMC_CHECK("File sending", false);
+ });
+}
+
+void QMCTest::checkFileSendingOutcome(const QString& txnId,
+ const QString& fileName)
+{
+ auto it = targetRoom->findPendingEvent(txnId);
+ if (it == targetRoom->pendingEvents().end())
+ {
+ cout << "Pending file event dropped before upload completion"
+ << endl;
+ QMC_CHECK("File sending", false);
+ return;
+ }
+ if (it->deliveryStatus() != EventStatus::FileUploaded)
+ {
+ cout << "Pending file event status upon upload completion is "
+ << it->deliveryStatus() << " != FileUploaded("
+ << EventStatus::FileUploaded << ')' << endl;
+ QMC_CHECK("File sending", false);
+ return;
+ }
+
+ QMetaObject::Connection sc;
+ sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this,
+ [this,sc,txnId,fileName] (const RoomEvent* evt, int pendingIdx) {
+ const auto& pendingEvents = targetRoom->pendingEvents();
+ Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
+
+ if (evt->transactionId() != txnId)
+ return;
+
+ cout << "Event " << txnId.toStdString()
+ << " arrived in the timeline" << endl;
+ disconnect(sc);
+ visit(*evt,
+ [&] (const RoomMessageEvent& e) {
+ QMC_CHECK("File sending",
+ !e.id().isEmpty() &&
+ pendingEvents[size_t(pendingIdx)]
+ ->transactionId() == txnId &&
+ e.hasFileContent() &&
+ e.content()->fileInfo()->originalName == fileName);
+ },
+ [this] (const RoomEvent&) {
+ QMC_CHECK("File sending", false);
});
- QMC_CHECK("Message sending", it != pending.end());
- // TODO: Wait when it actually gets sent; check that it obtained an id
- // Independently, check when it shows up in the timeline.
+ });
}
void QMCTest::addAndRemoveTag()
diff --git a/lib/connection.cpp b/lib/connection.cpp
index a16bc753..c17cbffc 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -43,6 +43,7 @@
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
#include <QtCore/QRegularExpression>
+#include <QtCore/QMimeDatabase>
#include <QtCore/QCoreApplication>
using namespace QMatrixClient;
@@ -466,13 +467,21 @@ MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
}
UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
- const QString& filename, const QString& contentType) const
+ const QString& filename, const QString& overrideContentType) const
{
+ auto contentType = overrideContentType;
+ if (contentType.isEmpty())
+ {
+ contentType =
+ QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource)
+ .name();
+ contentSource->open(QIODevice::ReadOnly);
+ }
return callApi<UploadContentJob>(contentSource, filename, contentType);
}
UploadContentJob* Connection::uploadFile(const QString& fileName,
- const QString& contentType)
+ const QString& overrideContentType)
{
auto sourceFile = new QFile(fileName);
if (!sourceFile->open(QIODevice::ReadOnly))
@@ -482,7 +491,7 @@ UploadContentJob* Connection::uploadFile(const QString& fileName,
return nullptr;
}
return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
- contentType);
+ overrideContentType);
}
GetContentJob* Connection::getContent(const QString& mediaId) const
diff --git a/lib/connection.h b/lib/connection.h
index 9a94aad6..ff3e2028 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -402,10 +402,10 @@ namespace QMatrixClient
// QIODevice* should already be open
UploadContentJob* uploadContent(QIODevice* contentSource,
- const QString& filename = {},
- const QString& contentType = {}) const;
+ const QString& filename = {},
+ const QString& overrideContentType = {}) const;
UploadContentJob* uploadFile(const QString& fileName,
- const QString& contentType = {});
+ const QString& overrideContentType = {});
GetContentJob* getContent(const QString& mediaId) const;
GetContentJob* getContent(const QUrl& url) const;
// If localFilename is empty, a temporary file will be created
diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp
index 79ef769c..8ec3fe48 100644
--- a/lib/eventitem.cpp
+++ b/lib/eventitem.cpp
@@ -17,3 +17,29 @@
*/
#include "eventitem.h"
+
+#include "events/roommessageevent.h"
+#include "events/roomavatarevent.h"
+
+using namespace QMatrixClient;
+
+void PendingEventItem::setFileUploaded(const QUrl& remoteUrl)
+{
+ // TODO: eventually we might introduce hasFileContent to RoomEvent,
+ // and unify the code below.
+ if (auto* rme = getAs<RoomMessageEvent>())
+ {
+ Q_ASSERT(rme->hasFileContent());
+ rme->editContent([remoteUrl] (EventContent::TypedBase& ec) {
+ ec.fileInfo()->url = remoteUrl;
+ });
+ }
+ if (auto* rae = getAs<RoomAvatarEvent>())
+ {
+ Q_ASSERT(rae->content().fileInfo());
+ rae->editContent([remoteUrl] (EventContent::FileInfo& fi) {
+ fi.url = remoteUrl;
+ });
+ }
+ setStatus(EventStatus::FileUploaded);
+}
diff --git a/lib/eventitem.h b/lib/eventitem.h
index 5f1d10c9..36ed2132 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -33,16 +33,17 @@ namespace QMatrixClient
/** Special marks an event can assume
*
* This is used to hint at a special status of some events in UI.
- * Most status values are mutually exclusive.
+ * All values except Redacted and Hidden are mutually exclusive.
*/
enum Code {
Normal = 0x0, //< No special designation
Submitted = 0x01, //< The event has just been submitted for sending
- Departed = 0x02, //< The event has left the client
- ReachedServer = 0x03, //< The server has received the event
- SendingFailed = 0x04, //< The server could not receive the event
+ FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server
+ Departed = 0x03, //< The event has left the client
+ ReachedServer = 0x04, //< The server has received the event
+ SendingFailed = 0x05, //< The server could not receive the event
Redacted = 0x08, //< The event has been redacted
- Hidden = 0x10, //< The event should be hidden
+ Hidden = 0x10, //< The event should not be shown in the timeline
};
Q_DECLARE_FLAGS(Status, Code)
Q_FLAG(Status)
@@ -70,6 +71,9 @@ namespace QMatrixClient
return std::exchange(evt, move(other));
}
+ protected:
+ template <typename EventT>
+ EventT* getAs() { return eventCast<EventT>(evt); }
private:
RoomEventPtr evt;
};
@@ -116,6 +120,7 @@ namespace QMatrixClient
QString annotation() const { return _annotation; }
void setDeparted() { setStatus(EventStatus::Departed); }
+ void setFileUploaded(const QUrl& remoteUrl);
void setReachedServer(const QString& eventId)
{
setStatus(EventStatus::ReachedServer);
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index a6b1c763..9a5e872c 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -17,6 +17,8 @@
*/
#include "eventcontent.h"
+
+#include "converters.h"
#include "util.h"
#include <QtCore/QMimeDatabase>
@@ -30,7 +32,7 @@ QJsonObject Base::toJson() const
return o;
}
-FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType,
+FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType,
const QString& originalFilename)
: mimeType(mimeType), url(u), payloadSize(payloadSize)
, originalName(originalFilename)
@@ -41,7 +43,7 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
: originalInfoJson(infoJson)
, mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
, url(u)
- , payloadSize(infoJson["size"_ls].toInt())
+ , payloadSize(fromJson<qint64>(infoJson["size"_ls]))
, originalName(originalFilename)
{
if (!mimeType.isValid())
@@ -51,13 +53,15 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
void FileInfo::fillInfoJson(QJsonObject* infoJson) const
{
Q_ASSERT(infoJson);
- infoJson->insert(QStringLiteral("size"), payloadSize);
- infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
+ if (payloadSize != -1)
+ infoJson->insert(QStringLiteral("size"), payloadSize);
+ if (mimeType.isValid())
+ infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
}
-ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType,
- const QSize& imageSize)
- : FileInfo(u, fileSize, mimeType), imageSize(imageSize)
+ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType,
+ const QSize& imageSize, const QString& originalFilename)
+ : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize)
{ }
ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
@@ -69,8 +73,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
{
FileInfo::fillInfoJson(infoJson);
- infoJson->insert(QStringLiteral("w"), imageSize.width());
- infoJson->insert(QStringLiteral("h"), imageSize.height());
+ if (imageSize.width() != -1)
+ infoJson->insert(QStringLiteral("w"), imageSize.width());
+ if (imageSize.height() != -1)
+ infoJson->insert(QStringLiteral("h"), imageSize.height());
}
Thumbnail::Thumbnail(const QJsonObject& infoJson)
@@ -80,7 +86,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson)
void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
{
- infoJson->insert(QStringLiteral("thumbnail_url"), url.toString());
- infoJson->insert(QStringLiteral("thumbnail_info"),
- toInfoJson<ImageInfo>(*this));
+ if (url.isValid())
+ infoJson->insert(QStringLiteral("thumbnail_url"), url.toString());
+ if (!imageSize.isEmpty())
+ infoJson->insert(QStringLiteral("thumbnail_info"),
+ toInfoJson<ImageInfo>(*this));
}
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index ea321fb6..0588c0e2 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -88,7 +88,7 @@ namespace QMatrixClient
class FileInfo
{
public:
- explicit FileInfo(const QUrl& u, int payloadSize = -1,
+ explicit FileInfo(const QUrl& u, qint64 payloadSize = -1,
const QMimeType& mimeType = {},
const QString& originalFilename = {});
FileInfo(const QUrl& u, const QJsonObject& infoJson,
@@ -109,7 +109,7 @@ namespace QMatrixClient
QJsonObject originalInfoJson;
QMimeType mimeType;
QUrl url;
- int payloadSize;
+ qint64 payloadSize;
QString originalName;
};
@@ -127,9 +127,10 @@ namespace QMatrixClient
class ImageInfo : public FileInfo
{
public:
- explicit ImageInfo(const QUrl& u, int fileSize = -1,
+ explicit ImageInfo(const QUrl& u, qint64 fileSize = -1,
QMimeType mimeType = {},
- const QSize& imageSize = {});
+ const QSize& imageSize = {},
+ const QString& originalFilename = {});
ImageInfo(const QUrl& u, const QJsonObject& infoJson,
const QString& originalFilename = {});
@@ -167,6 +168,7 @@ namespace QMatrixClient
explicit TypedBase(const QJsonObject& o = {}) : Base(o) { }
virtual QMimeType type() const = 0;
virtual const FileInfo* fileInfo() const { return nullptr; }
+ virtual FileInfo* fileInfo() { return nullptr; }
virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
};
@@ -196,6 +198,7 @@ namespace QMatrixClient
QMimeType type() const override { return InfoT::mimeType; }
const FileInfo* fileInfo() const override { return this; }
+ FileInfo* fileInfo() override { return this; }
protected:
void fillJson(QJsonObject* json) const override
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index 1c5cf058..c3007fa0 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -21,6 +21,9 @@
#include "logging.h"
#include <QtCore/QMimeDatabase>
+#include <QtCore/QFileInfo>
+#include <QtGui/QImageReader>
+#include <QtMultimedia/QMediaResource>
using namespace QMatrixClient;
using namespace EventContent;
@@ -71,8 +74,8 @@ MsgType jsonToMsgType(const QString& matrixType)
return MsgType::Unknown;
}
-inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType,
- TypedBase* content)
+QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
+ const QString& jsonMsgType, TypedBase* content)
{
auto json = content ? content->toJson() : QJsonObject();
json.insert(QStringLiteral("msgtype"), jsonMsgType);
@@ -86,7 +89,7 @@ static const auto BodyKey = "body"_ls;
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
const QString& jsonMsgType, TypedBase* content)
: RoomEvent(typeId(), matrixTypeId(),
- toMsgJson(plainBody, jsonMsgType, content))
+ assembleContentJson(plainBody, jsonMsgType, content))
, _content(content)
{ }
@@ -95,6 +98,40 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
: RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
{ }
+TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
+{
+ auto filePath = file.absoluteFilePath();
+ auto localUrl = QUrl::fromLocalFile(filePath);
+ auto mimeType = QMimeDatabase().mimeTypeForFile(file);
+ if (!asGenericFile)
+ {
+ auto mimeTypeName = mimeType.name();
+ if (mimeTypeName.startsWith("image/"))
+ return new ImageContent(localUrl, file.size(), mimeType,
+ QImageReader(filePath).size(),
+ file.fileName());
+
+ // duration can only be obtained asynchronously and can only be reliably
+ // done by starting to play the file. Left for a future implementation.
+ if (mimeTypeName.startsWith("video/"))
+ return new VideoContent(localUrl, file.size(), mimeType,
+ QMediaResource(localUrl).resolution(),
+ file.fileName());
+
+ if (mimeTypeName.startsWith("audio/"))
+ return new AudioContent(localUrl, file.size(), mimeType,
+ file.fileName());
+ }
+ return new FileContent(localUrl, file.size(), mimeType, file.fileName());
+}
+
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
+ const QFileInfo& file, bool asGenericFile)
+ : RoomMessageEvent(plainBody,
+ asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file),
+ contentFromFile(file, asGenericFile))
+{ }
+
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
: RoomEvent(typeId(), obj), _content(nullptr)
{
@@ -162,6 +199,25 @@ bool RoomMessageEvent::hasThumbnail() const
return content() && content()->thumbnailInfo();
}
+QString rawMsgTypeForMimeType(const QMimeType& mimeType)
+{
+ auto name = mimeType.name();
+ return name.startsWith("image/") ? QStringLiteral("m.image") :
+ name.startsWith("video/") ? QStringLiteral("m.video") :
+ name.startsWith("audio/") ? QStringLiteral("m.audio") :
+ QStringLiteral("m.file");
+}
+
+QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url)
+{
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url));
+}
+
+QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
+{
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi));
+}
+
TextContent::TextContent(const QString& text, const QString& contentType)
: mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text)
{
@@ -200,7 +256,8 @@ void TextContent::fillJson(QJsonObject* json) const
}
}
-LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail)
+LocationContent::LocationContent(const QString& geoUri,
+ const Thumbnail& thumbnail)
: geoUri(geoUri), thumbnail(thumbnail)
{ }
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index 4c29a93e..d5b570f5 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -21,6 +21,8 @@
#include "roomevent.h"
#include "eventcontent.h"
+class QFileInfo;
+
namespace QMatrixClient
{
namespace MessageEventContent = EventContent; // Back-compatibility
@@ -49,6 +51,9 @@ namespace QMatrixClient
explicit RoomMessageEvent(const QString& plainBody,
MsgType msgType = MsgType::Text,
EventContent::TypedBase* content = nullptr);
+ explicit RoomMessageEvent(const QString& plainBody,
+ const QFileInfo& file,
+ bool asGenericFile = false);
explicit RoomMessageEvent(const QJsonObject& obj);
MsgType msgtype() const;
@@ -56,14 +61,27 @@ namespace QMatrixClient
QString plainBody() const;
EventContent::TypedBase* content() const
{ return _content.data(); }
+ template <typename VisitorT>
+ void editContent(VisitorT visitor)
+ {
+ visitor(*_content);
+ editJson()[ContentKeyL] =
+ assembleContentJson(plainBody(), rawMsgtype(), content());
+ }
QMimeType mimeType() const;
bool hasTextContent() const;
bool hasFileContent() const;
bool hasThumbnail() const;
+ static QString rawMsgTypeForUrl(const QUrl& url);
+ static QString rawMsgTypeForFile(const QFileInfo& fi);
+
private:
QScopedPointer<EventContent::TypedBase> _content;
+ static QJsonObject assembleContentJson(const QString& plainBody,
+ const QString& jsonMsgType, EventContent::TypedBase* content);
+
REGISTER_ENUM(MsgType)
};
REGISTER_EVENT_TYPE(RoomMessageEvent)
@@ -112,7 +130,7 @@ namespace QMatrixClient
{
public:
LocationContent(const QString& geoUri,
- const ImageInfo& thumbnail);
+ const Thumbnail& thumbnail = {});
explicit LocationContent(const QJsonObject& json);
QMimeType type() const override;
@@ -132,6 +150,7 @@ namespace QMatrixClient
class PlayableContent : public ContentT
{
public:
+ using ContentT::ContentT;
PlayableContent(const QJsonObject& json)
: ContentT(json)
, duration(ContentT::originalInfoJson["duration"_ls].toInt())
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index d82de7e1..d488c0a0 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -88,6 +88,12 @@ namespace QMatrixClient {
}
const ContentT& content() const { return _content; }
+ template <typename VisitorT>
+ void editContent(VisitorT&& visitor)
+ {
+ visitor(_content);
+ editJson()[ContentKeyL] = _content.toJson();
+ }
[[deprecated("Use prevContent instead")]]
const ContentT* prev_content() const { return prevContent(); }
const ContentT* prevContent() const
diff --git a/lib/room.cpp b/lib/room.cpp
index 156b5b1f..8f50607f 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -53,6 +53,7 @@
#include <QtCore/QPointer>
#include <QtCore/QDir>
#include <QtCore/QTemporaryFile>
+#include <QtCore/QMimeDatabase>
#include <array>
#include <functional>
@@ -67,9 +68,11 @@ using std::llround;
enum EventsPlacement : int { Older = -1, Newer = 1 };
-// A workaround for MSVC 2015 that fails with "error C2440: 'return':
-// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'"
-#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4)
+// A workaround for MSVC 2015 and older GCC's that don't handle initializer
+// lists right (MSVC 2015, notably, fails with "error C2440: 'return':
+// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'")
+#if (defined(_MSC_VER) && _MSC_VER < 1910) || \
+ (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4)
# define WORKAROUND_EXTENDED_INITIALIZER_LIST
#endif
@@ -126,15 +129,17 @@ class Room::Private
struct FileTransferPrivateInfo
{
-#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
FileTransferPrivateInfo() = default;
- FileTransferPrivateInfo(BaseJob* j, QString fileName)
- : job(j), localFileInfo(fileName)
+ FileTransferPrivateInfo(BaseJob* j, const QString& fileName,
+ bool isUploading = false)
+ : status(FileTransferInfo::Started), job(j)
+ , localFileInfo(fileName), isUpload(isUploading)
{ }
-#endif
+
+ FileTransferInfo::Status status = FileTransferInfo::None;
QPointer<BaseJob> job = nullptr;
QFileInfo localFileInfo { };
- FileTransferInfo::Status status = FileTransferInfo::Started;
+ bool isUpload = false;
qint64 progress = 0;
qint64 total = -1;
@@ -234,6 +239,8 @@ class Room::Private
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ RoomEvent* addAsPending(RoomEventPtr&& event);
+
QString doSendEvent(const RoomEvent* pEvent);
PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr);
void onEventSendingFailure(const RoomEvent* pEvent,
@@ -592,6 +599,19 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
return timelineEdge();
}
+Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
+{
+ return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
+Room::PendingEvents::const_iterator
+Room::findPendingEvent(const QString& txnId) const
+{
+ return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
@@ -909,7 +929,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
return fileName;
}
-QUrl Room::urlToThumbnail(const QString& eventId)
+QUrl Room::urlToThumbnail(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
if (event->hasThumbnail())
@@ -923,7 +943,7 @@ QUrl Room::urlToThumbnail(const QString& eventId)
return {};
}
-QUrl Room::urlToDownload(const QString& eventId)
+QUrl Room::urlToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
{
@@ -935,7 +955,7 @@ QUrl Room::urlToDownload(const QString& eventId)
return {};
}
-QString Room::fileNameToDownload(const QString& eventId)
+QString Room::fileNameToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
return d->fileNameToDownload(event);
@@ -969,13 +989,28 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
return fti;
#else
- return { infoIt->status, int(progress), int(total),
+ return { infoIt->status, infoIt->isUpload, int(progress), int(total),
QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath())
};
#endif
}
+QUrl Room::fileSource(const QString& id) const
+{
+ auto url = urlToDownload(id);
+ if (url.isValid())
+ return url;
+
+ // No urlToDownload means it's a pending or completed upload.
+ auto infoIt = d->fileTransfers.find(id);
+ if (infoIt != d->fileTransfers.end())
+ return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
+
+ qCWarning(MAIN) << "File source for identifier" << id << "not found";
+ return {};
+}
+
QString Room::prettyPrint(const QString& plainText) const
{
return QMatrixClient::prettyPrint(plainText);
@@ -1255,7 +1290,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
}
}
-QString Room::Private::sendEvent(RoomEventPtr&& event)
+RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
{
if (event->transactionId().isEmpty())
event->setTransactionId(connection->generateTxnId());
@@ -1263,7 +1298,12 @@ QString Room::Private::sendEvent(RoomEventPtr&& event)
emit q->pendingEventAboutToAdd(pEvent);
unsyncedEvents.emplace_back(move(event));
emit q->pendingEventAdded();
- return doSendEvent(pEvent);
+ return pEvent;
+}
+
+QString Room::Private::sendEvent(RoomEventPtr&& event)
+{
+ return doSendEvent(addAsPending(std::move(event)));
}
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
@@ -1336,10 +1376,35 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
QString Room::retryMessage(const QString& txnId)
{
- auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
- [txnId] (const auto& evt) { return evt->transactionId() == txnId; });
+ const auto it = findPendingEvent(txnId);
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Retrying transaction" << txnId;
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCDebug(MAIN) << "File for transaction" << txnId
+ << "has already been uploaded, bypassing re-upload";
+ } else {
+ if (isJobRunning(transferIt->job))
+ {
+ qCDebug(MAIN) << "Abandoning the upload job for transaction"
+ << txnId << "and starting again";
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload will be retried"));
+ }
+ uploadFile(txnId,
+ QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath()));
+ // FIXME: Content type is no more passed here but it should
+ }
+ }
+ if (it->deliveryStatus() == EventStatus::ReachedServer)
+ {
+ qCWarning(MAIN) << "The previous attempt has reached the server; two"
+ " events are likely to be in the timeline after retry";
+ }
it->resetStatus();
return d->doSendEvent(it->event());
}
@@ -1350,7 +1415,22 @@ void Room::discardMessage(const QString& txnId)
[txnId] (const auto& evt) { return evt->transactionId() == txnId; });
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Discarding transaction" << txnId;
- emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin());
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (isJobRunning(transferIt->job))
+ {
+ transferIt->status = FileTransferInfo::Cancelled;
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ } else if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCWarning(MAIN) << "File for transaction" << txnId
+ << "has been uploaded but the message was discarded";
+ }
+ }
+ emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin()));
d->unsyncedEvents.erase(it);
emit pendingEventDiscarded();
}
@@ -1377,6 +1457,61 @@ QString Room::postHtmlText(const QString& plainText, const QString& html)
return postHtmlMessage(plainText, html, MessageEventType::Text);
}
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+ // Remote URL will only be known after upload; fill in the local path
+ // to enable the preview while the event is pending.
+ auto* pEvent = d->addAsPending(
+ makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile));
+ const auto txnId = pEvent->transactionId();
+ uploadFile(txnId, localPath);
+ QMetaObject::Connection cCompleted, cCancelled;
+ cCompleted = connect(this, &Room::fileTransferCompleted, this,
+ [cCompleted,cCancelled,this,pEvent,txnId]
+ (const QString& id, QUrl, const QUrl& mxcUri) {
+ if (id == txnId)
+ {
+ auto it = d->findAsPending(pEvent);
+ if (it != d->unsyncedEvents.end())
+ {
+ it->setFileUploaded(mxcUri);
+ emit pendingEventChanged(
+ int(it - d->unsyncedEvents.begin()));
+ d->doSendEvent(pEvent);
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN) << "File uploaded to" << mxcUri
+ << "but the event referring to it was cancelled";
+ }
+ disconnect(cCompleted);
+ disconnect(cCancelled);
+ }
+ });
+ cCancelled = connect(this, &Room::fileTransferCancelled, this,
+ [cCompleted,cCancelled,this,pEvent,txnId] (const QString& id) {
+ if (id == txnId)
+ {
+ auto it = d->findAsPending(pEvent);
+ if (it != d->unsyncedEvents.end())
+ {
+ emit pendingEventAboutToDiscard(
+ int(it - d->unsyncedEvents.begin()));
+ d->unsyncedEvents.erase(it);
+ emit pendingEventDiscarded();
+ }
+ disconnect(cCompleted);
+ disconnect(cCancelled);
+ }
+ });
+
+ return txnId;
+}
+
QString Room::postEvent(RoomEvent* event)
{
if (usesEncryption())
@@ -1532,7 +1667,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
auto job = connection()->uploadFile(fileName, overrideContentType);
if (isJobRunning(job))
{
- d->fileTransfers.insert(id, { job, fileName });
+ d->fileTransfers.insert(id, { job, fileName, true });
connect(job, &BaseJob::uploadProgress, this,
[this,id] (qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
@@ -1555,8 +1690,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
if (ongoingTransfer != d->fileTransfers.end() &&
ongoingTransfer->status == FileTransferInfo::Started)
{
- qCWarning(MAIN) << "Download for" << eventId
- << "already started; to restart, cancel it first";
+ qCWarning(MAIN) << "Transfer for" << eventId
+ << "is ongoing; download won't start";
return;
}
@@ -1819,11 +1954,15 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
break;
it = nextPending + 1;
- emit q->pendingEventAboutToMerge(nextPending->get(),
- nextPendingPair.second - unsyncedEvents.begin());
+ auto* nextPendingEvt = nextPending->get();
+ emit q->pendingEventAboutToMerge(nextPendingEvt,
+ int(nextPendingPair.second - unsyncedEvents.begin()));
qDebug(EVENTS) << "Merging pending event from transaction"
- << (*nextPending)->transactionId() << "into"
- << (*nextPending)->id();
+ << nextPendingEvt->transactionId() << "into"
+ << nextPendingEvt->id();
+ auto transfer = fileTransfers.take(nextPendingEvt->transactionId());
+ if (transfer.status != FileTransferInfo::None)
+ fileTransfers.insert(nextPendingEvt->id(), transfer);
unsyncedEvents.erase(nextPendingPair.second);
if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer))
{
diff --git a/lib/room.h b/lib/room.h
index 6384b706..029f87b7 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -42,10 +42,17 @@ namespace QMatrixClient
class SetRoomStateWithKeyJob;
class RedactEventJob;
+ /** The data structure used to expose file transfer information to views
+ *
+ * This is specifically tuned to work with QML exposing all traits as
+ * Q_PROPERTY values.
+ */
class FileTransferInfo
{
Q_GADGET
+ Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT)
Q_PROPERTY(bool active READ active CONSTANT)
+ Q_PROPERTY(bool started READ started CONSTANT)
Q_PROPERTY(bool completed READ completed CONSTANT)
Q_PROPERTY(bool failed READ failed CONSTANT)
Q_PROPERTY(int progress MEMBER progress CONSTANT)
@@ -53,16 +60,17 @@ namespace QMatrixClient
Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT)
Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT)
public:
- enum Status { None, Started, Completed, Failed };
+ enum Status { None, Started, Completed, Failed, Cancelled };
Status status = None;
+ bool isUpload = false;
int progress = 0;
int total = -1;
QUrl localDir { };
QUrl localPath { };
- bool active() const
- { return status == Started || status == Completed; }
+ bool started() const { return status == Started; }
bool completed() const { return status == Completed; }
+ bool active() const { return started() || completed(); }
bool failed() const { return status == Failed; }
};
@@ -226,6 +234,8 @@ namespace QMatrixClient
rev_iter_t findInTimeline(TimelineItem::index_t index) const;
rev_iter_t findInTimeline(const QString& evtId) const;
+ PendingEvents::iterator findPendingEvent(const QString & txnId);
+ PendingEvents::const_iterator findPendingEvent(const QString & txnId) const;
bool displayed() const;
/// Mark the room as currently displayed to the user
@@ -334,10 +344,11 @@ namespace QMatrixClient
/// Get the list of users this room is a direct chat with
QList<User*> directChatUsers() const;
- Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId);
- Q_INVOKABLE QUrl urlToDownload(const QString& eventId);
- Q_INVOKABLE QString fileNameToDownload(const QString& eventId);
+ Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
+ Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
+ Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const;
Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+ Q_INVOKABLE QUrl fileSource(const QString& id) const;
/** Pretty-prints plain text into HTML
* As of now, it's exactly the same as QMatrixClient::prettyPrint();
@@ -365,6 +376,8 @@ namespace QMatrixClient
QString postHtmlMessage(const QString& plainText,
const QString& html, MessageEventType type);
QString postHtmlText(const QString& plainText, const QString& html);
+ QString postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile = false);
/** Post a pre-created room message event
*
* Takes ownership of the event, deleting it once the matching one
diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri
index 8ca43e56..eefaec67 100644
--- a/libqmatrixclient.pri
+++ b/libqmatrixclient.pri
@@ -1,4 +1,4 @@
-QT += network
+QT += network multimedia
CONFIG += c++14 warn_on rtti_off create_prl object_parallel_to_source
win32-msvc* {