summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2020-11-15 04:52:49 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2020-11-25 19:05:12 +0100
commita31d3d08165646738d6ae624ac4eff6971207058 (patch)
tree15e3140a8086a105d7bb38ebee6ed771eeaa06c3 /src/timeline
parentBasic text input in qml (diff)
downloadnheko-a31d3d08165646738d6ae624ac4eff6971207058.tar.xz
Add file uploading
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/InputBar.cpp317
-rw-r--r--src/timeline/InputBar.h41
-rw-r--r--src/timeline/TimelineModel.h2
3 files changed, 358 insertions, 2 deletions
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index dcd4a106..bd8f6414 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -1,18 +1,27 @@
 #include "InputBar.h"
 
 #include <QClipboard>
+#include <QFileDialog>
 #include <QGuiApplication>
 #include <QMimeData>
+#include <QMimeDatabase>
+#include <QStandardPaths>
+#include <QUrl>
 
 #include <mtx/responses/common.hpp>
+#include <mtx/responses/media.hpp>
 
 #include "Cache.h"
 #include "ChatPage.h"
 #include "Logging.h"
 #include "MatrixClient.h"
+#include "Olm.h"
 #include "TimelineModel.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "dialogs/PreviewUploadOverlay.h"
+
+#include "blurhash.hpp"
 
 static constexpr size_t INPUT_HISTORY_SIZE = 10;
 
@@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse)
         if (!md)
                 return;
 
-        if (md->hasImage()) {
+        nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString());
+        const auto formats = md->formats().filter("/");
+        const auto image   = formats.filter("image/", Qt::CaseInsensitive);
+        const auto audio   = formats.filter("audio/", Qt::CaseInsensitive);
+        const auto video   = formats.filter("video/", Qt::CaseInsensitive);
+
+        if (!image.empty() && md->hasImage()) {
+                showPreview(*md, "", image);
+        } else if (!audio.empty()) {
+                showPreview(*md, "", audio);
+        } else if (!video.empty()) {
+                showPreview(*md, "", video);
+        } else if (md->hasUrls()) {
+                // Generic file path for any platform.
+                QString path;
+                for (auto &&u : md->urls()) {
+                        if (u.isLocalFile()) {
+                                path = u.toLocalFile();
+                                break;
+                        }
+                }
+
+                if (!path.isEmpty() && QFileInfo{path}.exists()) {
+                        showPreview(*md, path, formats);
+                } else {
+                        nhlog::ui()->warn("Clipboard does not contain any valid file paths.");
+                }
+        } else if (md->hasFormat("x-special/gnome-copied-files")) {
+                // Special case for X11 users. See "Notes for X11 Users" in md.
+                // Source: http://doc.qt.io/qt-5/qclipboard.html
+
+                // This MIME type returns a string with multiple lines separated by '\n'. The first
+                // line is the command to perform with the clipboard (not useful to us). The
+                // following lines are the file URIs.
+                //
+                // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
+                // nautilus_clipboard_get_uri_list_from_selection_data()
+                // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
+
+                auto data = md->data("x-special/gnome-copied-files").split('\n');
+                if (data.size() < 2) {
+                        nhlog::ui()->warn("MIME format is malformed, cannot perform paste.");
+                        return;
+                }
+
+                QString path;
+                for (int i = 1; i < data.size(); ++i) {
+                        QUrl url{data[i]};
+                        if (url.isLocalFile()) {
+                                path = url.toLocalFile();
+                                break;
+                        }
+                }
+
+                if (!path.isEmpty()) {
+                        showPreview(*md, path, formats);
+                } else {
+                        nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}",
+                                          data.join(", ").toStdString());
+                }
         } else if (md->hasText()) {
                 emit insertText(md->text());
         } else {
@@ -79,6 +147,37 @@ InputBar::send()
 }
 
 void
+InputBar::openFileSelection()
+{
+        const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
+        const auto fileName      = QFileDialog::getOpenFileName(
+          ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
+
+        if (fileName.isEmpty())
+                return;
+
+        QMimeDatabase db;
+        QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
+
+        QFile file{fileName};
+
+        if (!file.open(QIODevice::ReadOnly)) {
+                emit ChatPage::instance()->showNotification(
+                  QString("Error while reading media: %1").arg(file.errorString()));
+                return;
+        }
+
+        setUploading(true);
+
+        auto bin = file.readAll();
+
+        QMimeData data;
+        data.setData(mime.name(), bin);
+
+        showPreview(data, fileName, QStringList{mime.name()});
+}
+
+void
 InputBar::message(QString msg)
 {
         mtx::events::msg::Text text = {};
@@ -150,6 +249,112 @@ InputBar::emote(QString msg)
 }
 
 void
+InputBar::image(const QString &filename,
+                const std::optional<mtx::crypto::EncryptedFile> &file,
+                const QString &url,
+                const QString &mime,
+                uint64_t dsize,
+                const QSize &dimensions,
+                const QString &blurhash)
+{
+        mtx::events::msg::Image image;
+        image.info.mimetype = mime.toStdString();
+        image.info.size     = dsize;
+        image.info.blurhash = blurhash.toStdString();
+        image.body          = filename.toStdString();
+        image.info.h        = dimensions.height();
+        image.info.w        = dimensions.width();
+
+        if (file)
+                image.file = file;
+        else
+                image.url = url.toStdString();
+
+        if (!room->reply().isEmpty()) {
+                image.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                room->resetReply();
+        }
+
+        room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
+}
+
+void
+InputBar::file(const QString &filename,
+               const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+               const QString &url,
+               const QString &mime,
+               uint64_t dsize)
+{
+        mtx::events::msg::File file;
+        file.info.mimetype = mime.toStdString();
+        file.info.size     = dsize;
+        file.body          = filename.toStdString();
+
+        if (encryptedFile)
+                file.file = encryptedFile;
+        else
+                file.url = url.toStdString();
+
+        if (!room->reply().isEmpty()) {
+                file.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                room->resetReply();
+        }
+
+        room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
+}
+
+void
+InputBar::audio(const QString &filename,
+                const std::optional<mtx::crypto::EncryptedFile> &file,
+                const QString &url,
+                const QString &mime,
+                uint64_t dsize)
+{
+        mtx::events::msg::Audio audio;
+        audio.info.mimetype = mime.toStdString();
+        audio.info.size     = dsize;
+        audio.body          = filename.toStdString();
+        audio.url           = url.toStdString();
+
+        if (file)
+                audio.file = file;
+        else
+                audio.url = url.toStdString();
+
+        if (!room->reply().isEmpty()) {
+                audio.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                room->resetReply();
+        }
+
+        room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
+}
+
+void
+InputBar::video(const QString &filename,
+                const std::optional<mtx::crypto::EncryptedFile> &file,
+                const QString &url,
+                const QString &mime,
+                uint64_t dsize)
+{
+        mtx::events::msg::Video video;
+        video.info.mimetype = mime.toStdString();
+        video.info.size     = dsize;
+        video.body          = filename.toStdString();
+
+        if (file)
+                video.file = file;
+        else
+                video.url = url.toStdString();
+
+        if (!room->reply().isEmpty()) {
+                video.relates_to.in_reply_to.event_id = room->reply().toStdString();
+                room->resetReply();
+        }
+
+        room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
+}
+
+void
 InputBar::command(QString command, QString args)
 {
         if (command == "me") {
@@ -196,3 +401,113 @@ InputBar::command(QString command, QString args)
                 cache::dropOutboundMegolmSession(room->roomId().toStdString());
         }
 }
+
+void
+InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats)
+{
+        dialogs::PreviewUploadOverlay *previewDialog_ =
+          new dialogs::PreviewUploadOverlay(ChatPage::instance());
+        previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
+
+        if (source.hasImage())
+                previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()),
+                                           formats.front());
+        else if (!path.isEmpty())
+                previewDialog_->setPreview(path);
+        else if (!formats.isEmpty()) {
+                auto mime = formats.first();
+                previewDialog_->setPreview(source.data(mime), mime);
+        } else {
+                setUploading(false);
+                previewDialog_->deleteLater();
+                return;
+        }
+
+        connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() {
+                setUploading(false);
+        });
+
+        connect(
+          previewDialog_,
+          &dialogs::PreviewUploadOverlay::confirmUpload,
+          this,
+          [this](const QByteArray data, const QString &mime, const QString &fn) {
+                  setUploading(true);
+
+                  auto payload = std::string(data.data(), data.size());
+                  std::optional<mtx::crypto::EncryptedFile> encryptedFile;
+                  if (cache::isRoomEncrypted(room->roomId().toStdString())) {
+                          mtx::crypto::BinaryBuf buf;
+                          std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
+                          payload                      = mtx::crypto::to_string(buf);
+                  }
+
+                  QSize dimensions;
+                  QString blurhash;
+                  auto mimeClass = mime.split("/")[0];
+                  if (mimeClass == "image") {
+                          QImage img = utils::readImage(&data);
+
+                          dimensions = img.size();
+                          if (img.height() > 200 && img.width() > 360)
+                                  img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
+                          std::vector<unsigned char> data;
+                          for (int y = 0; y < img.height(); y++) {
+                                  for (int x = 0; x < img.width(); x++) {
+                                          auto p = img.pixel(x, y);
+                                          data.push_back(static_cast<unsigned char>(qRed(p)));
+                                          data.push_back(static_cast<unsigned char>(qGreen(p)));
+                                          data.push_back(static_cast<unsigned char>(qBlue(p)));
+                                  }
+                          }
+                          blurhash = QString::fromStdString(
+                            blurhash::encode(data.data(), img.width(), img.height(), 4, 3));
+                  }
+
+                  http::client()->upload(
+                    payload,
+                    encryptedFile ? "application/octet-stream" : mime.toStdString(),
+                    QFileInfo(fn).fileName().toStdString(),
+                    [this,
+                     filename      = fn,
+                     encryptedFile = std::move(encryptedFile),
+                     mimeClass,
+                     mime,
+                     size = payload.size(),
+                     dimensions,
+                     blurhash](const mtx::responses::ContentURI &res,
+                               mtx::http::RequestErr err) mutable {
+                            if (err) {
+                                    emit ChatPage::instance()->showNotification(
+                                      tr("Failed to upload media. Please try again."));
+                                    nhlog::net()->warn("failed to upload media: {} {} ({})",
+                                                       err->matrix_error.error,
+                                                       to_string(err->matrix_error.errcode),
+                                                       static_cast<int>(err->status_code));
+                                    setUploading(false);
+                                    return;
+                            }
+
+                            auto url = QString::fromStdString(res.content_uri);
+                            if (encryptedFile)
+                                    encryptedFile->url = res.content_uri;
+
+                            if (mimeClass == "image")
+                                    image(filename,
+                                          encryptedFile,
+                                          url,
+                                          mime,
+                                          size,
+                                          dimensions,
+                                          blurhash);
+                            else if (mimeClass == "audio")
+                                    audio(filename, encryptedFile, url, mime, size);
+                            else if (mimeClass == "video")
+                                    video(filename, encryptedFile, url, mime, size);
+                            else
+                                    file(filename, encryptedFile, url, mime, size);
+
+                            setUploading(false);
+                    });
+          });
+}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index f3a38c2e..35e3f8a4 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -3,11 +3,17 @@
 #include <QObject>
 #include <deque>
 
+#include <mtx/common.hpp>
+#include <mtx/responses/messages.hpp>
+
 class TimelineModel;
+class QMimeData;
+class QStringList;
 
 class InputBar : public QObject
 {
         Q_OBJECT
+        Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
 
 public:
         InputBar(TimelineModel *parent)
@@ -19,18 +25,53 @@ public slots:
         void send();
         void paste(bool fromMouse);
         void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
+        void openFileSelection();
+        bool uploading() const { return uploading_; }
 
 signals:
         void insertText(QString text);
+        void uploadingChanged(bool value);
 
 private:
         void message(QString body);
         void emote(QString body);
         void command(QString name, QString args);
+        void image(const QString &filename,
+                   const std::optional<mtx::crypto::EncryptedFile> &file,
+                   const QString &url,
+                   const QString &mime,
+                   uint64_t dsize,
+                   const QSize &dimensions,
+                   const QString &blurhash);
+        void file(const QString &filename,
+                  const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+                  const QString &url,
+                  const QString &mime,
+                  uint64_t dsize);
+        void audio(const QString &filename,
+                   const std::optional<mtx::crypto::EncryptedFile> &file,
+                   const QString &url,
+                   const QString &mime,
+                   uint64_t dsize);
+        void video(const QString &filename,
+                   const std::optional<mtx::crypto::EncryptedFile> &file,
+                   const QString &url,
+                   const QString &mime,
+                   uint64_t dsize);
+
+        void showPreview(const QMimeData &source, QString path, const QStringList &formats);
+        void setUploading(bool value)
+        {
+                if (value != uploading_) {
+                        uploading_ = value;
+                        emit uploadingChanged(value);
+                }
+        }
 
         TimelineModel *room;
         QString text;
         std::deque<QString> history_;
         std::size_t history_index_ = 0;
         int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
+        bool uploading_ = false;
 };
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 58a1496c..16a2565e 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -150,7 +150,7 @@ class TimelineModel : public QAbstractListModel
         Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
         Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
         Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
-        Q_PROPERTY(InputBar *input READ input)
+        Q_PROPERTY(InputBar *input READ input CONSTANT)
 
 public:
         explicit TimelineModel(TimelineViewManager *manager,