Add file uploading
This commit is contained in:
parent
0bb4885632
commit
a31d3d0816
17 changed files with 475 additions and 318 deletions
|
|
@ -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 {
|
||||
|
|
@ -78,6 +146,37 @@ InputBar::send()
|
|||
nhlog::ui()->debug("Send: {}", text.toStdString());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -149,6 +248,112 @@ InputBar::emote(QString msg)
|
|||
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue