diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 7421d594..c4820077 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -125,75 +125,7 @@ Item { } - Page { - id: uploadPopup - visible: room && room.input.uploads.length > 0 - Layout.preferredHeight: 200 - clip: true - - Layout.fillWidth: true - - padding: Nheko.paddingMedium - - contentItem: ListView { - id: uploadsList - anchors.horizontalCenter: parent.horizontalCenter - boundsBehavior: Flickable.StopAtBounds - - orientation: ListView.Horizontal - width: Math.min(contentWidth, parent.width) - model: room ? room.input.uploads : undefined - spacing: Nheko.paddingMedium - - delegate: Pane { - padding: Nheko.paddingSmall - height: uploadPopup.availableHeight - buttons.height - width: uploadPopup.availableHeight - buttons.height - - background: Rectangle { - color: Nheko.colors.window - radius: Nheko.paddingMedium - } - contentItem: ColumnLayout { - Image { - Layout.fillHeight: true - Layout.fillWidth: true - - sourceSize.height: height - sourceSize.width: width - - property string typeStr: switch(modelData.mediaType) { - case MediaUpload.Video: return "video-file"; - case MediaUpload.Audio: return "music"; - case MediaUpload.Image: return "image"; - default: return "zip"; - } - source: "image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + Nheko.colors.buttonText - } - MatrixTextField { - Layout.fillWidth: true - text: modelData.filename - onTextEdited: modelData.filename = text - } - } - } - } - - footer: DialogButtonBox { - id: buttons - - standardButtons: DialogButtonBox.Cancel - Button { - text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - } - onAccepted: room.input.acceptUploads() - onRejected: room.input.declineUploads() - } - - background: Rectangle { - color: Nheko.colors.base - } + UploadBox { } NotificationWarning { diff --git a/resources/qml/UploadBox.qml b/resources/qml/UploadBox.qml new file mode 100644 index 00000000..ba00f205 --- /dev/null +++ b/resources/qml/UploadBox.qml @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./components" +import "./ui" + +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Page { + id: uploadPopup + visible: room && room.input.uploads.length > 0 + Layout.preferredHeight: 200 + clip: true + + Layout.fillWidth: true + + padding: Nheko.paddingMedium + + contentItem: ListView { + id: uploadsList + anchors.horizontalCenter: parent.horizontalCenter + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar { + id: scr + } + + orientation: ListView.Horizontal + width: Math.min(contentWidth, parent.availableWidth) + model: room ? room.input.uploads : undefined + spacing: Nheko.paddingMedium + + delegate: Pane { + padding: Nheko.paddingSmall + height: uploadPopup.availableHeight - buttons.height - (scr.visible? scr.height : 0) + width: uploadPopup.availableHeight - buttons.height + + background: Rectangle { + color: Nheko.colors.window + radius: Nheko.paddingMedium + } + contentItem: ColumnLayout { + Image { + Layout.fillHeight: true + Layout.fillWidth: true + + sourceSize.height: height + sourceSize.width: width + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + + property string typeStr: switch(modelData.mediaType) { + case MediaUpload.Video: return "video-file"; + case MediaUpload.Audio: return "music"; + case MediaUpload.Image: return "image"; + default: return "zip"; + } + source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + Nheko.colors.buttonText) + } + MatrixTextField { + Layout.fillWidth: true + text: modelData.filename + onTextEdited: modelData.filename = text + } + } + } + } + + footer: DialogButtonBox { + id: buttons + + standardButtons: DialogButtonBox.Cancel + Button { + text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + onAccepted: room.input.acceptUploads() + onRejected: room.input.declineUploads() + } + + background: Rectangle { + color: Nheko.colors.base + } +} diff --git a/resources/res.qrc b/resources/res.qrc index ad86c88d..3b762d20 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -99,6 +99,7 @@ qml/MatrixText.qml qml/MatrixTextField.qml qml/ToggleButton.qml + qml/UploadBox.qml qml/MessageInput.qml qml/MessageView.qml qml/NhekoBusyIndicator.qml diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 00cea86e..e4dfe92e 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -139,6 +139,19 @@ struct EventFile } }; +struct EventThumbnailFile +{ + template + using file_t = decltype(Content::info.thumbnail_file); + template + std::optional operator()(const mtx::events::Event &e) + { + if constexpr (is_detected::value) + return e.content.info.thumbnail_file; + return std::nullopt; + } +}; + struct EventUrl { template @@ -163,6 +176,8 @@ struct EventThumbnailUrl std::string operator()(const mtx::events::Event &e) { if constexpr (is_detected::value) { + if (auto file = EventThumbnailFile{}(e)) + return file->url; return e.content.info.thumbnail_url; } return ""; @@ -424,6 +439,12 @@ mtx::accessors::file(const mtx::events::collections::TimelineEvents &event) return std::visit(EventFile{}, event); } +std::optional +mtx::accessors::thumbnail_file(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventThumbnailFile{}, event); +} + std::string mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) { diff --git a/src/EventAccessors.h b/src/EventAccessors.h index a74c58bc..9d8a34e7 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -78,6 +78,8 @@ formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event) std::optional file(const mtx::events::collections::TimelineEvents &event); +std::optional +thumbnail_file(const mtx::events::collections::TimelineEvents &event); std::string url(const mtx::events::collections::TimelineEvents &event); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index aa470989..e1223021 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -39,6 +38,20 @@ static constexpr size_t INPUT_HISTORY_SIZE = 10; +QUrl +MediaUpload::thumbnailDataUrl() const +{ + if (thumbnail_.isNull()) + return {}; + + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + thumbnail_.save(&buffer, "PNG"); + QString base64 = QString::fromUtf8(byteArray.toBase64()); + return QString("data:image/png;base64,") + base64; +} + bool InputVideoSurface::present(const QVideoFrame &frame) { @@ -465,6 +478,10 @@ InputBar::image(const QString &filename, const QString &mime, uint64_t dsize, const QSize &dimensions, + const std::optional &thumbnailEncryptedFile, + const QString &thumbnailUrl, + uint64_t thumbnailSize, + const QSize &thumbnailDimensions, const QString &blurhash) { mtx::events::msg::Image image; @@ -480,6 +497,18 @@ InputBar::image(const QString &filename, else image.url = url.toStdString(); + if (!thumbnailUrl.isEmpty()) { + if (thumbnailEncryptedFile) + image.info.thumbnail_file = thumbnailEncryptedFile; + else + image.info.thumbnail_url = thumbnailUrl.toStdString(); + + image.info.thumbnail_info.h = thumbnailDimensions.height(); + image.info.thumbnail_info.w = thumbnailDimensions.width(); + image.info.thumbnail_info.size = thumbnailSize; + image.info.thumbnail_info.mimetype = "image/png"; + } + if (!room->reply().isEmpty()) { image.relations.relations.push_back( {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); @@ -566,11 +595,13 @@ InputBar::video(const QString &filename, const std::optional &thumbnailEncryptedFile, const QString &thumbnailUrl, uint64_t thumbnailSize, - const QSize &thumbnailDimensions) + const QSize &thumbnailDimensions, + const QString &blurhash) { mtx::events::msg::Video video; video.info.mimetype = mime.toStdString(); video.info.size = dsize; + video.info.blurhash = blurhash.toStdString(); video.body = filename.toStdString(); if (duration > 0) @@ -946,7 +977,17 @@ InputBar::finalizeUpload(MediaUpload *upload, QString url) auto size = upload->size(); auto encryptedFile = upload->encryptedFile_(); if (mimeClass == u"image") - image(filename, encryptedFile, url, mime, size, upload->dimensions(), upload->blurhash()); + image(filename, + encryptedFile, + url, + mime, + size, + upload->dimensions(), + upload->thumbnailEncryptedFile_(), + upload->thumbnailUrl(), + upload->thumbnailSize(), + upload->thumbnailImg().size(), + upload->blurhash()); else if (mimeClass == u"audio") audio(filename, encryptedFile, url, mime, size, upload->duration()); else if (mimeClass == u"video") @@ -960,7 +1001,8 @@ InputBar::finalizeUpload(MediaUpload *upload, QString url) upload->thumbnailEncryptedFile_(), upload->thumbnailUrl(), upload->thumbnailSize(), - upload->thumbnailImg().size()); + upload->thumbnailImg().size(), + upload->blurhash()); else file(filename, encryptedFile, url, mime, size); diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 4472fe84..28a4bcf6 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -52,15 +53,11 @@ signals: class MediaUpload : public QObject { Q_OBJECT - // Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) Q_PROPERTY(int mediaType READ type NOTIFY mediaTypeChanged) - // // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646 - // Q_PROPERTY(QUrl thumbnail READ thumbnail NOTIFY thumbnailChanged) + // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646 + Q_PROPERTY(QUrl thumbnail READ thumbnailDataUrl NOTIFY thumbnailChanged) // Q_PROPERTY(QString humanSize READ humanSize NOTIFY huSizeChanged) Q_PROPERTY(QString filename READ filename WRITE setFilename 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 @@ -111,6 +108,7 @@ public: QImage thumbnailImg() const { return thumbnail_; } QString thumbnailUrl() const { return thumbnailUrl_; } + QUrl thumbnailDataUrl() const; [[nodiscard]] uint64_t thumbnailSize() const { return thumbnailSize_; } void setFilename(QString fn) @@ -125,13 +123,18 @@ signals: void uploadComplete(MediaUpload *self, QString url); void uploadFailed(MediaUpload *self); void filenameChanged(); + void thumbnailChanged(); void mediaTypeChanged(); public slots: void startUpload(); private slots: - void setThumbnail(QImage img) { this->thumbnail_ = std::move(img); } + void setThumbnail(QImage img) + { + this->thumbnail_ = std::move(img); + emit thumbnailChanged(); + } public: // void uploadThumbnail(QImage img); @@ -225,6 +228,10 @@ private: const QString &mime, uint64_t dsize, const QSize &dimensions, + const std::optional &thumbnailEncryptedFile, + const QString &thumbnailUrl, + uint64_t thumbnailSize, + const QSize &thumbnailDimensions, const QString &blurhash); void file(const QString &filename, const std::optional &encryptedFile, @@ -245,9 +252,10 @@ private: uint64_t duration, const QSize &dimensions, const std::optional &thumbnailEncryptedFile, - const QString &thumnailUrl, - uint64_t thumnailSize, - const QSize &thumbnailDimensions); + const QString &thumbnailUrl, + uint64_t thumbnailSize, + const QSize &thumbnailDimensions, + const QString &blurhash); void startUploadFromPath(const QString &path); void startUploadFromMimeData(const QMimeData &source, const QString &format); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 4c1ce2dc..28d8f0bb 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1367,6 +1367,10 @@ struct SendMessageVisitor if (encInfo) emit model_->newEncryptedImage(encInfo.value()); + encInfo = mtx::accessors::thumbnail_file(msg); + if (encInfo) + emit model_->newEncryptedImage(encInfo.value()); + model_->sendEncryptedMessage(msg, Event); } else { msg.type = Event;