summary refs log tree commit diff
path: root/src
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
parentBasic text input in qml (diff)
downloadnheko-a31d3d08165646738d6ae624ac4eff6971207058.tar.xz
Add file uploading
Diffstat (limited to '')
-rw-r--r--src/ChatPage.cpp120
-rw-r--r--src/ChatPage.h11
-rw-r--r--src/TextInputWidget.cpp146
-rw-r--r--src/TextInputWidget.h14
-rw-r--r--src/Utils.cpp5
-rw-r--r--src/Utils.h2
-rw-r--r--src/dialogs/PreviewUploadOverlay.cpp5
-rw-r--r--src/dialogs/PreviewUploadOverlay.h1
-rw-r--r--src/timeline/InputBar.cpp317
-rw-r--r--src/timeline/InputBar.h41
-rw-r--r--src/timeline/TimelineModel.h2
11 files changed, 367 insertions, 297 deletions
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 6e1ed8ca..e09041e7 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
                 this,
                 SIGNAL(unreadMessages(int)));
 
-        connect(
-          text_input_,
-          &TextInputWidget::uploadMedia,
-          this,
-          [this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) {
-                  if (!dev->open(QIODevice::ReadOnly)) {
-                          emit uploadFailed(
-                            QString("Error while reading media: %1").arg(dev->errorString()));
-                          return;
-                  }
-
-                  auto bin = dev->readAll();
-                  QMimeDatabase db;
-                  QMimeType mime = db.mimeTypeForData(bin);
-
-                  auto payload = std::string(bin.data(), bin.size());
-                  std::optional<mtx::crypto::EncryptedFile> encryptedFile;
-                  if (cache::isRoomEncrypted(current_room_.toStdString())) {
-                          mtx::crypto::BinaryBuf buf;
-                          std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
-                          payload                      = mtx::crypto::to_string(buf);
-                  }
-
-                  QSize dimensions;
-                  QString blurhash;
-                  if (mimeClass == "image") {
-                          QImage img = utils::readImage(&bin);
-
-                          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.name().toStdString(),
-                    QFileInfo(fn).fileName().toStdString(),
-                    [this,
-                     room_id  = current_room_,
-                     filename = fn,
-                     encryptedFile,
-                     mimeClass,
-                     mime = mime.name(),
-                     size = payload.size(),
-                     dimensions,
-                     blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
-                            if (err) {
-                                    emit uploadFailed(
-                                      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));
-                                    return;
-                            }
-
-                            emit mediaUploaded(room_id,
-                                               filename,
-                                               encryptedFile,
-                                               QString::fromStdString(res.content_uri),
-                                               mimeClass,
-                                               mime,
-                                               size,
-                                               dimensions,
-                                               blurhash);
-                    });
-          });
-
-        connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
-                text_input_->hideUploadSpinner();
-                emit showNotification(msg);
-        });
-        connect(this,
-                &ChatPage::mediaUploaded,
-                this,
-                [this](QString roomid,
-                       QString filename,
-                       std::optional<mtx::crypto::EncryptedFile> encryptedFile,
-                       QString url,
-                       QString mimeClass,
-                       QString mime,
-                       qint64 dsize,
-                       QSize dimensions,
-                       QString blurhash) {
-                        text_input_->hideUploadSpinner();
-
-                        if (encryptedFile)
-                                encryptedFile->url = url.toStdString();
-
-                        if (mimeClass == "image")
-                                view_manager_->queueImageMessage(roomid,
-                                                                 filename,
-                                                                 encryptedFile,
-                                                                 url,
-                                                                 mime,
-                                                                 dsize,
-                                                                 dimensions,
-                                                                 blurhash);
-                        else if (mimeClass == "audio")
-                                view_manager_->queueAudioMessage(
-                                  roomid, filename, encryptedFile, url, mime, dsize);
-                        else if (mimeClass == "video")
-                                view_manager_->queueVideoMessage(
-                                  roomid, filename, encryptedFile, url, mime, dsize);
-                        else
-                                view_manager_->queueFileMessage(
-                                  roomid, filename, encryptedFile, url, mime, dsize);
-                });
-
         connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() {
                 if (callManager_->onActiveCall()) {
                         callManager_->hangUp();
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 9a38fd6e..41c6f276 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -126,17 +126,6 @@ signals:
         void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
                                         const QPoint widgetPos);
 
-        void uploadFailed(const QString &msg);
-        void mediaUploaded(const QString &roomid,
-                           const QString &filename,
-                           const std::optional<mtx::crypto::EncryptedFile> &file,
-                           const QString &url,
-                           const QString &mimeClass,
-                           const QString &mime,
-                           qint64 dsize,
-                           const QSize &dimensions,
-                           const QString &blurhash);
-
         void contentLoaded();
         void closing();
         void changeWindowTitle(const int);
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index dec7a574..232c0cad 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -88,10 +88,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
         typingTimer_->setSingleShot(true);
 
         connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
-        connect(&previewDialog_,
-                &dialogs::PreviewUploadOverlay::confirmUpload,
-                this,
-                &FilteredTextEdit::uploadData);
 
         connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
         connect(
@@ -355,81 +351,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
         }
 }
 
-bool
-FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const
-{
-        return (source->hasImage() || QTextEdit::canInsertFromMimeData(source));
-}
-
-void
-FilteredTextEdit::insertFromMimeData(const QMimeData *source)
-{
-        qInfo() << "Got mime formats: \n" << source->formats();
-        const auto formats = source->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() && source->hasImage()) {
-                QImage img = qvariant_cast<QImage>(source->imageData());
-                previewDialog_.setPreview(img, image.front());
-        } else if (!audio.empty()) {
-                showPreview(source, audio);
-        } else if (!video.empty()) {
-                showPreview(source, video);
-        } else if (source->hasUrls()) {
-                // Generic file path for any platform.
-                QString path;
-                for (auto &&u : source->urls()) {
-                        if (u.isLocalFile()) {
-                                path = u.toLocalFile();
-                                break;
-                        }
-                }
-
-                if (!path.isEmpty() && QFileInfo{path}.exists()) {
-                        previewDialog_.setPreview(path);
-                } else {
-                        qWarning()
-                          << "Clipboard does not contain any valid file paths:" << source->urls();
-                }
-        } else if (source->hasFormat("x-special/gnome-copied-files")) {
-                // Special case for X11 users. See "Notes for X11 Users" in source.
-                // 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 = source->data("x-special/gnome-copied-files").split('\n');
-                if (data.size() < 2) {
-                        qWarning() << "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()) {
-                        previewDialog_.setPreview(path);
-                } else {
-                        qWarning() << "Clipboard does not contain any valid file paths:" << data;
-                }
-        } else {
-                QTextEdit::insertFromMimeData(source);
-        }
-}
-
 void
 FilteredTextEdit::stopTyping()
 {
@@ -494,28 +415,6 @@ FilteredTextEdit::textChanged()
         working_history_[history_index_] = toPlainText();
 }
 
-void
-FilteredTextEdit::uploadData(const QByteArray data,
-                             const QString &mediaType,
-                             const QString &filename)
-{
-        QSharedPointer<QBuffer> buffer{new QBuffer{this}};
-        buffer->setData(data);
-
-        emit startedUpload();
-
-        emit media(buffer, mediaType, filename);
-}
-
-void
-FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats)
-{
-        // Retrieve data as MIME type.
-        auto const &mime = formats.first();
-        QByteArray data  = source->data(mime);
-        previewDialog_.setPreview(data, mime);
-}
-
 TextInputWidget::TextInputWidget(QWidget *parent)
   : QWidget(parent)
 {
@@ -624,7 +523,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
 #endif
         connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
         connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
-        connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
         connect(emojiBtn_,
                 SIGNAL(emojiSelected(const QString &)),
                 this,
@@ -633,9 +531,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
         connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
 
         connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
-
-        connect(
-          input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner);
 }
 
 void
@@ -655,47 +550,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji)
 }
 
 void
-TextInputWidget::openFileSelection()
-{
-        const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-        const auto fileName =
-          QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)"));
-
-        if (fileName.isEmpty())
-                return;
-
-        QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
-
-        const auto format = mime.name().split("/")[0];
-
-        QSharedPointer<QFile> file{new QFile{fileName, this}};
-
-        emit uploadMedia(file, format, QFileInfo(fileName).fileName());
-
-        showUploadSpinner();
-}
-
-void
-TextInputWidget::showUploadSpinner()
-{
-        topLayout_->removeWidget(sendFileBtn_);
-        sendFileBtn_->hide();
-
-        topLayout_->insertWidget(1, spinner_);
-        spinner_->start();
-}
-
-void
-TextInputWidget::hideUploadSpinner()
-{
-        topLayout_->removeWidget(spinner_);
-        topLayout_->insertWidget(1, sendFileBtn_);
-        sendFileBtn_->show();
-        spinner_->stop();
-}
-
-void
 TextInputWidget::stopTyping()
 {
         input_->stopTyping();
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index f9d84871..44419547 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -57,9 +57,6 @@ signals:
         void startedTyping();
         void stoppedTyping();
         void startedUpload();
-        void message(QString msg);
-        void command(QString name, QString args);
-        void media(QSharedPointer<QIODevice> data, QString mimeClass, const QString &filename);
 
         //! Trigger the suggestion popup.
         void showSuggestions(const QString &query);
@@ -73,8 +70,6 @@ public slots:
 
 protected:
         void keyPressEvent(QKeyEvent *event) override;
-        bool canInsertFromMimeData(const QMimeData *source) const override;
-        void insertFromMimeData(const QMimeData *source) override;
         void focusOutEvent(QFocusEvent *event) override
         {
                 suggestionsPopup_.hide();
@@ -131,9 +126,7 @@ private:
 
         void insertCompletion(QString completion);
         void textChanged();
-        void uploadData(const QByteArray data, const QString &media, const QString &filename);
         void afterCompletion(int);
-        void showPreview(const QMimeData *source, const QStringList &formats);
 };
 
 class TextInputWidget : public QWidget
@@ -161,8 +154,6 @@ public:
         }
 
 public slots:
-        void openFileSelection();
-        void hideUploadSpinner();
         void focusLineEdit() { input_->setFocus(); }
         void changeCallButtonState(webrtc::State);
 
@@ -172,9 +163,6 @@ private slots:
 signals:
         void heightChanged(int height);
 
-        void uploadMedia(const QSharedPointer<QIODevice> data,
-                         QString mimeClass,
-                         const QString &filename);
         void callButtonPress();
 
         void sendJoinRoomRequest(const QString &room);
@@ -192,8 +180,6 @@ protected:
         void paintEvent(QPaintEvent *) override;
 
 private:
-        void showUploadSpinner();
-
         QHBoxLayout *topLayout_;
         FilteredTextEdit *input_;
 
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 38dbba22..2896e773 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -677,9 +677,10 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
 }
 
 QImage
-utils::readImage(QByteArray *data)
+utils::readImage(const QByteArray *data)
 {
-        QBuffer buf(data);
+        QBuffer buf;
+        buf.setData(*data);
         QImageReader reader(&buf);
         reader.setAutoTransform(true);
         return reader.read();
diff --git a/src/Utils.h b/src/Utils.h
index 5e7fb601..f59e8673 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value);
 
 //! Read image respecting exif orientation
 QImage
-readImage(QByteArray *data);
+readImage(const QByteArray *data);
 }
diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp
index 20959b0a..e03993c7 100644
--- a/src/dialogs/PreviewUploadOverlay.cpp
+++ b/src/dialogs/PreviewUploadOverlay.cpp
@@ -60,7 +60,10 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
                 emit confirmUpload(data_, mediaType_, fileName_.text());
                 close();
         });
-        connect(&cancel_, &QPushButton::clicked, this, &PreviewUploadOverlay::close);
+        connect(&cancel_, &QPushButton::clicked, this, [this]() {
+                emit aborted();
+                close();
+        });
 }
 
 void
diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h
index 11cd49bc..5139e3f2 100644
--- a/src/dialogs/PreviewUploadOverlay.h
+++ b/src/dialogs/PreviewUploadOverlay.h
@@ -40,6 +40,7 @@ public:
 
 signals:
         void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
+        void aborted();
 
 private:
         void init();
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,