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);
+ });
+ });
+}
|