summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2022-03-20 22:49:33 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2022-03-20 22:49:33 +0100
commitd3471a1097167963df35f96ca198860f6ec892bc (patch)
tree9b38fbe772f488acd852a99066dcd4d61ca97f07 /src/timeline
parentelide usernames in timeline (#997) (diff)
downloadnheko-d3471a1097167963df35f96ca198860f6ec892bc.tar.xz
Move uploads to InputBar
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/InputBar.cpp336
-rw-r--r--src/timeline/InputBar.h102
2 files changed, 293 insertions, 145 deletions
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp

index 349ce7af..1b7d6efb 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -5,6 +5,7 @@ #include "InputBar.h" +#include <QBuffer> #include <QClipboard> #include <QDropEvent> #include <QFileDialog> @@ -31,7 +32,6 @@ #include "TimelineViewManager.h" #include "UserSettingsPage.h" #include "Utils.h" -#include "dialogs/PreviewUploadOverlay.h" #include "blurhash.hpp" @@ -67,29 +67,23 @@ InputBar::insertMimeData(const QMimeData *md) if (md->hasImage()) { if (formats.contains(QStringLiteral("image/svg+xml"), Qt::CaseInsensitive)) { - showPreview(*md, QLatin1String(""), QStringList(QStringLiteral("image/svg+xml"))); + startUploadFromMimeData(*md, QStringLiteral("image/svg+xml")); + } else if (formats.contains(QStringLiteral("image/png"), Qt::CaseInsensitive)) { + startUploadFromMimeData(*md, QStringLiteral("image/png")); } else { - showPreview(*md, QLatin1String(""), image); + startUploadFromMimeData(*md, image.first()); } } else if (!audio.empty()) { - showPreview(*md, QLatin1String(""), audio); + startUploadFromMimeData(*md, audio.first()); } else if (!video.empty()) { - showPreview(*md, QLatin1String(""), video); + startUploadFromMimeData(*md, video.first()); } else if (md->hasUrls()) { // Generic file path for any platform. - QString path; for (auto &&u : md->urls()) { if (u.isLocalFile()) { - path = u.toLocalFile(); - break; + startUploadFromPath(u.toLocalFile()); } } - - if (!path.isEmpty() && QFileInfo::exists(path)) { - showPreview(*md, path, formats); - } else { - nhlog::ui()->warn("Clipboard does not contain any valid file paths."); - } } else if (md->hasFormat(QStringLiteral("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 @@ -108,21 +102,12 @@ InputBar::insertMimeData(const QMimeData *md) return; } - QString path; for (int i = 1; i < data.size(); ++i) { QUrl url{data[i]}; if (url.isLocalFile()) { - path = url.toLocalFile(); - break; + startUploadFromPath(url.toLocalFile()); } } - - 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 { @@ -275,25 +260,7 @@ InputBar::openFileSelection() 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( - QStringLiteral("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()}); + startUploadFromPath(fileName); } void @@ -661,123 +628,204 @@ InputBar::command(const QString &command, QString args) } } -void -InputBar::showPreview(const QMimeData &source, const QString &path, const QStringList &formats) +MediaUpload::MediaUpload(std::unique_ptr<QIODevice> source_, + QString mimetype, + QString originalFilename, + bool encrypt, + QObject *parent) + : QObject(parent) + , source(std::move(source_)) + , mimetype_(std::move(mimetype)) + , originalFilename_(QFileInfo(originalFilename).fileName()) + , encrypt_(encrypt) { - auto *previewDialog_ = new dialogs::PreviewUploadOverlay(nullptr); - previewDialog_->setAttribute(Qt::WA_DeleteOnClose); + mimeClass_ = mimetype_.left(mimetype_.indexOf(u'/')); - // Force SVG to _not_ be handled as an image, but as raw data - if (source.hasImage() && - (formats.empty() || formats.front() != QLatin1String("image/svg+xml"))) { - if (!formats.empty() && formats.front().startsWith(QLatin1String("image/"))) { - // known format, keep as-is - previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), formats.front()); - } else { - // unknown image format, default to image/png - previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), - QStringLiteral("image/png")); - } - } else if (!path.isEmpty()) - previewDialog_->setPreview(path); - else if (!formats.isEmpty()) { - const auto &mime = formats.first(); - previewDialog_->setPreview(source.data(mime), mime); - } else { - setUploading(false); - previewDialog_->deleteLater(); + if (!source->isOpen()) + source->open(QIODevice::ReadOnly); + + data = source->readAll(); + + if (!data.size()) { + nhlog::ui()->warn("Attempted to upload zero-byte file?! Mimetype {}, filename {}", + mimetype_.toStdString(), + originalFilename_.toStdString()); + emit uploadFailed(this); return; } - connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() { - setUploading(false); - }); + nhlog::ui()->debug("Mime: {}", mimetype_.toStdString()); + if (mimeClass_ == u"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)); + } +} + +void +MediaUpload::startUpload() +{ + auto payload = std::string(data.data(), data.size()); + if (encrypt_) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(std::move(payload)); + payload = mtx::crypto::to_string(buf); + } + size_ = payload.size(); - connect( - previewDialog_, - &dialogs::PreviewUploadOverlay::confirmUpload, - this, - [this](const QByteArray &data, const QString &mime, const QString &fn) { - if (!data.size()) { - nhlog::ui()->warn("Attempted to upload zero-byte file?! Mimetype {}, filename {}", - mime.toStdString(), - fn.toStdString()); + http::client()->upload( + payload, + encryptedFile ? "application/octet-stream" : mimetype_.toStdString(), + originalFilename_.toStdString(), + [this](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)); + emit uploadFailed(this); return; } - setUploading(true); - setText(QLatin1String("")); + auto url = QString::fromStdString(res.content_uri); + if (encryptedFile) + encryptedFile->url = res.content_uri; - 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); - } + emit uploadComplete(this, std::move(url)); + }); +} - QSize dimensions; - QString blurhash; - auto mimeClass = mime.left(mime.indexOf(u'/')); - nhlog::ui()->debug("Mime: {}", mime.toStdString()); - if (mimeClass == u"image") { - QImage img = utils::readImage(data); +void +InputBar::finalizeUpload(MediaUpload *upload, QString url) +{ + auto mime = upload->mimetype(); + auto filename = upload->filename(); + auto mimeClass = upload->mimeClass(); + auto size = upload->size(); + auto encryptedFile = upload->encryptedFile_(); + if (mimeClass == u"image") + image(filename, encryptedFile, url, mime, size, upload->dimensions(), upload->blurhash()); + else if (mimeClass == u"audio") + audio(filename, encryptedFile, url, mime, size); + else if (mimeClass == u"video") + video(filename, encryptedFile, url, mime, size); + else + file(filename, encryptedFile, url, mime, size); - 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)); - } + removeRunUpload(upload); +} - 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; - } +void +InputBar::removeRunUpload(MediaUpload *upload) +{ + auto it = std::find_if(runningUploads.begin(), + runningUploads.end(), + [upload](const UploadHandle &h) { return h.get() == upload; }); + if (it != runningUploads.end()) + runningUploads.erase(it); - auto url = QString::fromStdString(res.content_uri); - if (encryptedFile) - encryptedFile->url = res.content_uri; + if (runningUploads.empty()) + setUploading(false); + else + runningUploads.front()->startUpload(); +} - if (mimeClass == u"image") - image(filename, encryptedFile, url, mime, size, dimensions, blurhash); - else if (mimeClass == u"audio") - audio(filename, encryptedFile, url, mime, size); - else if (mimeClass == u"video") - video(filename, encryptedFile, url, mime, size); - else - file(filename, encryptedFile, url, mime, size); +void +InputBar::startUploadFromPath(const QString &path) +{ + if (path.isEmpty()) + return; - setUploading(false); - }); - }); + auto file = std::make_unique<QFile>(path); + + if (!file->open(QIODevice::ReadOnly)) { + nhlog::ui()->warn( + "Failed to open file ({}): {}", path.toStdString(), file->errorString().toStdString()); + return; + } + + QMimeDatabase db; + auto mime = db.mimeTypeForFileNameAndData(path, file.get()); + + startUpload(std::move(file), path, mime.name()); +} + +void +InputBar::startUploadFromMimeData(const QMimeData &source, const QString &format) +{ + auto file = std::make_unique<QBuffer>(); + file->setData(source.data(format)); + + if (!file->open(QIODevice::ReadOnly)) { + nhlog::ui()->warn("Failed to open buffer: {}", file->errorString().toStdString()); + return; + } + + startUpload(std::move(file), QStringLiteral(""), format); +} +void +InputBar::startUpload(std::unique_ptr<QIODevice> dev, const QString &orgPath, const QString &format) +{ + auto upload = + UploadHandle(new MediaUpload(std::move(dev), format, orgPath, room->isEncrypted(), this)); + connect(upload.get(), &MediaUpload::uploadComplete, this, &InputBar::finalizeUpload); + + unconfirmedUploads.push_back(std::move(upload)); + + nhlog::ui()->info("Uploads {}", unconfirmedUploads.size()); + emit uploadsChanged(); +} + +void +InputBar::acceptUploads() +{ + if (unconfirmedUploads.empty()) + return; + + bool wasntRunning = runningUploads.empty(); + runningUploads.insert(runningUploads.end(), + std::make_move_iterator(unconfirmedUploads.begin()), + std::make_move_iterator(unconfirmedUploads.end())); + unconfirmedUploads.clear(); + emit uploadsChanged(); + + if (wasntRunning) { + setUploading(true); + runningUploads.front()->startUpload(); + } +} + +void +InputBar::declineUploads() +{ + unconfirmedUploads.clear(); + emit uploadsChanged(); +} + +QVariantList +InputBar::uploads() const +{ + QVariantList l; + l.reserve((int)unconfirmedUploads.size()); + + for (auto &e : unconfirmedUploads) + l.push_back(QVariant::fromValue(e.get())); + return l; } void diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 20c3d17e..607736b6 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h
@@ -5,10 +5,14 @@ #pragma once +#include <QIODevice> #include <QObject> +#include <QSize> #include <QStringList> #include <QTimer> +#include <QVariantList> #include <deque> +#include <memory> #include <mtx/common.hpp> #include <mtx/responses/messages.hpp> @@ -25,12 +29,85 @@ enum class MarkdownOverride OFF, }; +class MediaUpload : public QObject +{ + Q_OBJECT + // Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) + // Q_PROPERTY(MediaType mediaType READ type NOTIFY mediaTypeChanged) + // // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646 + // Q_PROPERTY(QUrl thumbnail READ thumbnail NOTIFY thumbnailChanged) + // Q_PROPERTY(QString humanSize READ humanSize NOTIFY huSizeChanged) + // Q_PROPERTY(QString filename READ filename NOTIFY filenameChanged) + // Q_PROPERTY(QString mimetype READ mimetype NOTIFY mimetypeChanged) + // Q_PROPERTY(int height READ height NOTIFY heightChanged) + // Q_PROPERTY(int width READ width NOTIFY widthChanged) + + // thumbnail video + // https://stackoverflow.com/questions/26229633/display-on-screen-using-qabstractvideosurface + +public: + enum MediaType + { + File, + Image, + Video, + Audio, + }; + Q_ENUM(MediaType); + + explicit MediaUpload(std::unique_ptr<QIODevice> data, + QString mimetype, + QString originalFilename, + bool encrypt, + QObject *parent = nullptr); + + [[nodiscard]] QString url() const { return url_; } + [[nodiscard]] QString mimetype() const { return mimetype_; } + [[nodiscard]] QString mimeClass() const { return mimeClass_; } + [[nodiscard]] QString filename() const { return originalFilename_; } + [[nodiscard]] QString blurhash() const { return blurhash_; } + [[nodiscard]] uint64_t size() const { return size_; } + [[nodiscard]] std::optional<mtx::crypto::EncryptedFile> encryptedFile_() + { + return encryptedFile; + } + [[nodiscard]] QSize dimensions() const { return dimensions_; } + +signals: + void uploadComplete(MediaUpload *self, QString url); + void uploadFailed(MediaUpload *self); + +public slots: + void startUpload(); + +private slots: + void updateThumbnailUrl(QString url) { this->thumbnailUrl_ = std::move(url); } + +public: + // void uploadThumbnail(QImage img); + + std::unique_ptr<QIODevice> source; + QByteArray data; + QString mimetype_; + QString mimeClass_; + QString originalFilename_; + QString blurhash_; + QString thumbnailUrl_; + QString url_; + std::optional<mtx::crypto::EncryptedFile> encryptedFile; + + QSize dimensions_; + uint64_t size_ = 0; + bool encrypt_; +}; + class InputBar : public QObject { Q_OBJECT Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged) Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged) public: explicit InputBar(TimelineModel *parent) @@ -45,6 +122,8 @@ public: connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); } + QVariantList uploads() const; + public slots: [[nodiscard]] QString text() const; QString previousText(); @@ -65,15 +144,22 @@ public slots: void reaction(const QString &reactedEvent, const QString &reactionKey); void sticker(CombinedImagePackModel *model, int row); + void acceptUploads(); + void declineUploads(); + private slots: void startTyping(); void stopTyping(); + void finalizeUpload(MediaUpload *upload, QString url); + void removeRunUpload(MediaUpload *upload); + signals: void insertText(QString text); void textChanged(QString newText); void uploadingChanged(bool value); void containsAtRoomChanged(); + void uploadsChanged(); private: void emote(const QString &body, bool rainbowify); @@ -102,7 +188,9 @@ private: const QString &mime, uint64_t dsize); - void showPreview(const QMimeData &source, const QString &path, const QStringList &formats); + void startUploadFromPath(const QString &path); + void startUploadFromMimeData(const QMimeData &source, const QString &format); + void startUpload(std::unique_ptr<QIODevice> dev, const QString &orgPath, const QString &format); void setUploading(bool value) { if (value != uploading_) { @@ -121,4 +209,16 @@ private: int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; bool uploading_ = false; bool containsAtRoom_ = false; + + struct DeleteLaterDeleter + { + void operator()(QObject *p) + { + if (p) + p->deleteLater(); + } + }; + using UploadHandle = std::unique_ptr<MediaUpload, DeleteLaterDeleter>; + std::vector<UploadHandle> unconfirmedUploads; + std::vector<UploadHandle> runningUploads; };