From fdb76bb5c1bdce765479442a70ddca80b867caa6 Mon Sep 17 00:00:00 2001
From: Konstantinos Sideris <sideris.konstantin@gmail.com>
Date: Wed, 29 Nov 2017 23:39:35 +0200
Subject: [PATCH] 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 1c47689c5..ebb181119 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 80dc9df9a..b0f6993d2 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 88706e4a3..80c16740b 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 b94acbdbd..9646405cc 100644
--- a/include/TimelineItem.h
+++ b/include/TimelineItem.h
@@ -17,12 +17,14 @@
 
 #pragma once
 
+#include <QDateTime>
 #include <QHBoxLayout>
 #include <QLabel>
 #include <QPainter>
 #include <QStyle>
 #include <QStyleOption>
 
+#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<msgs::Image> &e,
@@ -83,6 +87,12 @@ protected:
 private:
         void init();
 
+        template<class Widget>
+        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<class Widget>
+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 e3bedff08..715d8a9af 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -17,26 +17,28 @@
 
 #pragma once
 
+#include <QApplication>
 #include <QLayout>
 #include <QList>
 #include <QQueue>
 #include <QScrollArea>
+#include <QSettings>
 #include <QStyle>
 #include <QStyleOption>
 
 #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<class Widget, events::MessageEventType MsgType>
         void addUserMessage(const QString &url, const QString &filename);
         void updatePendingMessage(int txn_id, QString event_id);
         void scrollDown();
@@ -193,3 +197,28 @@ private:
         QList<PendingMessage> pending_sent_msgs_;
         QSharedPointer<MatrixClient> client_;
 };
+
+template<class Widget, events::MessageEventType MsgType>
+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 d9fb730e5..854c25507 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 82e694a10..5214d49a4 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -187,6 +187,10 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> 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<MatrixClient> 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 cd9347836..96fd9c07a 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<MatrixClient> client,
-                   const events::MessageEvent<msgs::File> &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<QString> 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<MatrixClient> client,
         connect(client_.data(), &MatrixClient::fileDownloaded, this, &FileItem::fileDownloaded);
 }
 
+FileItem::FileItem(QSharedPointer<MatrixClient> client,
+                   const events::MessageEvent<msgs::File> &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<MatrixClient> client,
                    const QString &url,
                    const QString &filename,
@@ -70,25 +75,9 @@ FileItem::FileItem(QSharedPointer<MatrixClient> 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<QString> 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 a171cd093..66630c80b 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -21,6 +21,7 @@
 #include <QJsonArray>
 #include <QJsonDocument>
 #include <QJsonObject>
+#include <QMimeDatabase>
 #include <QNetworkReply>
 #include <QNetworkRequest>
 #include <QPixmap>
@@ -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 829784fd9..c4d013574 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 b57f51182..7297abc36 100644
--- a/src/TimelineItem.cc
+++ b/src/TimelineItem.cc
@@ -15,20 +15,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <QDateTime>
 #include <QFontDatabase>
 #include <QRegExp>
 #include <QSettings>
 #include <QTextEdit>
 
 #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 = "<a href=\"\\1\">\\1</a>";
@@ -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<ImageItem>(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<FileItem>(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 bdc59af3b..346ecc522 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -18,7 +18,6 @@
 #include <QApplication>
 #include <QDebug>
 #include <QFileInfo>
-#include <QSettings>
 #include <QTimer>
 
 #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 1f047d7c2..daec481bd 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -22,6 +22,8 @@
 #include <QFileInfo>
 #include <QSettings>
 
+#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<ImageItem, matrix::events::MessageEventType::Image>(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<FileItem, matrix::events::MessageEventType::File>(url, filename);
 }
 
 void
-- 
GitLab