diff --git a/CMakeLists.txt b/CMakeLists.txt
index 027bc44e..550a3aa4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -311,7 +311,6 @@ set(SRC_FILES
# Dialogs
src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp
- src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp
# Emoji
@@ -509,7 +508,6 @@ qt5_wrap_cpp(MOC_HEADERS
# Dialogs
src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h
- src/dialogs/PreviewUploadOverlay.h
src/dialogs/ReCaptcha.h
# Emoji
diff --git a/resources/icons/ui/image.svg b/resources/icons/ui/image.svg
new file mode 100644
index 00000000..ca73a76e
--- /dev/null
+++ b/resources/icons/ui/image.svg
@@ -0,0 +1 @@
+
diff --git a/resources/icons/ui/music.svg b/resources/icons/ui/music.svg
new file mode 100644
index 00000000..5f72b736
--- /dev/null
+++ b/resources/icons/ui/music.svg
@@ -0,0 +1 @@
+
diff --git a/resources/icons/ui/video-file.svg b/resources/icons/ui/video-file.svg
new file mode 100644
index 00000000..08c0a6bb
--- /dev/null
+++ b/resources/icons/ui/video-file.svg
@@ -0,0 +1 @@
+
diff --git a/resources/icons/ui/zip.svg b/resources/icons/ui/zip.svg
new file mode 100644
index 00000000..e22534d6
--- /dev/null
+++ b/resources/icons/ui/zip.svg
@@ -0,0 +1 @@
+
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 4fce9a75..bbe61ee9 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -376,6 +376,7 @@ Item {
required property string filesize
required property string url
required property string thumbnailUrl
+ required property string duration
required property bool isOnlyEmoji
required property bool isSender
required property bool isEncrypted
@@ -492,6 +493,7 @@ Item {
filesize: wrapper.filesize
url: wrapper.url
thumbnailUrl: wrapper.thumbnailUrl
+ duration: wrapper.duration
isOnlyEmoji: wrapper.isOnlyEmoji
isSender: wrapper.isSender
isEncrypted: wrapper.isEncrypted
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index bb6514d1..032821ba 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -41,6 +41,7 @@ Item {
required property var reactions
required property int trustlevel
required property int encryptionError
+ required property int duration
required property var timestamp
required property int status
required property int relatedEventCacheBuster
@@ -128,6 +129,7 @@ Item {
userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
+ duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? ""
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
@@ -154,6 +156,7 @@ Item {
typeString: r.typeString ?? ""
url: r.url
thumbnailUrl: r.thumbnailUrl
+ duration: r.duration
originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 1933baeb..c4820077 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -124,6 +124,10 @@ Item {
color: Nheko.theme.separator
}
+
+ 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/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 08b2098e..0e211ded 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -18,6 +18,7 @@ Item {
required property int type
required property string typeString
required property int originalWidth
+ required property int duration
required property string blurhash
required property string body
required property string formattedBody
@@ -161,6 +162,7 @@ Item {
url: d.url
body: d.body
filesize: d.filesize
+ duration: d.duration
metadataWidth: d.metadataWidth
}
@@ -178,6 +180,7 @@ Item {
url: d.url
body: d.body
filesize: d.filesize
+ duration: d.duration
metadataWidth: d.metadataWidth
}
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 5d7beaad..4828843c 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -17,6 +17,7 @@ Item {
required property double proportionalHeight
required property int type
required property int originalWidth
+ required property int duration
required property string thumbnailUrl
required property string eventId
required property string url
@@ -57,7 +58,7 @@ Item {
Image {
anchors.fill: parent
- source: thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale"
+ source: thumbnailUrl ? thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale" : ""
asynchronous: true
fillMode: Image.PreserveAspectFit
@@ -85,7 +86,7 @@ Item {
anchors.bottom: fileInfoLabel.top
playingVideo: type == MtxEvent.VideoMessage
positionValue: mxcmedia.position
- duration: mxcmedia.duration
+ duration: mediaLoaded ? mxcmedia.duration : content.duration
mediaLoaded: mxcmedia.loaded
mediaState: mxcmedia.state
onPositionChanged: mxcmedia.position = position
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 513b7c0b..27fb4e07 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -34,6 +34,7 @@ Item {
property string roomTopic
property string roomName
property string callType
+ property int duration
property int encryptionError
property int relatedEventCacheBuster
property int maxWidth
@@ -112,6 +113,7 @@ Item {
typeString: r.typeString ?? ""
url: r.url
thumbnailUrl: r.thumbnailUrl
+ duration: r.duration
originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent
diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml
index 1844af73..d73957ee 100644
--- a/resources/qml/ui/media/MediaControls.qml
+++ b/resources/qml/ui/media/MediaControls.qml
@@ -214,7 +214,7 @@ Rectangle {
Label {
Layout.alignment: Qt.AlignRight
- text: (!control.mediaLoaded) ? "-- / --" : (durationToString(control.positionValue) + " / " + durationToString(control.duration))
+ text: (!control.mediaLoaded ? "-- " : durationToString(control.positionValue)) + " / " + durationToString(control.duration)
color: Nheko.colors.text
}
diff --git a/resources/res.qrc b/resources/res.qrc
index a383f805..3b762d20 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -46,6 +46,10 @@
icons/ui/volume-off-indicator.svg
icons/ui/volume-up.svg
icons/ui/world.svg
+ icons/ui/music.svg
+ icons/ui/image.svg
+ icons/ui/zip.svg
+ icons/ui/video-file.svg
icons/emoji-categories/activity.svg
icons/emoji-categories/flags.svg
icons/emoji-categories/foods.svg
@@ -95,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 935ff73a..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,12 +176,28 @@ 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 "";
}
};
+struct EventDuration
+{
+ template
+ using thumbnail_url_t = decltype(Content::info.duration);
+ template
+ uint64_t operator()(const mtx::events::Event &e)
+ {
+ if constexpr (is_detected::value) {
+ return e.content.info.duration;
+ }
+ return 0;
+ }
+};
+
struct EventBlurhash
{
template
@@ -410,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)
{
@@ -420,6 +455,11 @@ mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &ev
{
return std::visit(EventThumbnailUrl{}, event);
}
+uint64_t
+mtx::accessors::duration(const mtx::events::collections::TimelineEvents &event)
+{
+ return std::visit(EventDuration{}, event);
+}
std::string
mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event)
{
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index e46d4786..9d8a34e7 100644
--- a/src/EventAccessors.h
+++ b/src/EventAccessors.h
@@ -78,11 +78,15 @@ 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);
std::string
thumbnail_url(const mtx::events::collections::TimelineEvents &event);
+uint64_t
+duration(const mtx::events::collections::TimelineEvents &event);
std::string
blurhash(const mtx::events::collections::TimelineEvents &event);
std::string
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 73e556f7..c4af7f0c 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -251,6 +251,8 @@ MainWindow::registerQmlTypes()
qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
+ qmlRegisterUncreatableType(
+ "im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml"));
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
"im.nheko.EmojiModel",
1,
diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp
deleted file mode 100644
index a00358d9..00000000
--- a/src/dialogs/PreviewUploadOverlay.cpp
+++ /dev/null
@@ -1,223 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "dialogs/PreviewUploadOverlay.h"
-
-#include "Config.h"
-#include "Logging.h"
-#include "MainWindow.h"
-#include "Utils.h"
-
-using namespace dialogs;
-
-constexpr const char *DEFAULT = "Upload %1?";
-constexpr const char *ERR_MSG = "Failed to load image type '%1'. Continue upload?";
-
-PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
- : QWidget{parent}
- , titleLabel_{this}
- , fileName_{this}
- , upload_{tr("Upload"), this}
- , cancel_{tr("Cancel"), this}
-{
- auto hlayout = new QHBoxLayout;
- hlayout->setContentsMargins(0, 0, 0, 0);
- hlayout->addStretch(1);
- hlayout->addWidget(&cancel_);
- hlayout->addWidget(&upload_);
-
- auto vlayout = new QVBoxLayout{this};
- vlayout->addWidget(&titleLabel_);
- vlayout->addWidget(&infoLabel_);
- vlayout->addWidget(&fileName_);
- vlayout->addLayout(hlayout);
- vlayout->setSpacing(conf::modals::WIDGET_SPACING);
- vlayout->setContentsMargins(conf::modals::WIDGET_MARGIN,
- conf::modals::WIDGET_MARGIN,
- conf::modals::WIDGET_MARGIN,
- conf::modals::WIDGET_MARGIN);
-
- upload_.setDefault(true);
- connect(&upload_, &QPushButton::clicked, this, [this]() {
- emit confirmUpload(data_, mediaType_, fileName_.text());
- close();
- });
-
- connect(&fileName_, &QLineEdit::returnPressed, this, [this]() {
- emit confirmUpload(data_, mediaType_, fileName_.text());
- close();
- });
-
- connect(&cancel_, &QPushButton::clicked, this, [this]() {
- emit aborted();
- close();
- });
-}
-
-void
-PreviewUploadOverlay::init()
-{
- QSize winsize;
- QPoint center;
-
- auto window = MainWindow::instance();
- if (window) {
- winsize = window->frameGeometry().size();
- center = window->frameGeometry().center();
- } else {
- nhlog::ui()->warn("unable to retrieve MainWindow's size");
- }
-
- fileName_.setText(QFileInfo{filePath_}.fileName());
-
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
-
- QFont font;
- font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
- titleLabel_.setFont(font);
- titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- titleLabel_.setAlignment(Qt::AlignCenter);
- infoLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
- fileName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
- fileName_.setAlignment(Qt::AlignCenter);
- upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
- cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
-
- if (isImage_) {
- infoLabel_.setAlignment(Qt::AlignCenter);
-
- const auto maxWidth = winsize.width() * 0.8;
- const auto maxHeight = winsize.height() * 0.8;
-
- // Scale image preview to fit into the application window.
- infoLabel_.setPixmap(utils::scaleDown(maxWidth, maxHeight, image_));
- move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
- } else {
- infoLabel_.setAlignment(Qt::AlignLeft);
- }
- infoLabel_.setScaledContents(false);
-
- show();
-}
-
-void
-PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64_t upload_size)
-{
- if (mediaType_.split('/')[0] == QLatin1String("image")) {
- if (!image_.loadFromData(data_)) {
- titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
- } else {
- titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_));
- }
- isImage_ = true;
- } else {
- auto const info = QString{tr("Media type: %1\n"
- "Media size: %2\n")}
- .arg(mime, utils::humanReadableFileSize(upload_size));
-
- titleLabel_.setText(QString{tr(DEFAULT)}.arg(QStringLiteral("file")));
- infoLabel_.setText(info);
- }
-}
-
-void
-PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime)
-{
- nhlog::ui()->info(
- "Pasting image with size: {}x{}, format: {}", src.height(), src.width(), mime.toStdString());
-
- auto const &split = mime.split('/');
- auto const &type = split[1];
-
- QBuffer buffer(&data_);
- buffer.open(QIODevice::WriteOnly);
- if (src.save(&buffer, type.toStdString().c_str()))
- titleLabel_.setText(QString{tr(DEFAULT)}.arg(QStringLiteral("image")));
- else
- titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type));
-
- mediaType_ = mime;
- filePath_ = "clipboard." + type;
- image_.convertFromImage(src);
- isImage_ = true;
-
- titleLabel_.setText(QString{tr(DEFAULT)}.arg(QStringLiteral("image")));
- init();
-}
-
-void
-PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime)
-{
- nhlog::ui()->info("Pasting {} bytes of data, mimetype {}", data.size(), mime.toStdString());
-
- auto const &split = mime.split('/');
- auto const &type = split[1];
-
- data_ = data;
- mediaType_ = mime;
- filePath_ = "clipboard." + type;
- isImage_ = false;
-
- if (mime == QLatin1String("image/svg+xml")) {
- isImage_ = true;
- image_.loadFromData(data_, mediaType_.toStdString().c_str());
- }
-
- setLabels(type, mime, data_.size());
- init();
-}
-
-void
-PreviewUploadOverlay::setPreview(const QString &path)
-{
- QFile file{path};
-
- if (!file.open(QIODevice::ReadOnly)) {
- nhlog::ui()->warn(
- "Failed to open file ({}): {}", path.toStdString(), file.errorString().toStdString());
- close();
- return;
- }
-
- QMimeDatabase db;
- auto mime = db.mimeTypeForFileNameAndData(path, &file);
-
- if ((data_ = file.readAll()).isEmpty()) {
- nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString());
- close();
- return;
- }
-
- auto const &split = mime.name().split('/');
-
- mediaType_ = mime.name();
- filePath_ = file.fileName();
- isImage_ = false;
-
- setLabels(split[1], mime.name(), data_.size());
- init();
-}
-
-void
-PreviewUploadOverlay::keyPressEvent(QKeyEvent *event)
-{
- if (event->matches(QKeySequence::Cancel)) {
- emit aborted();
- close();
- } else {
- QWidget::keyPressEvent(event);
- }
-}
diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h
deleted file mode 100644
index aa72d4d2..00000000
--- a/src/dialogs/PreviewUploadOverlay.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-class QMimeData;
-
-namespace dialogs {
-
-class PreviewUploadOverlay : public QWidget
-{
- Q_OBJECT
-public:
- PreviewUploadOverlay(QWidget *parent = nullptr);
-
- void setPreview(const QImage &src, const QString &mime);
- void setPreview(const QByteArray data, const QString &mime);
- void setPreview(const QString &path);
- void keyPressEvent(QKeyEvent *event);
-
-signals:
- void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
- void aborted();
-
-private:
- void init();
- void setLabels(const QString &type, const QString &mime, uint64_t upload_size);
-
- bool isImage_;
- QPixmap image_;
-
- QByteArray data_;
- QString filePath_;
- QString mediaType_;
-
- QLabel titleLabel_;
- QLabel infoLabel_;
- QLineEdit fileName_;
-
- QPushButton upload_;
- QPushButton cancel_;
-};
-} // dialogs
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 349ce7af..e1223021 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -5,16 +5,18 @@
#include "InputBar.h"
+#include
#include
#include
#include
#include
#include
+#include
+#include
#include
#include
#include
#include
-#include
#include
#include
@@ -31,12 +33,97 @@
#include "TimelineViewManager.h"
#include "UserSettingsPage.h"
#include "Utils.h"
-#include "dialogs/PreviewUploadOverlay.h"
#include "blurhash.hpp"
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)
+{
+ QImage::Format format = QImage::Format_Invalid;
+
+ switch (frame.pixelFormat()) {
+ case QVideoFrame::Format_ARGB32:
+ format = QImage::Format_ARGB32;
+ break;
+ case QVideoFrame::Format_ARGB32_Premultiplied:
+ format = QImage::Format_ARGB32_Premultiplied;
+ break;
+ case QVideoFrame::Format_RGB24:
+ format = QImage::Format_RGB888;
+ break;
+ case QVideoFrame::Format_BGR24:
+ format = QImage::Format_BGR888;
+ break;
+ case QVideoFrame::Format_RGB32:
+ format = QImage::Format_RGB32;
+ break;
+ case QVideoFrame::Format_RGB565:
+ format = QImage::Format_RGB16;
+ break;
+ case QVideoFrame::Format_RGB555:
+ format = QImage::Format_RGB555;
+ break;
+ default:
+ format = QImage::Format_Invalid;
+ }
+
+ if (format == QImage::Format_Invalid) {
+ emit newImage({});
+ return false;
+ } else {
+ QVideoFrame frametodraw(frame);
+
+ if (!frametodraw.map(QAbstractVideoBuffer::ReadOnly)) {
+ emit newImage({});
+ return false;
+ }
+
+ // this is a shallow operation. it just refer the frame buffer
+ QImage image(frametodraw.bits(),
+ frametodraw.width(),
+ frametodraw.height(),
+ frametodraw.bytesPerLine(),
+ format);
+
+ emit newImage(std::move(image));
+ return true;
+ }
+}
+
+QList
+InputVideoSurface::supportedPixelFormats(QAbstractVideoBuffer::HandleType type) const
+{
+ if (type == QAbstractVideoBuffer::NoHandle) {
+ return {
+ QVideoFrame::Format_ARGB32,
+ QVideoFrame::Format_ARGB32_Premultiplied,
+ QVideoFrame::Format_RGB24,
+ QVideoFrame::Format_BGR24,
+ QVideoFrame::Format_RGB32,
+ QVideoFrame::Format_RGB565,
+ QVideoFrame::Format_RGB555,
+ };
+ } else {
+ return {};
+ }
+}
+
void
InputBar::paste(bool fromMouse)
{
@@ -67,29 +154,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 +189,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 +347,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
@@ -424,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;
@@ -439,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()});
@@ -485,7 +555,8 @@ InputBar::audio(const QString &filename,
const std::optional &file,
const QString &url,
const QString &mime,
- uint64_t dsize)
+ uint64_t dsize,
+ uint64_t duration)
{
mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString();
@@ -493,6 +564,9 @@ InputBar::audio(const QString &filename,
audio.body = filename.toStdString();
audio.url = url.toStdString();
+ if (duration > 0)
+ audio.info.duration = duration;
+
if (file)
audio.file = file;
else
@@ -515,18 +589,45 @@ InputBar::video(const QString &filename,
const std::optional &file,
const QString &url,
const QString &mime,
- uint64_t dsize)
+ uint64_t dsize,
+ uint64_t duration,
+ const QSize &dimensions,
+ const std::optional &thumbnailEncryptedFile,
+ const QString &thumbnailUrl,
+ uint64_t thumbnailSize,
+ 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)
+ video.info.duration = duration;
+ if (dimensions.isValid()) {
+ video.info.h = dimensions.height();
+ video.info.w = dimensions.width();
+ }
+
if (file)
video.file = file;
else
video.url = url.toStdString();
+ if (!thumbnailUrl.isEmpty()) {
+ if (thumbnailEncryptedFile)
+ video.info.thumbnail_file = thumbnailEncryptedFile;
+ else
+ video.info.thumbnail_url = thumbnailUrl.toStdString();
+
+ video.info.thumbnail_info.h = thumbnailDimensions.height();
+ video.info.thumbnail_info.w = thumbnailDimensions.width();
+ video.info.thumbnail_info.size = thumbnailSize;
+ video.info.thumbnail_info.mimetype = "image/png";
+ }
+
if (!room->reply().isEmpty()) {
video.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
@@ -661,125 +762,351 @@ InputBar::command(const QString &command, QString args)
}
}
-void
-InputBar::showPreview(const QMimeData &source, const QString &path, const QStringList &formats)
+MediaUpload::MediaUpload(std::unique_ptr 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(source.imageData()), formats.front());
- } else {
- // unknown image format, default to image/png
- previewDialog_->setPreview(qvariant_cast(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();
+ source->reset();
+
+ 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);
+ setThumbnail(img.scaled(
+ std::min(800, img.width()), std::min(800, img.height()), Qt::KeepAspectRatioByExpanding));
- 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());
+ dimensions_ = img.size();
+ if (img.height() > 200 && img.width() > 360)
+ img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
+ std::vector 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(qRed(p)));
+ data_.push_back(static_cast(qGreen(p)));
+ data_.push_back(static_cast(qBlue(p)));
+ }
+ }
+ blurhash_ =
+ QString::fromStdString(blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
+ } else if (mimeClass_ == u"video" || mimeClass_ == u"audio") {
+ auto mediaPlayer = new QMediaPlayer(
+ this,
+ mimeClass_ == u"video" ? QFlags{QMediaPlayer::StreamPlayback, QMediaPlayer::VideoSurface}
+ : QFlags{QMediaPlayer::StreamPlayback});
+ mediaPlayer->setMuted(true);
+
+ if (mimeClass_ == u"video") {
+ auto newSurface = new InputVideoSurface(this);
+ connect(
+ newSurface, &InputVideoSurface::newImage, this, [this, mediaPlayer](QImage img) {
+ mediaPlayer->stop();
+
+ auto orientation = mediaPlayer->metaData(QMediaMetaData::Orientation).toInt();
+ if (orientation == 90 || orientation == 270 || orientation == 180) {
+ img =
+ img.transformed(QTransform().rotate(orientation), Qt::SmoothTransformation);
+ }
+
+ nhlog::ui()->debug("Got image {}x{}", img.width(), img.height());
+
+ this->setThumbnail(img);
+
+ if (!dimensions_.isValid())
+ this->dimensions_ = img.size();
+
+ if (img.height() > 200 && img.width() > 360)
+ img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
+ std::vector 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(qRed(p)));
+ data_.push_back(static_cast(qGreen(p)));
+ data_.push_back(static_cast(qBlue(p)));
+ }
+ }
+ blurhash_ = QString::fromStdString(
+ blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
+ });
+ mediaPlayer->setVideoOutput(newSurface);
+ }
+
+ connect(mediaPlayer,
+ qOverload(&QMediaPlayer::error),
+ this,
+ [mediaPlayer](QMediaPlayer::Error error) {
+ nhlog::ui()->debug("Media player error {} and errorStr {}",
+ error,
+ mediaPlayer->errorString().toStdString());
+ });
+ connect(mediaPlayer,
+ &QMediaPlayer::mediaStatusChanged,
+ [mediaPlayer](QMediaPlayer::MediaStatus status) {
+ nhlog::ui()->debug(
+ "Media player status {} and error {}", status, mediaPlayer->error());
+ });
+ connect(mediaPlayer,
+ qOverload(&QMediaPlayer::metaDataChanged),
+ [this, mediaPlayer](QString t, QVariant) {
+ nhlog::ui()->debug("Got metadata {}", t.toStdString());
+
+ if (mediaPlayer->duration() > 0)
+ this->duration_ = mediaPlayer->duration();
+
+ dimensions_ = mediaPlayer->metaData(QMediaMetaData::Resolution).toSize();
+ auto orientation = mediaPlayer->metaData(QMediaMetaData::Orientation).toInt();
+ if (orientation == 90 || orientation == 270) {
+ dimensions_.transpose();
+ }
+ });
+ connect(mediaPlayer, &QMediaPlayer::durationChanged, [this, mediaPlayer](qint64 duration) {
+ if (duration > 0) {
+ this->duration_ = mediaPlayer->duration();
+ if (mimeClass_ == u"audio")
+ mediaPlayer->stop();
+ }
+ nhlog::ui()->debug("Duration changed {}", duration);
+ });
+ mediaPlayer->setMedia(QMediaContent(originalFilename_), source.get());
+
+ mediaPlayer->play();
+ }
+}
+
+void
+MediaUpload::startUpload()
+{
+ if (!thumbnail_.isNull() && thumbnailUrl_.isEmpty()) {
+ QByteArray ba;
+ QBuffer buffer(&ba);
+ buffer.open(QIODevice::WriteOnly);
+ thumbnail_.save(&buffer, "PNG");
+ auto payload = std::string(ba.data(), ba.size());
+ if (encrypt_) {
+ mtx::crypto::BinaryBuf buf;
+ std::tie(buf, thumbnailEncryptedFile) = mtx::crypto::encrypt_file(std::move(payload));
+ payload = mtx::crypto::to_string(buf);
+ }
+ thumbnailSize_ = payload.size();
+
+ http::client()->upload(
+ payload,
+ encryptedFile ? "application/octet-stream" : "image/png",
+ "",
+ [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(err->status_code));
+ thumbnail_ = QImage();
+ startUpload();
+ return;
+ }
+
+ thumbnailUrl_ = QString::fromStdString(res.content_uri);
+ if (thumbnailEncryptedFile)
+ thumbnailEncryptedFile->url = res.content_uri;
+
+ startUpload();
+ });
+ return;
+ }
+
+ 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();
+
+ http::client()->upload(
+ payload,
+ encryptedFile ? "application/octet-stream" : mimetype_.toStdString(),
+ encrypt_ ? "" : 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(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 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.left(mime.indexOf(u'/'));
- nhlog::ui()->debug("Mime: {}", mime.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 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(qRed(p)));
- data_.push_back(static_cast(qGreen(p)));
- data_.push_back(static_cast(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(err->status_code));
- setUploading(false);
- return;
- }
-
- auto url = QString::fromStdString(res.content_uri);
- if (encryptedFile)
- encryptedFile->url = res.content_uri;
-
- 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);
-
- setUploading(false);
- });
+ emit uploadComplete(this, std::move(url));
});
}
+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->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")
+ video(filename,
+ encryptedFile,
+ url,
+ mime,
+ size,
+ upload->duration(),
+ upload->dimensions(),
+ upload->thumbnailEncryptedFile_(),
+ upload->thumbnailUrl(),
+ upload->thumbnailSize(),
+ upload->thumbnailImg().size(),
+ upload->blurhash());
+ else
+ file(filename, encryptedFile, url, mime, size);
+
+ removeRunUpload(upload);
+}
+
+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);
+
+ if (runningUploads.empty())
+ setUploading(false);
+ else
+ runningUploads.front()->startUpload();
+}
+
+void
+InputBar::startUploadFromPath(const QString &path)
+{
+ if (path.isEmpty())
+ return;
+
+ auto file = std::make_unique(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();
+ 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), {}, format);
+}
+void
+InputBar::startUpload(std::unique_ptr 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()->debug("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
InputBar::startTyping()
{
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 20c3d17e..28a4bcf6 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -5,10 +5,17 @@
#pragma once
+#include
+#include
+#include
#include
+#include
#include
#include
+#include
+#include
#include
+#include
#include
#include
@@ -25,12 +32,139 @@ enum class MarkdownOverride
OFF,
};
+class InputVideoSurface : public QAbstractVideoSurface
+{
+ Q_OBJECT
+
+public:
+ InputVideoSurface(QObject *parent)
+ : QAbstractVideoSurface(parent)
+ {}
+
+ bool present(const QVideoFrame &frame) override;
+
+ QList
+ supportedPixelFormats(QAbstractVideoBuffer::HandleType type) const override;
+
+signals:
+ void newImage(QImage img);
+};
+
+class MediaUpload : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(int mediaType READ type NOTIFY mediaTypeChanged)
+ // 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)
+
+ // 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 data,
+ QString mimetype,
+ QString originalFilename,
+ bool encrypt,
+ QObject *parent = nullptr);
+
+ [[nodiscard]] int type() const
+ {
+ if (mimeClass_ == u"video")
+ return MediaType::Video;
+ else if (mimeClass_ == u"audio")
+ return MediaType::Audio;
+ else if (mimeClass_ == u"image")
+ return MediaType::Image;
+ else
+ return MediaType::File;
+ }
+ [[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]] uint64_t duration() const { return duration_; }
+ [[nodiscard]] std::optional encryptedFile_()
+ {
+ return encryptedFile;
+ }
+ [[nodiscard]] std::optional thumbnailEncryptedFile_()
+ {
+ return thumbnailEncryptedFile;
+ }
+ [[nodiscard]] QSize dimensions() const { return dimensions_; }
+
+ QImage thumbnailImg() const { return thumbnail_; }
+ QString thumbnailUrl() const { return thumbnailUrl_; }
+ QUrl thumbnailDataUrl() const;
+ [[nodiscard]] uint64_t thumbnailSize() const { return thumbnailSize_; }
+
+ void setFilename(QString fn)
+ {
+ if (fn != originalFilename_) {
+ originalFilename_ = std::move(fn);
+ emit filenameChanged();
+ }
+ }
+
+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);
+ emit thumbnailChanged();
+ }
+
+public:
+ // void uploadThumbnail(QImage img);
+
+ std::unique_ptr source;
+ QByteArray data;
+ QString mimetype_;
+ QString mimeClass_;
+ QString originalFilename_;
+ QString blurhash_;
+ QString thumbnailUrl_;
+ QString url_;
+ std::optional encryptedFile, thumbnailEncryptedFile;
+
+ QImage thumbnail_;
+
+ QSize dimensions_;
+ uint64_t size_ = 0;
+ uint64_t thumbnailSize_ = 0;
+ uint64_t duration_ = 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 +179,8 @@ public:
connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping);
}
+ QVariantList uploads() const;
+
public slots:
[[nodiscard]] QString text() const;
QString previousText();
@@ -65,15 +201,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);
@@ -85,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,
@@ -95,14 +242,24 @@ private:
const std::optional &file,
const QString &url,
const QString &mime,
- uint64_t dsize);
+ uint64_t dsize,
+ uint64_t duration);
void video(const QString &filename,
const std::optional &file,
const QString &url,
const QString &mime,
- uint64_t dsize);
+ uint64_t dsize,
+ uint64_t duration,
+ const QSize &dimensions,
+ const std::optional &thumbnailEncryptedFile,
+ const QString &thumbnailUrl,
+ uint64_t thumbnailSize,
+ const QSize &thumbnailDimensions,
+ const QString &blurhash);
- 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 dev, const QString &orgPath, const QString &format);
void setUploading(bool value)
{
if (value != uploading_) {
@@ -121,4 +278,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;
+ std::vector unconfirmedUploads;
+ std::vector runningUploads;
};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8e6c7235..28d8f0bb 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -474,6 +474,7 @@ TimelineModel::roleNames() const
{Timestamp, "timestamp"},
{Url, "url"},
{ThumbnailUrl, "thumbnailUrl"},
+ {Duration, "duration"},
{Blurhash, "blurhash"},
{Filename, "filename"},
{Filesize, "filesize"},
@@ -627,6 +628,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl:
return QVariant(QString::fromStdString(thumbnail_url(event)));
+ case Duration:
+ return QVariant(static_cast(duration(event)));
case Blurhash:
return QVariant(QString::fromStdString(blurhash(event)));
case Filename:
@@ -739,6 +742,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
m.insert(names[Timestamp], data(event, static_cast(Timestamp)));
m.insert(names[Url], data(event, static_cast(Url)));
m.insert(names[ThumbnailUrl], data(event, static_cast(ThumbnailUrl)));
+ m.insert(names[Duration], data(event, static_cast(Duration)));
m.insert(names[Blurhash], data(event, static_cast(Blurhash)));
m.insert(names[Filename], data(event, static_cast(Filename)));
m.insert(names[Filesize], data(event, static_cast(Filesize)));
@@ -1363,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;
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index f47203f0..7e21a394 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -215,6 +215,7 @@ public:
Timestamp,
Url,
ThumbnailUrl,
+ Duration,
Blurhash,
Filename,
Filesize,