summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--include/MatrixClient.h19
-rw-r--r--include/TextInputWidget.h18
-rw-r--r--include/dialogs/PreviewImageOverlay.h57
-rw-r--r--include/timeline/TimelineView.h10
-rw-r--r--include/timeline/TimelineViewManager.h7
-rw-r--r--include/timeline/widgets/AudioItem.h1
-rw-r--r--include/timeline/widgets/FileItem.h1
-rw-r--r--include/timeline/widgets/ImageItem.h1
-rw-r--r--include/timeline/widgets/VideoItem.h1
-rw-r--r--resources/styles/nheko-dark.qss9
-rw-r--r--resources/styles/nheko.qss7
-rw-r--r--src/ChatPage.cc31
-rw-r--r--src/MatrixClient.cc44
-rw-r--r--src/TextInputWidget.cc65
-rw-r--r--src/dialogs/PreviewImageOverlay.cc142
-rw-r--r--src/timeline/TimelineView.cc8
-rw-r--r--src/timeline/TimelineViewManager.cc7
-rw-r--r--src/timeline/widgets/AudioItem.cc6
-rw-r--r--src/timeline/widgets/FileItem.cc6
-rw-r--r--src/timeline/widgets/ImageItem.cc17
-rw-r--r--src/timeline/widgets/VideoItem.cc2
22 files changed, 388 insertions, 73 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f44198f2..876409b6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -144,6 +144,7 @@ set(SRC_FILES
     # Dialogs
     src/dialogs/CreateRoom.cc
     src/dialogs/ImageOverlay.cc
+    src/dialogs/PreviewImageOverlay.cc
     src/dialogs/InviteUsers.cc
     src/dialogs/JoinRoom.cc
     src/dialogs/LeaveRoom.cc
@@ -229,6 +230,7 @@ qt5_wrap_cpp(MOC_HEADERS
     # Dialogs
     include/dialogs/CreateRoom.h
     include/dialogs/ImageOverlay.h
+    include/dialogs/PreviewImageOverlay.h
     include/dialogs/InviteUsers.h
     include/dialogs/JoinRoom.h
     include/dialogs/LeaveRoom.h
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 8936003f..df21ccdb 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -55,9 +55,15 @@ public:
         void downloadImage(const QString &event_id, const QUrl &url);
         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 uploadAudio(const QString &roomid, const QString &filename);
+        void uploadImage(const QString &roomid,
+                         const QSharedPointer<QIODevice> data,
+                         const QString &filename);
+        void uploadFile(const QString &roomid,
+                        const QSharedPointer<QIODevice> data,
+                        const QString &filename);
+        void uploadAudio(const QString &roomid,
+                         const QSharedPointer<QIODevice> data,
+                         const QString &filename);
         void joinRoom(const QString &roomIdOrAlias);
         void leaveRoom(const QString &roomId);
         void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000);
@@ -98,7 +104,10 @@ signals:
                              const QString &homeserver,
                              const QString &token);
         void versionSuccess();
-        void imageUploaded(const QString &roomid, const QString &filename, const QString &url);
+        void imageUploaded(const QString &roomid,
+                           const QSharedPointer<QIODevice> data,
+                           const QString &filename,
+                           const QString &url);
         void fileUploaded(const QString &roomid, const QString &filename, const QString &url);
         void audioUploaded(const QString &roomid, const QString &filename, const QString &url);
 
@@ -131,7 +140,7 @@ signals:
         void roomCreationFailed(const QString &msg);
 
 private:
-        QNetworkReply *makeUploadRequest(const QString &filename);
+        QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
 
         // Client API prefix.
         QString clientApiUrl_;
diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h
index df309e27..cc01be69 100644
--- a/include/TextInputWidget.h
+++ b/include/TextInputWidget.h
@@ -27,8 +27,14 @@
 #include "FlatButton.h"
 #include "LoadingIndicator.h"
 
+#include "dialogs/PreviewImageOverlay.h"
+
 #include "emoji/PickButton.h"
 
+namespace dialogs {
+class PreviewImageOverlay;
+}
+
 class FilteredTextEdit : public QTextEdit
 {
         Q_OBJECT
@@ -48,16 +54,22 @@ signals:
         void stoppedTyping();
         void message(QString);
         void command(QString name, QString args);
+        void image(const QSharedPointer<QIODevice> iodev, const QString &img_name);
 
 protected:
         void keyPressEvent(QKeyEvent *event) override;
+        bool canInsertFromMimeData(const QMimeData *source) const override;
+        void insertFromMimeData(const QMimeData *source) override;
 
 private:
         std::deque<QString> true_history_, working_history_;
         size_t history_index_;
         QTimer *typingTimer_;
 
+        dialogs::PreviewImageOverlay previewDialog_;
+
         void textChanged();
+        void receiveImage(const QByteArray img, const QString &img_name);
         void afterCompletion(int);
 };
 
@@ -83,9 +95,9 @@ signals:
         void sendTextMessage(QString msg);
         void sendEmoteMessage(QString msg);
 
-        void uploadImage(QString filename);
-        void uploadFile(QString filename);
-        void uploadAudio(QString filename);
+        void uploadImage(QSharedPointer<QIODevice> data, const QString &filename);
+        void uploadFile(QSharedPointer<QIODevice> data, const QString &filename);
+        void uploadAudio(QSharedPointer<QIODevice> data, const QString &filename);
 
         void sendJoinRoomRequest(const QString &room);
 
diff --git a/include/dialogs/PreviewImageOverlay.h b/include/dialogs/PreviewImageOverlay.h
new file mode 100644
index 00000000..a1ab32ee
--- /dev/null
+++ b/include/dialogs/PreviewImageOverlay.h
@@ -0,0 +1,57 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QLabel>
+#include <QLineEdit>
+#include <QPixmap>
+#include <QWidget>
+
+#include "FlatButton.h"
+
+class QMimeData;
+
+namespace dialogs {
+
+class PreviewImageOverlay : public QWidget
+{
+        Q_OBJECT
+public:
+        PreviewImageOverlay(QWidget *parent = nullptr);
+
+        void setImageAndCreate(const QByteArray data, const QString &type);
+        void setImageAndCreate(const QString &path);
+
+signals:
+        void confirmImageUpload(const QByteArray data, const QString &img_name);
+
+private:
+        void init();
+
+        QPixmap image_;
+        QByteArray imageData_;
+        QString imagePath_;
+
+        QLabel titleLabel_;
+        QLabel imageLabel_;
+        QLineEdit imageName_;
+
+        FlatButton upload_;
+        FlatButton cancel_;
+};
+} // dialogs
diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index cde64148..bba40669 100644
--- a/include/timeline/TimelineView.h
+++ b/include/timeline/TimelineView.h
@@ -87,7 +87,9 @@ public:
         void addUserMessage(mtx::events::MessageType ty, const QString &msg);
 
         template<class Widget, mtx::events::MessageType MsgType>
-        void addUserMessage(const QString &url, const QString &filename);
+        void addUserMessage(const QString &url,
+                            const QSharedPointer<QIODevice> data,
+                            const QString &filename);
         void updatePendingMessage(int txn_id, QString event_id);
         void scrollDown();
         void addDateSeparator(QDateTime datetime, int position);
@@ -216,11 +218,13 @@ private:
 
 template<class Widget, mtx::events::MessageType MsgType>
 void
-TimelineView::addUserMessage(const QString &url, const QString &filename)
+TimelineView::addUserMessage(const QString &url,
+                             const QSharedPointer<QIODevice> data,
+                             const QString &filename)
 {
         auto with_sender = lastSender_ != local_user_;
 
-        auto widget = new Widget(client_, url, filename, this);
+        auto widget = new Widget(client_, url, data, filename, this);
 
         TimelineItem *view_item =
           new TimelineItem(widget, local_user_, with_sender, scroll_widget_);
diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h
index 2c32da16..c19031c6 100644
--- a/include/timeline/TimelineViewManager.h
+++ b/include/timeline/TimelineViewManager.h
@@ -23,6 +23,8 @@
 
 #include <mtx.hpp>
 
+class QFile;
+
 class MatrixClient;
 class RoomInfoListItem;
 class TimelineView;
@@ -64,7 +66,10 @@ public slots:
         void setHistoryView(const QString &room_id);
         void queueTextMessage(const QString &msg);
         void queueEmoteMessage(const QString &msg);
-        void queueImageMessage(const QString &roomid, const QString &filename, const QString &url);
+        void queueImageMessage(const QString &roomid,
+                               const QSharedPointer<QIODevice> data,
+                               const QString &filename,
+                               const QString &url);
         void queueFileMessage(const QString &roomid, const QString &filename, const QString &url);
         void queueAudioMessage(const QString &roomid, const QString &filename, const QString &url);
 
diff --git a/include/timeline/widgets/AudioItem.h b/include/timeline/widgets/AudioItem.h
index f8e7cc07..ca81f498 100644
--- a/include/timeline/widgets/AudioItem.h
+++ b/include/timeline/widgets/AudioItem.h
@@ -48,6 +48,7 @@ public:
 
         AudioItem(QSharedPointer<MatrixClient> client,
                   const QString &url,
+                  const QSharedPointer<QIODevice> data,
                   const QString &filename,
                   QWidget *parent = nullptr);
 
diff --git a/include/timeline/widgets/FileItem.h b/include/timeline/widgets/FileItem.h
index fd0b0249..72589189 100644
--- a/include/timeline/widgets/FileItem.h
+++ b/include/timeline/widgets/FileItem.h
@@ -42,6 +42,7 @@ public:
 
         FileItem(QSharedPointer<MatrixClient> client,
                  const QString &url,
+                 const QSharedPointer<QIODevice> data,
                  const QString &filename,
                  QWidget *parent = nullptr);
 
diff --git a/include/timeline/widgets/ImageItem.h b/include/timeline/widgets/ImageItem.h
index 467c70ab..d24b7239 100644
--- a/include/timeline/widgets/ImageItem.h
+++ b/include/timeline/widgets/ImageItem.h
@@ -36,6 +36,7 @@ public:
 
         ImageItem(QSharedPointer<MatrixClient> client,
                   const QString &url,
+                  const QSharedPointer<QIODevice> data,
                   const QString &filename,
                   QWidget *parent = nullptr);
 
diff --git a/include/timeline/widgets/VideoItem.h b/include/timeline/widgets/VideoItem.h
index 88ff21ec..53c3e21a 100644
--- a/include/timeline/widgets/VideoItem.h
+++ b/include/timeline/widgets/VideoItem.h
@@ -37,6 +37,7 @@ public:
 
         VideoItem(QSharedPointer<MatrixClient> client,
                   const QString &url,
+                  const QSharedPointer<QIODevice> data,
                   const QString &filename,
                   QWidget *parent = nullptr);
 
diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss
index 26425590..88631858 100644
--- a/resources/styles/nheko-dark.qss
+++ b/resources/styles/nheko-dark.qss
@@ -95,16 +95,13 @@ dialogs--LeaveRoom,
 dialogs--CreateRoom,
 dialogs--InviteUsers,
 dialogs--ReadReceipts,
-dialogs--JoinRoom {
-    background-color: #383c4a;
-    color: #caccd1;
-}
-
-QListWidget {
+dialogs--JoinRoom,
+dialogs--PreviewImageOverlay {
     background-color: #383c4a;
     color: #caccd1;
 }
 
+QListWidget,
 WelcomePage,
 LoginPage,
 RegisterPage {
diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss
index c135c12a..a5f99353 100644
--- a/resources/styles/nheko.qss
+++ b/resources/styles/nheko.qss
@@ -98,11 +98,8 @@ dialogs--LeaveRoom,
 dialogs--CreateRoom,
 dialogs--InviteUsers,
 dialogs--ReadReceipts,
-dialogs--JoinRoom {
-    background-color: white;
-    color: #333;
-}
-
+dialogs--JoinRoom,
+dialogs--PreviewImageOverlay,
 QListWidget {
     background-color: white;
     color: #333;
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index ebdec835..f49c0a08 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -228,17 +228,26 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
                 client_.data(),
                 &MatrixClient::joinRoom);
 
-        connect(text_input_, &TextInputWidget::uploadImage, this, [=](QString filename) {
-                client_->uploadImage(current_room_, filename);
-        });
+        connect(text_input_,
+                &TextInputWidget::uploadImage,
+                this,
+                [=](QSharedPointer<QIODevice> data, const QString &fn) {
+                        client_->uploadImage(current_room_, data, fn);
+                });
 
-        connect(text_input_, &TextInputWidget::uploadFile, this, [=](QString filename) {
-                client_->uploadFile(current_room_, filename);
-        });
+        connect(text_input_,
+                &TextInputWidget::uploadFile,
+                this,
+                [=](QSharedPointer<QIODevice> data, const QString &fn) {
+                        client_->uploadFile(current_room_, data, fn);
+                });
 
-        connect(text_input_, &TextInputWidget::uploadAudio, this, [=](QString filename) {
-                client_->uploadAudio(current_room_, filename);
-        });
+        connect(text_input_,
+                &TextInputWidget::uploadAudio,
+                this,
+                [=](QSharedPointer<QIODevice> data, const QString &fn) {
+                        client_->uploadAudio(current_room_, data, fn);
+                });
 
         connect(
           client_.data(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification);
@@ -246,9 +255,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
         connect(client_.data(),
                 &MatrixClient::imageUploaded,
                 this,
-                [=](QString roomid, QString filename, QString url) {
+                [=](QString roomid, QSharedPointer<QIODevice> data, QString filename, QString url) {
                         text_input_->hideUploadSpinner();
-                        view_manager_->queueImageMessage(roomid, filename, url);
+                        view_manager_->queueImageMessage(roomid, data, filename, url);
                 });
         connect(client_.data(),
                 &MatrixClient::fileUploaded,
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 72467385..67203d59 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -821,14 +821,16 @@ MatrixClient::messages(const QString &roomid, const QString &from_token, int lim
 }
 
 void
-MatrixClient::uploadImage(const QString &roomid, const QString &filename)
+MatrixClient::uploadImage(const QString &roomid,
+                          const QSharedPointer<QIODevice> data,
+                          const QString &filename)
 {
-        auto reply = makeUploadRequest(filename);
+        auto reply = makeUploadRequest(data);
 
         if (reply == nullptr)
                 return;
 
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() {
+        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() {
                 reply->deleteLater();
 
                 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@@ -838,12 +840,12 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename)
                         return;
                 }
 
-                auto data = reply->readAll();
+                auto res_data = reply->readAll();
 
-                if (data.isEmpty())
+                if (res_data.isEmpty())
                         return;
 
-                auto json = QJsonDocument::fromJson(data);
+                auto json = QJsonDocument::fromJson(res_data);
 
                 if (!json.isObject()) {
                         qDebug() << "Media upload: Response is not a json object.";
@@ -857,16 +859,18 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename)
                         return;
                 }
 
-                emit imageUploaded(roomid, filename, object.value("content_uri").toString());
+                emit imageUploaded(roomid, data, filename, object.value("content_uri").toString());
         });
 }
 
 void
-MatrixClient::uploadFile(const QString &roomid, const QString &filename)
+MatrixClient::uploadFile(const QString &roomid,
+                         const QSharedPointer<QIODevice> data,
+                         const QString &filename)
 {
-        auto reply = makeUploadRequest(filename);
+        auto reply = makeUploadRequest(data);
 
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() {
+        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() {
                 reply->deleteLater();
 
                 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@@ -900,11 +904,13 @@ MatrixClient::uploadFile(const QString &roomid, const QString &filename)
 }
 
 void
-MatrixClient::uploadAudio(const QString &roomid, const QString &filename)
+MatrixClient::uploadAudio(const QString &roomid,
+                          const QSharedPointer<QIODevice> data,
+                          const QString &filename)
 {
-        auto reply = makeUploadRequest(filename);
+        auto reply = makeUploadRequest(data);
 
-        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() {
+        connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() {
                 reply->deleteLater();
 
                 int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@@ -1158,7 +1164,7 @@ MatrixClient::readEvent(const QString &room_id, const QString &event_id)
 }
 
 QNetworkReply *
-MatrixClient::makeUploadRequest(const QString &filename)
+MatrixClient::makeUploadRequest(QSharedPointer<QIODevice> iodev)
 {
         QUrlQuery query;
         query.addQueryItem("access_token", token_);
@@ -1167,20 +1173,18 @@ MatrixClient::makeUploadRequest(const QString &filename)
         endpoint.setPath(mediaApiUrl_ + "/upload");
         endpoint.setQuery(query);
 
-        QFile file(filename);
-        if (!file.open(QIODevice::ReadWrite)) {
-                qDebug() << "Error while reading" << filename;
+        if (!iodev->open(QIODevice::ReadOnly)) {
+                qWarning() << "Error while reading device:" << iodev->errorString();
                 return nullptr;
         }
 
         QMimeDatabase db;
-        QMimeType mime = db.mimeTypeForFile(filename, QMimeDatabase::MatchContent);
+        QMimeType mime = db.mimeTypeForData(iodev.data());
 
         QNetworkRequest request(QString(endpoint.toEncoded()));
-        request.setHeader(QNetworkRequest::ContentLengthHeader, file.size());
         request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name());
 
-        auto reply = post(request, file.readAll());
+        auto reply = post(request, iodev.data());
 
         return reply;
 }
diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index dc2bebe7..f9198c78 100644
--- a/src/TextInputWidget.cc
+++ b/src/TextInputWidget.cc
@@ -16,10 +16,13 @@
  */
 
 #include <QAbstractTextDocumentLayout>
+#include <QApplication>
+#include <QBuffer>
+#include <QClipboard>
 #include <QDebug>
-#include <QFile>
 #include <QFileDialog>
 #include <QImageReader>
+#include <QMimeData>
 #include <QMimeDatabase>
 #include <QMimeType>
 #include <QPainter>
@@ -33,6 +36,7 @@ static constexpr size_t INPUT_HISTORY_SIZE = 127;
 FilteredTextEdit::FilteredTextEdit(QWidget *parent)
   : QTextEdit{parent}
   , history_index_{0}
+  , previewDialog_{parent}
 {
         connect(document()->documentLayout(),
                 &QAbstractTextDocumentLayout::documentSizeChanged,
@@ -50,6 +54,12 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
         typingTimer_->setSingleShot(true);
 
         connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
+        connect(&previewDialog_,
+                &dialogs::PreviewImageOverlay::confirmImageUpload,
+                this,
+                &FilteredTextEdit::receiveImage);
+
+        previewDialog_.hide();
 }
 
 void
@@ -101,6 +111,42 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
         }
 }
 
+bool
+FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const
+{
+        return (source->hasImage() || QTextEdit::canInsertFromMimeData(source));
+}
+
+void
+FilteredTextEdit::insertFromMimeData(const QMimeData *source)
+{
+        if (source->hasImage()) {
+                const auto formats = source->formats();
+                const auto idx     = formats.indexOf(
+                  QRegularExpression{"image/.+", QRegularExpression::CaseInsensitiveOption});
+
+                // Note: in the future we may want to look into what the best choice is from the
+                // formats list. For now we will default to PNG format.
+                QString type = "png";
+                if (idx != -1) {
+                        type = formats.at(idx).split('/')[1];
+                }
+
+                // Encode raw pixel data of image.
+                QByteArray data = source->data("image/" + type);
+                previewDialog_.setImageAndCreate(data, type);
+                previewDialog_.show();
+        } else if (source->hasFormat("x-special/gnome-copied-files") &&
+                   QImageReader{source->text()}.canRead()) {
+                // Special case for X11 users. See "Notes for X11 Users" in source.
+                // Source: http://doc.qt.io/qt-5/qclipboard.html
+                previewDialog_.setImageAndCreate(source->text());
+                previewDialog_.show();
+        } else {
+                QTextEdit::insertFromMimeData(source);
+        }
+}
+
 void
 FilteredTextEdit::stopTyping()
 {
@@ -146,6 +192,7 @@ FilteredTextEdit::submit()
         history_index_ = 0;
 
         QString text = toPlainText();
+
         if (text.startsWith('/')) {
                 int command_end = text.indexOf(' ');
                 if (command_end == -1)
@@ -170,6 +217,14 @@ FilteredTextEdit::textChanged()
         working_history_[history_index_] = toPlainText();
 }
 
+void
+FilteredTextEdit::receiveImage(const QByteArray img, const QString &img_name)
+{
+        QSharedPointer<QBuffer> buffer{new QBuffer{this}};
+        buffer->setData(img);
+        emit image(buffer, img_name);
+}
+
 TextInputWidget::TextInputWidget(QWidget *parent)
   : QFrame(parent)
 {
@@ -231,6 +286,7 @@ TextInputWidget::TextInputWidget(QWidget *parent)
         connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
         connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage);
         connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command);
+        connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage);
         connect(emojiBtn_,
                 SIGNAL(emojiSelected(const QString &)),
                 this,
@@ -289,12 +345,13 @@ TextInputWidget::openFileSelection()
 
         const auto format = mime.name().split("/")[0];
 
+        QSharedPointer<QFile> file{new QFile{fileName, this}};
         if (format == "image")
-                emit uploadImage(fileName);
+                emit uploadImage(file, fileName);
         else if (format == "audio")
-                emit uploadAudio(fileName);
+                emit uploadAudio(file, fileName);
         else
-                emit uploadFile(fileName);
+                emit uploadFile(file, fileName);
 
         showUploadSpinner();
 }
diff --git a/src/dialogs/PreviewImageOverlay.cc b/src/dialogs/PreviewImageOverlay.cc
new file mode 100644
index 00000000..31ef00ed
--- /dev/null
+++ b/src/dialogs/PreviewImageOverlay.cc
@@ -0,0 +1,142 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QApplication>
+#include <QBuffer>
+#include <QDebug>
+#include <QFile>
+#include <QFileInfo>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+
+#include "Config.h"
+
+#include "dialogs/PreviewImageOverlay.h"
+
+using namespace dialogs;
+
+static constexpr const char *DEFAULT = "Upload image?";
+static constexpr const char *ERROR   = "Failed to load image type '%1'. Continue upload?";
+
+PreviewImageOverlay::PreviewImageOverlay(QWidget *parent)
+  : QWidget{parent}
+  , titleLabel_{tr(DEFAULT), this}
+  , imageLabel_{this}
+  , imageName_{tr("clipboard"), this}
+  , upload_{tr("Upload"), this}
+  , cancel_{tr("Cancel"), this}
+{
+        auto hlayout = new QHBoxLayout;
+        hlayout->addWidget(&upload_);
+        hlayout->addWidget(&cancel_);
+
+        auto vlayout = new QVBoxLayout{this};
+        vlayout->addWidget(&titleLabel_);
+        vlayout->addWidget(&imageLabel_);
+        vlayout->addWidget(&imageName_);
+        vlayout->addLayout(hlayout);
+
+        connect(&upload_, &QPushButton::clicked, [&]() {
+                emit confirmImageUpload(imageData_, imageName_.text());
+                close();
+        });
+        connect(&cancel_, &QPushButton::clicked, [&]() { close(); });
+}
+
+void
+PreviewImageOverlay::init()
+{
+        auto window   = QApplication::activeWindow();
+        auto winsize  = window->frameGeometry().size();
+        auto center   = window->frameGeometry().center();
+        auto img_size = image_.size();
+
+        imageName_.setText(QFileInfo{imagePath_}.fileName());
+
+        setAutoFillBackground(true);
+        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+        setWindowModality(Qt::WindowModal);
+
+        titleLabel_.setStyleSheet(
+          QString{"font-weight: bold; font-size: %1px;"}.arg(conf::headerFontSize));
+        titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+        titleLabel_.setAlignment(Qt::AlignCenter);
+        imageLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+        imageLabel_.setAlignment(Qt::AlignCenter);
+        imageName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+        imageName_.setAlignment(Qt::AlignCenter);
+        upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+        cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+        upload_.setFontSize(conf::btn::fontSize);
+        cancel_.setFontSize(conf::btn::fontSize);
+
+        // Scale image preview to the size of the current window if it is larger.
+        if ((img_size.height() * img_size.width()) > (winsize.height() * winsize.width())) {
+                imageLabel_.setPixmap(image_.scaled(winsize, Qt::KeepAspectRatio));
+        } else {
+                imageLabel_.setPixmap(image_);
+                move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
+        }
+        imageLabel_.setScaledContents(false);
+
+        raise();
+}
+
+void
+PreviewImageOverlay::setImageAndCreate(const QByteArray data, const QString &type)
+{
+        imageData_  = data;
+        imagePath_  = "clipboard." + type;
+        auto loaded = image_.loadFromData(imageData_);
+        if (!loaded) {
+                titleLabel_.setText(QString{tr(ERROR)}.arg(type));
+        } else {
+                titleLabel_.setText(tr(DEFAULT));
+        }
+
+        init();
+}
+
+void
+PreviewImageOverlay::setImageAndCreate(const QString &path)
+{
+        QFile file{path};
+        imagePath_ = path;
+
+        if (!file.open(QIODevice::ReadOnly)) {
+                qWarning() << "Failed to open image from:" << path;
+                qWarning() << "Reason:" << file.errorString();
+                close();
+                return;
+        }
+
+        if ((imageData_ = file.readAll()).isEmpty()) {
+                qWarning() << "Failed to read image:" << file.errorString();
+                close();
+                return;
+        }
+
+        auto loaded = image_.loadFromData(imageData_);
+        if (!loaded) {
+                auto t = QFileInfo{path}.suffix();
+                titleLabel_.setText(QString{tr(ERROR)}.arg(t));
+        } else {
+                titleLabel_.setText(tr(DEFAULT));
+        }
+
+        init();
+}
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index d21f30f0..75ce8141 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -510,12 +510,8 @@ TimelineView::sendNextPendingMessage()
         case mtx::events::MessageType::Image:
         case mtx::events::MessageType::File:
                 // FIXME: Improve the API
-                client_->sendRoomMessage(m.ty,
-                                         m.txn_id,
-                                         room_id_,
-                                         QFileInfo(m.filename).fileName(),
-                                         QFileInfo(m.filename),
-                                         m.body);
+                client_->sendRoomMessage(
+                  m.ty, m.txn_id, room_id_, m.filename, QFileInfo(m.filename), m.body);
                 break;
         default:
                 client_->sendRoomMessage(m.ty, m.txn_id, room_id_, m.body, QFileInfo());
diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index de1e1e32..65c9ac83 100644
--- a/src/timeline/TimelineViewManager.cc
+++ b/src/timeline/TimelineViewManager.cc
@@ -85,6 +85,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg)
 
 void
 TimelineViewManager::queueImageMessage(const QString &roomid,
+                                       const QSharedPointer<QIODevice> data,
                                        const QString &filename,
                                        const QString &url)
 {
@@ -95,7 +96,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
 
         auto view = views_[roomid];
 
-        view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(url, filename);
+        view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(url, data, filename);
 }
 
 void
@@ -110,7 +111,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
 
         auto view = views_[roomid];
 
-        view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename);
+        view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, nullptr, filename);
 }
 
 void
@@ -125,7 +126,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
 
         auto view = views_[roomid];
 
-        view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename);
+        view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, nullptr, filename);
 }
 
 void
diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc
index 5d9dd77b..e84cbb3a 100644
--- a/src/timeline/widgets/AudioItem.cc
+++ b/src/timeline/widgets/AudioItem.cc
@@ -89,14 +89,16 @@ AudioItem::AudioItem(QSharedPointer<MatrixClient> client,
 
 AudioItem::AudioItem(QSharedPointer<MatrixClient> client,
                      const QString &url,
+                     const QSharedPointer<QIODevice> data,
                      const QString &filename,
                      QWidget *parent)
   : QWidget(parent)
   , url_{url}
-  , text_{QFileInfo(filename).fileName()}
+  , text_{QFileInfo{filename}.fileName()}
   , client_{client}
 {
-        readableFileSize_ = calculateFileSize(QFileInfo(filename).size());
+        Q_UNUSED(data);
+        readableFileSize_ = calculateFileSize(QFileInfo{filename}.size());
 
         init();
 }
diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc
index 3c38dc31..a6159309 100644
--- a/src/timeline/widgets/FileItem.cc
+++ b/src/timeline/widgets/FileItem.cc
@@ -76,14 +76,16 @@ FileItem::FileItem(QSharedPointer<MatrixClient> client,
 
 FileItem::FileItem(QSharedPointer<MatrixClient> client,
                    const QString &url,
+                   const QSharedPointer<QIODevice> data,
                    const QString &filename,
                    QWidget *parent)
   : QWidget(parent)
   , url_{url}
-  , text_{QFileInfo(filename).fileName()}
+  , text_{QFileInfo{filename}.fileName()}
   , client_{client}
 {
-        readableFileSize_ = calculateFileSize(QFileInfo(filename).size());
+        Q_UNUSED(data);
+        readableFileSize_ = calculateFileSize(QFileInfo{filename}.size());
 
         init();
 }
diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc
index 9038456d..48a4c1eb 100644
--- a/src/timeline/widgets/ImageItem.cc
+++ b/src/timeline/widgets/ImageItem.cc
@@ -61,11 +61,12 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client,
 
 ImageItem::ImageItem(QSharedPointer<MatrixClient> client,
                      const QString &url,
+                     const QSharedPointer<QIODevice> data,
                      const QString &filename,
                      QWidget *parent)
   : QWidget(parent)
   , url_{url}
-  , text_{QFileInfo(filename).fileName()}
+  , text_{filename}
   , client_{client}
 {
         setMouseTracking(true);
@@ -83,7 +84,19 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client,
         url_                 = QString("%1/_matrix/media/r0/download/%2")
                  .arg(client_.data()->getHomeServer().toString(), media_params);
 
-        setImage(QPixmap(filename));
+        if (data.isNull()) {
+                qWarning() << "No image data to display";
+                return;
+        }
+
+        if (data->reset()) {
+                QPixmap p;
+                p.loadFromData(data->readAll());
+                setImage(p);
+        } else {
+                qWarning() << "Failed to seek to beginning of device:" << data->errorString();
+                return;
+        }
 }
 
 void
diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc
index b3987b83..b46dff7b 100644
--- a/src/timeline/widgets/VideoItem.cc
+++ b/src/timeline/widgets/VideoItem.cc
@@ -66,6 +66,7 @@ VideoItem::VideoItem(QSharedPointer<MatrixClient> client,
 
 VideoItem::VideoItem(QSharedPointer<MatrixClient> client,
                      const QString &url,
+                     const QSharedPointer<QIODevice> data,
                      const QString &filename,
                      QWidget *parent)
   : QWidget(parent)
@@ -73,6 +74,7 @@ VideoItem::VideoItem(QSharedPointer<MatrixClient> client,
   , text_{QFileInfo(filename).fileName()}
   , client_{client}
 {
+        Q_UNUSED(data);
         readableFileSize_ = calculateFileSize(QFileInfo(filename).size());
 
         init();