|
|
@ -1,18 +1,27 @@ |
|
|
|
#include "InputBar.h" |
|
|
|
#include "InputBar.h" |
|
|
|
|
|
|
|
|
|
|
|
#include <QClipboard> |
|
|
|
#include <QClipboard> |
|
|
|
|
|
|
|
#include <QFileDialog> |
|
|
|
#include <QGuiApplication> |
|
|
|
#include <QGuiApplication> |
|
|
|
#include <QMimeData> |
|
|
|
#include <QMimeData> |
|
|
|
|
|
|
|
#include <QMimeDatabase> |
|
|
|
|
|
|
|
#include <QStandardPaths> |
|
|
|
|
|
|
|
#include <QUrl> |
|
|
|
|
|
|
|
|
|
|
|
#include <mtx/responses/common.hpp> |
|
|
|
#include <mtx/responses/common.hpp> |
|
|
|
|
|
|
|
#include <mtx/responses/media.hpp> |
|
|
|
|
|
|
|
|
|
|
|
#include "Cache.h" |
|
|
|
#include "Cache.h" |
|
|
|
#include "ChatPage.h" |
|
|
|
#include "ChatPage.h" |
|
|
|
#include "Logging.h" |
|
|
|
#include "Logging.h" |
|
|
|
#include "MatrixClient.h" |
|
|
|
#include "MatrixClient.h" |
|
|
|
|
|
|
|
#include "Olm.h" |
|
|
|
#include "TimelineModel.h" |
|
|
|
#include "TimelineModel.h" |
|
|
|
#include "UserSettingsPage.h" |
|
|
|
#include "UserSettingsPage.h" |
|
|
|
#include "Utils.h" |
|
|
|
#include "Utils.h" |
|
|
|
|
|
|
|
#include "dialogs/PreviewUploadOverlay.h" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "blurhash.hpp" |
|
|
|
|
|
|
|
|
|
|
|
static constexpr size_t INPUT_HISTORY_SIZE = 10; |
|
|
|
static constexpr size_t INPUT_HISTORY_SIZE = 10; |
|
|
|
|
|
|
|
|
|
|
@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse) |
|
|
|
if (!md) |
|
|
|
if (!md) |
|
|
|
return; |
|
|
|
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()) { |
|
|
|
} else if (md->hasText()) { |
|
|
|
emit insertText(md->text()); |
|
|
|
emit insertText(md->text()); |
|
|
|
} else { |
|
|
|
} else { |
|
|
@ -78,6 +146,37 @@ InputBar::send() |
|
|
|
nhlog::ui()->debug("Send: {}", text.toStdString()); |
|
|
|
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 |
|
|
|
void |
|
|
|
InputBar::message(QString msg) |
|
|
|
InputBar::message(QString msg) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -149,6 +248,112 @@ InputBar::emote(QString msg) |
|
|
|
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); |
|
|
|
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 |
|
|
|
void |
|
|
|
InputBar::command(QString command, QString args) |
|
|
|
InputBar::command(QString command, QString args) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -196,3 +401,113 @@ InputBar::command(QString command, QString args) |
|
|
|
cache::dropOutboundMegolmSession(room->roomId().toStdString()); |
|
|
|
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); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|