diff options
author | Nicolas Werner <nicolas.werner@hotmail.de> | 2020-11-15 04:52:49 +0100 |
---|---|---|
committer | Nicolas Werner <nicolas.werner@hotmail.de> | 2020-11-25 19:05:12 +0100 |
commit | a31d3d08165646738d6ae624ac4eff6971207058 (patch) | |
tree | 15e3140a8086a105d7bb38ebee6ed771eeaa06c3 /src/timeline | |
parent | Basic text input in qml (diff) | |
download | nheko-a31d3d08165646738d6ae624ac4eff6971207058.tar.xz |
Add file uploading
Diffstat (limited to 'src/timeline')
-rw-r--r-- | src/timeline/InputBar.cpp | 317 | ||||
-rw-r--r-- | src/timeline/InputBar.h | 41 | ||||
-rw-r--r-- | src/timeline/TimelineModel.h | 2 |
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, |