Merge pull request #1008 from Nheko-Reborn/new-upload

New upload flow
This commit is contained in:
DeepBlueV7.X 2022-03-21 06:11:46 +01:00 committed by GitHub
commit 15ff5dace5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 822 additions and 436 deletions

View file

@ -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

View file

@ -0,0 +1 @@
<svg width="512" height="512" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm.58 16.401-5.805-5.686a.75.75 0 0 0-.966-.071l-.084.07-5.807 5.687c.182.064.378.099.582.099h11.5c.203 0 .399-.035.58-.099l-5.805-5.686L18.33 19.4ZM17.75 4.5H6.25A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .208.036.408.103.594l5.823-5.701a2.25 2.25 0 0 1 3.02-.116l.128.116 5.822 5.702c.067-.186.104-.386.104-.595V6.25a1.75 1.75 0 0 0-1.75-1.75Zm-2.498 2a2.252 2.252 0 1 1 0 4.504 2.252 2.252 0 0 1 0-4.504Zm0 1.5a.752.752 0 1 0 0 1.504.752.752 0 0 0 0-1.504Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 704 B

View file

@ -0,0 +1 @@
<svg width="512" height="512" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.698 2.148A.75.75 0 0 1 20 2.75v13.5a.764.764 0 0 1-.004.079 3.5 3.5 0 1 1-1.496-2.702V7.758l-8.5 2.55v7.942a.756.756 0 0 1-.004.079A3.5 3.5 0 1 1 8.5 15.627V5.75a.75.75 0 0 1 .534-.718l10-3a.75.75 0 0 1 .664.116ZM10 8.742l8.5-2.55V3.758L10 6.308v2.434ZM6.5 16.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm8 0a2 2 0 1 0 4 0 2 2 0 0 0-4 0Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 458 B

View file

@ -0,0 +1 @@
<svg width="512" height="512" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.25 4h11.5a3.25 3.25 0 0 1 3.245 3.066L21 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L17.75 20H6.25a3.25 3.25 0 0 1-3.245-3.066L3 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L6.25 4h11.5-11.5Zm11.5 1.5H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 7.25v9.5a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143v-9.5a1.75 1.75 0 0 0-1.607-1.744L17.75 5.5Zm-7.697 4.085a.5.5 0 0 1 .587-.256l.084.033 4.382 2.19a.5.5 0 0 1 .076.848l-.076.047-4.382 2.191a.5.5 0 0 1-.716-.357L10 14.19V9.809a.5.5 0 0 1 .053-.224Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1 @@
<svg width="512" height="512" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M9.49 4.402A2.25 2.25 0 0 0 8.208 4H4.25l-.154.005A2.25 2.25 0 0 0 2 6.25v11.5l.005.154A2.25 2.25 0 0 0 4.25 20h15.5l.154-.005A2.25 2.25 0 0 0 22 17.75v-9l-.005-.154-.017-.158A2.25 2.25 0 0 0 19.75 6.5h-7.728L9.647 4.521l-.156-.119ZM13.498 8v2.245c0 .414.335.75.75.75h.75v1.003h-.25a.75.75 0 0 0 0 1.5h.25v1.5h-.25a.75.75 0 0 0 0 1.5h.25V18.5H4.25l-.102-.007a.75.75 0 0 1-.648-.743v-7.251l4.707.001.196-.009a2.25 2.25 0 0 0 1.244-.512L12.021 8h1.476Zm3 10h.25a.75.75 0 0 0 0-1.5h-.25V15h.25a.75.75 0 0 0 0-1.5h-.25v-2.505h.75a.75.75 0 0 0 .75-.75V8h1.753l.102.007a.75.75 0 0 1 .648.743v9l-.007.102a.75.75 0 0 1-.743.648h-3.253V18Zm0-10v1.495h-1.5V8h1.5ZM4.25 5.5h3.957l.104.007a.75.75 0 0 1 .376.167l1.891 1.575-1.89 1.577-.086.061A.75.75 0 0 1 8.207 9L3.5 8.999V6.25l.007-.102A.75.75 0 0 1 4.25 5.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 931 B

View file

@ -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

View file

@ -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

View file

@ -124,6 +124,10 @@ Item {
color: Nheko.theme.separator
}
UploadBox {
}
NotificationWarning {
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -46,6 +46,10 @@
<file>icons/ui/volume-off-indicator.svg</file>
<file>icons/ui/volume-up.svg</file>
<file>icons/ui/world.svg</file>
<file>icons/ui/music.svg</file>
<file>icons/ui/image.svg</file>
<file>icons/ui/zip.svg</file>
<file>icons/ui/video-file.svg</file>
<file>icons/emoji-categories/activity.svg</file>
<file>icons/emoji-categories/flags.svg</file>
<file>icons/emoji-categories/foods.svg</file>
@ -95,6 +99,7 @@
<file>qml/MatrixText.qml</file>
<file>qml/MatrixTextField.qml</file>
<file>qml/ToggleButton.qml</file>
<file>qml/UploadBox.qml</file>
<file>qml/MessageInput.qml</file>
<file>qml/MessageView.qml</file>
<file>qml/NhekoBusyIndicator.qml</file>

View file

@ -139,6 +139,19 @@ struct EventFile
}
};
struct EventThumbnailFile
{
template<class Content>
using file_t = decltype(Content::info.thumbnail_file);
template<class T>
std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e)
{
if constexpr (is_detected<file_t, T>::value)
return e.content.info.thumbnail_file;
return std::nullopt;
}
};
struct EventUrl
{
template<class Content>
@ -163,12 +176,28 @@ struct EventThumbnailUrl
std::string operator()(const mtx::events::Event<T> &e)
{
if constexpr (is_detected<thumbnail_url_t, T>::value) {
if (auto file = EventThumbnailFile{}(e))
return file->url;
return e.content.info.thumbnail_url;
}
return "";
}
};
struct EventDuration
{
template<class Content>
using thumbnail_url_t = decltype(Content::info.duration);
template<class T>
uint64_t operator()(const mtx::events::Event<T> &e)
{
if constexpr (is_detected<thumbnail_url_t, T>::value) {
return e.content.info.duration;
}
return 0;
}
};
struct EventBlurhash
{
template<class Content>
@ -410,6 +439,12 @@ mtx::accessors::file(const mtx::events::collections::TimelineEvents &event)
return std::visit(EventFile{}, event);
}
std::optional<mtx::crypto::EncryptedFile>
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)
{

View file

@ -78,11 +78,15 @@ formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event)
std::optional<mtx::crypto::EncryptedFile>
file(const mtx::events::collections::TimelineEvents &event);
std::optional<mtx::crypto::EncryptedFile>
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

View file

@ -251,6 +251,8 @@ MainWindow::registerQmlTypes()
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
qmlRegisterUncreatableType<MediaUpload>(
"im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml"));
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
"im.nheko.EmojiModel",
1,

View file

@ -1,223 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QBuffer>
#include <QFile>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QMimeDatabase>
#include <QVBoxLayout>
#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);
}
}

View file

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QImage>
#include <QLabel>
#include <QLineEdit>
#include <QPixmap>
#include <QPushButton>
#include <QWidget>
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

View file

@ -5,16 +5,18 @@
#include "InputBar.h"
#include <QBuffer>
#include <QClipboard>
#include <QDropEvent>
#include <QFileDialog>
#include <QGuiApplication>
#include <QInputMethod>
#include <QMediaMetaData>
#include <QMediaPlayer>
#include <QMimeData>
#include <QMimeDatabase>
#include <QStandardPaths>
#include <QTextBoundaryFinder>
#include <QUrl>
#include <QRegularExpression>
#include <mtx/responses/common.hpp>
@ -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<QVideoFrame::PixelFormat>
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<mtx::crypto::EncryptedFile> &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<mtx::crypto::EncryptedFile> &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<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize)
uint64_t dsize,
uint64_t duration,
const QSize &dimensions,
const std::optional<mtx::crypto::EncryptedFile> &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<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();
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<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));
} 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<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));
});
mediaPlayer->setVideoOutput(newSurface);
}
connect(mediaPlayer,
qOverload<QMediaPlayer::Error>(&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<const QString &, const QVariant &>(&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<int>(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<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);
}
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<unsigned char> data_;
for (int y = 0; y < img.height(); y++) {
for (int x = 0; x < img.width(); x++) {
auto p = img.pixel(x, y);
data_.push_back(static_cast<unsigned char>(qRed(p)));
data_.push_back(static_cast<unsigned char>(qGreen(p)));
data_.push_back(static_cast<unsigned char>(qBlue(p)));
}
}
blurhash = QString::fromStdString(
blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
}
http::client()->upload(
payload,
encryptedFile ? "application/octet-stream" : mime.toStdString(),
QFileInfo(fn).fileName().toStdString(),
[this,
filename = fn,
encryptedFile = std::move(encryptedFile),
mimeClass,
mime,
size = payload.size(),
dimensions,
blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) mutable {
if (err) {
emit ChatPage::instance()->showNotification(
tr("Failed to upload media. Please try again."));
nhlog::net()->warn("failed to upload media: {} {} ({})",
err->matrix_error.error,
to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
setUploading(false);
return;
}
auto url = QString::fromStdString(res.content_uri);
if (encryptedFile)
encryptedFile->url = res.content_uri;
if (mimeClass == 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<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), {}, 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()->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()
{

View file

@ -5,10 +5,17 @@
#pragma once
#include <QAbstractVideoSurface>
#include <QIODevice>
#include <QImage>
#include <QObject>
#include <QSize>
#include <QStringList>
#include <QTimer>
#include <QUrl>
#include <QVariantList>
#include <deque>
#include <memory>
#include <mtx/common.hpp>
#include <mtx/responses/messages.hpp>
@ -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<QVideoFrame::PixelFormat>
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<QIODevice> 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<mtx::crypto::EncryptedFile> encryptedFile_()
{
return encryptedFile;
}
[[nodiscard]] std::optional<mtx::crypto::EncryptedFile> 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<QIODevice> source;
QByteArray data;
QString mimetype_;
QString mimeClass_;
QString originalFilename_;
QString blurhash_;
QString thumbnailUrl_;
QString url_;
std::optional<mtx::crypto::EncryptedFile> 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<mtx::crypto::EncryptedFile> &thumbnailEncryptedFile,
const QString &thumbnailUrl,
uint64_t thumbnailSize,
const QSize &thumbnailDimensions,
const QString &blurhash);
void file(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
@ -95,14 +242,24 @@ private:
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize);
uint64_t dsize,
uint64_t duration);
void video(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize);
uint64_t dsize,
uint64_t duration,
const QSize &dimensions,
const std::optional<mtx::crypto::EncryptedFile> &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<QIODevice> 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<MediaUpload, DeleteLaterDeleter>;
std::vector<UploadHandle> unconfirmedUploads;
std::vector<UploadHandle> runningUploads;
};

View file

@ -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<qulonglong>(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<int>(Timestamp)));
m.insert(names[Url], data(event, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
m.insert(names[Duration], data(event, static_cast<int>(Duration)));
m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
m.insert(names[Filename], data(event, static_cast<int>(Filename)));
m.insert(names[Filesize], data(event, static_cast<int>(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;

View file

@ -215,6 +215,7 @@ public:
Timestamp,
Url,
ThumbnailUrl,
Duration,
Blurhash,
Filename,
Filesize,