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;