From fdb76bb5c1bdce765479442a70ddca80b867caa6 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Wed, 29 Nov 2017 23:39:35 +0200 Subject: Implement file uploads fixes #24 --- include/FileItem.h | 1 + include/MatrixClient.h | 2 ++ include/TextInputWidget.h | 1 + include/TimelineItem.h | 43 +++++++++++++++++++++++++++++++ include/TimelineView.h | 33 ++++++++++++++++++++++-- include/TimelineViewManager.h | 1 + src/ChatPage.cc | 11 ++++++++ src/FileItem.cc | 47 +++++++++++++-------------------- src/MatrixClient.cc | 60 +++++++++++++++++++++++++++++++++++++++++++ src/TextInputWidget.cc | 35 ++++++++++++------------- src/TimelineItem.cc | 33 ++++++------------------ src/TimelineView.cc | 27 +------------------ src/TimelineViewManager.cc | 19 +++++++++++++- 13 files changed, 211 insertions(+), 102 deletions(-) diff --git a/include/FileItem.h b/include/FileItem.h index 1c47689c..ebb18111 100644 --- a/include/FileItem.h +++ b/include/FileItem.h @@ -80,6 +80,7 @@ private slots: private: QString calculateFileSize(int nbytes) const; void openUrl(); + void init(); QUrl url_; QString text_; diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 80dc9df9..b0f6993d 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -56,6 +56,7 @@ public: void downloadFile(const QString &event_id, const QUrl &url); void messages(const QString &room_id, const QString &from_token, int limit = 30) noexcept; void uploadImage(const QString &roomid, const QString &filename); + void uploadFile(const QString &roomid, const QString &filename); void joinRoom(const QString &roomIdOrAlias); void leaveRoom(const QString &roomId); void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000); @@ -92,6 +93,7 @@ signals: const QString &token); void versionSuccess(); void imageUploaded(const QString &roomid, const QString &filename, const QString &url); + void fileUploaded(const QString &roomid, const QString &filename, const QString &url); void roomAvatarRetrieved(const QString &roomid, const QPixmap &img); void userAvatarRetrieved(const QString &userId, const QImage &img); diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h index 88706e4a..80c16740 100644 --- a/include/TextInputWidget.h +++ b/include/TextInputWidget.h @@ -85,6 +85,7 @@ signals: void sendTextMessage(QString msg); void sendEmoteMessage(QString msg); void uploadImage(QString filename); + void uploadFile(QString filename); void sendJoinRoomRequest(const QString &room); void startedTyping(); diff --git a/include/TimelineItem.h b/include/TimelineItem.h index b94acbdb..9646405c 100644 --- a/include/TimelineItem.h +++ b/include/TimelineItem.h @@ -17,12 +17,14 @@ #pragma once +#include #include #include #include #include #include +#include "AvatarProvider.h" #include "Emote.h" #include "File.h" #include "Image.h" @@ -30,6 +32,7 @@ #include "Notice.h" #include "RoomInfoListItem.h" #include "Text.h" +#include "TimelineViewManager.h" class ImageItem; class FileItem; @@ -61,6 +64,7 @@ public: QWidget *parent = 0); // m.image TimelineItem(ImageItem *item, const QString &userid, bool withSender, QWidget *parent = 0); + TimelineItem(FileItem *item, const QString &userid, bool withSender, QWidget *parent = 0); TimelineItem(ImageItem *img, const events::MessageEvent &e, @@ -83,6 +87,12 @@ protected: private: void init(); + template + void setupLocalWidgetLayout(Widget *widget, + const QString &userid, + const QString &msgDescription, + bool withSender); + void generateBody(const QString &body); void generateBody(const QString &userid, const QString &body); void generateTimestamp(const QDateTime &time); @@ -110,3 +120,36 @@ private: QLabel *userName_; QLabel *body_; }; + +template +void +TimelineItem::setupLocalWidgetLayout(Widget *widget, + const QString &userid, + const QString &msgDescription, + bool withSender) +{ + auto displayName = TimelineViewManager::displayName(userid); + auto timestamp = QDateTime::currentDateTime(); + + descriptionMsg_ = { + "You", userid, QString(" %1").arg(msgDescription), descriptiveTime(timestamp)}; + + generateTimestamp(timestamp); + + auto widgetLayout = new QHBoxLayout(); + widgetLayout->setContentsMargins(0, 5, 0, 0); + widgetLayout->addWidget(widget); + widgetLayout->addStretch(1); + + if (withSender) { + generateBody(displayName, ""); + setupAvatarLayout(displayName); + mainLayout_->addLayout(headerLayout_); + + AvatarProvider::resolve(userid, this); + } else { + setupSimpleLayout(); + } + + mainLayout_->addLayout(widgetLayout); +} diff --git a/include/TimelineView.h b/include/TimelineView.h index e3bedff0..715d8a9a 100644 --- a/include/TimelineView.h +++ b/include/TimelineView.h @@ -17,26 +17,28 @@ #pragma once +#include #include #include #include #include +#include #include #include #include "Emote.h" #include "File.h" #include "Image.h" +#include "MatrixClient.h" #include "MessageEvent.h" #include "Notice.h" #include "Text.h" +#include "TimelineItem.h" class FloatingButton; -class MatrixClient; class RoomMessages; class ScrollBar; class Timeline; -class TimelineItem; struct DescInfo; namespace msgs = matrix::events::messages; @@ -102,6 +104,8 @@ public: // Add new events at the end of the timeline. int addEvents(const Timeline &timeline); void addUserMessage(matrix::events::MessageEventType ty, const QString &msg); + + template void addUserMessage(const QString &url, const QString &filename); void updatePendingMessage(int txn_id, QString event_id); void scrollDown(); @@ -193,3 +197,28 @@ private: QList pending_sent_msgs_; QSharedPointer client_; }; + +template +void +TimelineView::addUserMessage(const QString &url, const QString &filename) +{ + QSettings settings; + auto user_id = settings.value("auth/user_id").toString(); + auto with_sender = lastSender_ != user_id; + + auto widget = new Widget(client_, url, filename, this); + + TimelineItem *view_item = new TimelineItem(widget, user_id, with_sender, scroll_widget_); + scroll_layout_->addWidget(view_item); + + lastMessageDirection_ = TimelineDirection::Bottom; + + QApplication::processEvents(); + + lastSender_ = user_id; + + int txn_id = client_->incrementTransactionId(); + + PendingMessage message(MsgType, txn_id, url, filename, "", view_item); + handleNewUserMessage(message); +} diff --git a/include/TimelineViewManager.h b/include/TimelineViewManager.h index d9fb730e..854c2550 100644 --- a/include/TimelineViewManager.h +++ b/include/TimelineViewManager.h @@ -67,6 +67,7 @@ public slots: void queueTextMessage(const QString &msg); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, const QString &url); + void queueFileMessage(const QString &roomid, const QString &filename, const QString &url); private slots: void messageSent(const QString &eventid, const QString &roomid, int txnid); diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 82e694a1..5214d49a 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -187,6 +187,10 @@ ChatPage::ChatPage(QSharedPointer client, QWidget *parent) client_->uploadImage(current_room_, filename); }); + connect(text_input_, &TextInputWidget::uploadFile, this, [=](QString filename) { + client_->uploadFile(current_room_, filename); + }); + connect(client_.data(), &MatrixClient::joinFailed, this, &ChatPage::showNotification); connect(client_.data(), &MatrixClient::imageUploaded, @@ -195,6 +199,13 @@ ChatPage::ChatPage(QSharedPointer client, QWidget *parent) text_input_->hideUploadSpinner(); view_manager_->queueImageMessage(roomid, filename, url); }); + connect(client_.data(), + &MatrixClient::fileUploaded, + this, + [=](QString roomid, QString filename, QString url) { + text_input_->hideUploadSpinner(); + view_manager_->queueFileMessage(roomid, filename, url); + }); connect(client_.data(), SIGNAL(roomAvatarRetrieved(const QString &, const QPixmap &)), diff --git a/src/FileItem.cc b/src/FileItem.cc index cd934783..96fd9c07 100644 --- a/src/FileItem.cc +++ b/src/FileItem.cc @@ -30,25 +30,16 @@ namespace events = matrix::events; namespace msgs = matrix::events::messages; -FileItem::FileItem(QSharedPointer client, - const events::MessageEvent &event, - QWidget *parent) - : QWidget(parent) - , event_{event} - , client_{client} +void +FileItem::init() { setMouseTracking(true); setCursor(Qt::PointingHandCursor); setAttribute(Qt::WA_Hover, true); - url_ = event.msgContent().url(); - text_ = event.content().body(); - readableFileSize_ = calculateFileSize(event.msgContent().info().size); - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); QList url_parts = url_.toString().split("mxc://"); - if (url_parts.size() != 2) { qDebug() << "Invalid format for image" << url_.toString(); return; @@ -61,6 +52,20 @@ FileItem::FileItem(QSharedPointer client, connect(client_.data(), &MatrixClient::fileDownloaded, this, &FileItem::fileDownloaded); } +FileItem::FileItem(QSharedPointer client, + const events::MessageEvent &event, + QWidget *parent) + : QWidget(parent) + , url_{event.msgContent().url()} + , text_{event.content().body()} + , event_{event} + , client_{client} +{ + readableFileSize_ = calculateFileSize(event.msgContent().info().size); + + init(); +} + FileItem::FileItem(QSharedPointer client, const QString &url, const QString &filename, @@ -70,25 +75,9 @@ FileItem::FileItem(QSharedPointer client, , text_{QFileInfo(filename).fileName()} , client_{client} { - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - // TODO: calculateFileSize - /* readableFileSize_ = calculateFileSize(event.msgContent().info().size); */ - - QList url_parts = url_.toString().split("mxc://"); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); + readableFileSize_ = calculateFileSize(QFileInfo(filename).size()); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } - - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2") - .arg(client_.data()->getHomeServer().toString(), media_params); + init(); } QString diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index a171cd09..66630c80 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -287,6 +288,9 @@ MatrixClient::sendRoomMessage(matrix::events::MessageEventType ty, case matrix::events::MessageEventType::Image: body = {{"msgtype", "m.image"}, {"body", msg}, {"url", url}}; break; + case matrix::events::MessageEventType::File: + body = {{"msgtype", "m.file"}, {"body", msg}, {"url", url}}; + break; default: qDebug() << "SendRoomMessage: Unknown message type for" << msg; return; @@ -755,6 +759,62 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename) }); } +void +MatrixClient::uploadFile(const QString &roomid, const QString &filename) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(mediaApiUrl_ + "/upload"); + endpoint.setQuery(query); + + QFile file(filename); + if (!file.open(QIODevice::ReadWrite)) { + qDebug() << "Error while reading" << filename; + return; + } + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(filename, QMimeDatabase::MatchContent); + + QNetworkRequest request(QString(endpoint.toEncoded())); + request.setHeader(QNetworkRequest::ContentLengthHeader, file.size()); + request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name()); + + auto reply = post(request, file.readAll()); + connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + emit syncFailed(reply->errorString()); + return; + } + + auto data = reply->readAll(); + + if (data.isEmpty()) + return; + + auto json = QJsonDocument::fromJson(data); + + if (!json.isObject()) { + qDebug() << "Media upload: Response is not a json object."; + return; + } + + QJsonObject object = json.object(); + if (!object.contains("content_uri")) { + qDebug() << "Media upload: Missing content_uri key"; + qDebug() << object; + return; + } + + emit fileUploaded(roomid, filename, object.value("content_uri").toString()); + }); +} void MatrixClient::joinRoom(const QString &roomIdOrAlias) { diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc index 829784fd..c4d01357 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc @@ -276,30 +276,27 @@ TextInputWidget::command(QString command, QString args) void TextInputWidget::openFileSelection() { - QStringList supportedFiles; - supportedFiles << "jpeg" - << "gif" - << "png" - << "bmp" - << "tiff" - << "webp"; - - auto fileName = QFileDialog::getOpenFileName( - this, - tr("Select an image"), - "", - tr("Image Files (*.bmp *.gif *.jpg *.jpeg *.png *.tiff *.webp)")); + QStringList imageExtensions; + imageExtensions << "jpeg" + << "gif" + << "png" + << "bmp" + << "tiff" + << "webp"; + + auto fileName = + QFileDialog::getOpenFileName(this, tr("Select an file"), "", tr("All Files (*)")); if (fileName.isEmpty()) return; - auto imageFormat = QString(QImageReader::imageFormat(fileName)); - if (!supportedFiles.contains(imageFormat)) { - qDebug() << "Unsupported image format for" << fileName; - return; - } + auto format = QString(QImageReader::imageFormat(fileName)); + + if (imageExtensions.contains(format)) + emit uploadImage(fileName); + else + emit uploadFile(fileName); - emit uploadImage(fileName); showUploadSpinner(); } diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc index b57f5118..7297abc3 100644 --- a/src/TimelineItem.cc +++ b/src/TimelineItem.cc @@ -15,20 +15,17 @@ * along with this program. If not, see . */ -#include #include #include #include #include #include "Avatar.h" -#include "AvatarProvider.h" #include "Config.h" #include "FileItem.h" #include "ImageItem.h" #include "Sync.h" #include "TimelineItem.h" -#include "TimelineViewManager.h" static const QRegExp URL_REGEX("((?:https?|ftp)://\\S+)"); static const QString URL_HTML = "\\1"; @@ -119,29 +116,15 @@ TimelineItem::TimelineItem(ImageItem *image, { init(); - auto displayName = TimelineViewManager::displayName(userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"You", userid, " sent an image", descriptiveTime(timestamp)}; - - generateTimestamp(timestamp); - - auto imageLayout = new QHBoxLayout(); - imageLayout->setMargin(0); - imageLayout->addWidget(image); - imageLayout->addStretch(1); - - if (withSender) { - generateBody(displayName, ""); - setupAvatarLayout(displayName); - mainLayout_->addLayout(headerLayout_); + setupLocalWidgetLayout(image, userid, "sent an image", withSender); +} - AvatarProvider::resolve(userid, this); - } else { - setupSimpleLayout(); - } +TimelineItem::TimelineItem(FileItem *file, const QString &userid, bool withSender, QWidget *parent) + : QWidget{parent} +{ + init(); - mainLayout_->addLayout(imageLayout); + setupLocalWidgetLayout(file, userid, "sent a file", withSender); } /* @@ -169,7 +152,7 @@ TimelineItem::TimelineItem(ImageItem *image, generateTimestamp(timestamp); auto imageLayout = new QHBoxLayout(); - imageLayout->setMargin(0); + imageLayout->setContentsMargins(0, 5, 0, 0); imageLayout->addWidget(image); imageLayout->addStretch(1); diff --git a/src/TimelineView.cc b/src/TimelineView.cc index bdc59af3..346ecc52 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc @@ -18,7 +18,6 @@ #include #include #include -#include #include #include "FileItem.h" @@ -27,7 +26,6 @@ #include "RoomMessages.h" #include "ScrollBar.h" #include "Sync.h" -#include "TimelineItem.h" #include "TimelineView.h" namespace events = matrix::events; @@ -569,30 +567,6 @@ TimelineView::addUserMessage(matrix::events::MessageEventType ty, const QString handleNewUserMessage(message); } -void -TimelineView::addUserMessage(const QString &url, const QString &filename) -{ - QSettings settings; - auto user_id = settings.value("auth/user_id").toString(); - auto with_sender = lastSender_ != user_id; - - auto image = new ImageItem(client_, url, filename, this); - - TimelineItem *view_item = new TimelineItem(image, user_id, with_sender, scroll_widget_); - scroll_layout_->addWidget(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - QApplication::processEvents(); - - lastSender_ = user_id; - - int txn_id = client_->incrementTransactionId(); - PendingMessage message( - matrix::events::MessageEventType::Image, txn_id, url, filename, "", view_item); - handleNewUserMessage(message); -} - void TimelineView::handleNewUserMessage(PendingMessage msg) { @@ -610,6 +584,7 @@ TimelineView::sendNextPendingMessage() PendingMessage &m = pending_msgs_.head(); switch (m.ty) { case matrix::events::MessageEventType::Image: + case matrix::events::MessageEventType::File: client_->sendRoomMessage( m.ty, m.txn_id, room_id_, QFileInfo(m.filename).fileName(), m.body); break; diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc index 1f047d7c..daec481b 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc @@ -22,6 +22,8 @@ #include #include +#include "FileItem.h" +#include "ImageItem.h" #include "MatrixClient.h" #include "Sync.h" #include "TimelineView.h" @@ -92,7 +94,22 @@ TimelineViewManager::queueImageMessage(const QString &roomid, auto view = views_[roomid]; - view->addUserMessage(url, filename); + view->addUserMessage(url, filename); +} + +void +TimelineViewManager::queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url) +{ + if (!views_.contains(roomid)) { + qDebug() << "Cannot send m.file message to a non-managed view"; + return; + } + + auto view = views_[roomid]; + + view->addUserMessage(url, filename); } void -- cgit v1.2.3