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

New upload flow
pull/1009/head
DeepBlueV7.X 3 years ago committed by GitHub
commit 15ff5dace5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CMakeLists.txt
  2. 1
      resources/icons/ui/image.svg
  3. 1
      resources/icons/ui/music.svg
  4. 1
      resources/icons/ui/video-file.svg
  5. 1
      resources/icons/ui/zip.svg
  6. 2
      resources/qml/MessageView.qml
  7. 3
      resources/qml/TimelineRow.qml
  8. 4
      resources/qml/TimelineView.qml
  9. 89
      resources/qml/UploadBox.qml
  10. 3
      resources/qml/delegates/MessageDelegate.qml
  11. 5
      resources/qml/delegates/PlayableMediaMessage.qml
  12. 2
      resources/qml/delegates/Reply.qml
  13. 2
      resources/qml/ui/media/MediaControls.qml
  14. 5
      resources/res.qrc
  15. 40
      src/EventAccessors.cpp
  16. 4
      src/EventAccessors.h
  17. 2
      src/MainWindow.cpp
  18. 223
      src/dialogs/PreviewUploadOverlay.cpp
  19. 53
      src/dialogs/PreviewUploadOverlay.h
  20. 625
      src/timeline/InputBar.cpp
  21. 175
      src/timeline/InputBar.h
  22. 8
      src/timeline/TimelineModel.cpp
  23. 1
      src/timeline/TimelineModel.h

@ -311,7 +311,6 @@ set(SRC_FILES
# Dialogs # Dialogs
src/dialogs/CreateRoom.cpp src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp src/dialogs/FallbackAuth.cpp
src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp src/dialogs/ReCaptcha.cpp
# Emoji # Emoji
@ -509,7 +508,6 @@ qt5_wrap_cpp(MOC_HEADERS
# Dialogs # Dialogs
src/dialogs/CreateRoom.h src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h src/dialogs/FallbackAuth.h
src/dialogs/PreviewUploadOverlay.h
src/dialogs/ReCaptcha.h src/dialogs/ReCaptcha.h
# Emoji # Emoji

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

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

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

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

@ -376,6 +376,7 @@ Item {
required property string filesize required property string filesize
required property string url required property string url
required property string thumbnailUrl required property string thumbnailUrl
required property string duration
required property bool isOnlyEmoji required property bool isOnlyEmoji
required property bool isSender required property bool isSender
required property bool isEncrypted required property bool isEncrypted
@ -492,6 +493,7 @@ Item {
filesize: wrapper.filesize filesize: wrapper.filesize
url: wrapper.url url: wrapper.url
thumbnailUrl: wrapper.thumbnailUrl thumbnailUrl: wrapper.thumbnailUrl
duration: wrapper.duration
isOnlyEmoji: wrapper.isOnlyEmoji isOnlyEmoji: wrapper.isOnlyEmoji
isSender: wrapper.isSender isSender: wrapper.isSender
isEncrypted: wrapper.isEncrypted isEncrypted: wrapper.isEncrypted

@ -41,6 +41,7 @@ Item {
required property var reactions required property var reactions
required property int trustlevel required property int trustlevel
required property int encryptionError required property int encryptionError
required property int duration
required property var timestamp required property var timestamp
required property int status required property int status
required property int relatedEventCacheBuster required property int relatedEventCacheBuster
@ -128,6 +129,7 @@ Item {
userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? ""
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
@ -154,6 +156,7 @@ Item {
typeString: r.typeString ?? "" typeString: r.typeString ?? ""
url: r.url url: r.url
thumbnailUrl: r.thumbnailUrl thumbnailUrl: r.thumbnailUrl
duration: r.duration
originalWidth: r.originalWidth originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent isStateEvent: r.isStateEvent

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

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

@ -18,6 +18,7 @@ Item {
required property int type required property int type
required property string typeString required property string typeString
required property int originalWidth required property int originalWidth
required property int duration
required property string blurhash required property string blurhash
required property string body required property string body
required property string formattedBody required property string formattedBody
@ -161,6 +162,7 @@ Item {
url: d.url url: d.url
body: d.body body: d.body
filesize: d.filesize filesize: d.filesize
duration: d.duration
metadataWidth: d.metadataWidth metadataWidth: d.metadataWidth
} }
@ -178,6 +180,7 @@ Item {
url: d.url url: d.url
body: d.body body: d.body
filesize: d.filesize filesize: d.filesize
duration: d.duration
metadataWidth: d.metadataWidth metadataWidth: d.metadataWidth
} }

@ -17,6 +17,7 @@ Item {
required property double proportionalHeight required property double proportionalHeight
required property int type required property int type
required property int originalWidth required property int originalWidth
required property int duration
required property string thumbnailUrl required property string thumbnailUrl
required property string eventId required property string eventId
required property string url required property string url
@ -57,7 +58,7 @@ Item {
Image { Image {
anchors.fill: parent anchors.fill: parent
source: thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale" source: thumbnailUrl ? thumbnailUrl.replace("mxc://", "image://MxcImage/") + "?scale" : ""
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
@ -85,7 +86,7 @@ Item {
anchors.bottom: fileInfoLabel.top anchors.bottom: fileInfoLabel.top
playingVideo: type == MtxEvent.VideoMessage playingVideo: type == MtxEvent.VideoMessage
positionValue: mxcmedia.position positionValue: mxcmedia.position
duration: mxcmedia.duration duration: mediaLoaded ? mxcmedia.duration : content.duration
mediaLoaded: mxcmedia.loaded mediaLoaded: mxcmedia.loaded
mediaState: mxcmedia.state mediaState: mxcmedia.state
onPositionChanged: mxcmedia.position = position onPositionChanged: mxcmedia.position = position

@ -34,6 +34,7 @@ Item {
property string roomTopic property string roomTopic
property string roomName property string roomName
property string callType property string callType
property int duration
property int encryptionError property int encryptionError
property int relatedEventCacheBuster property int relatedEventCacheBuster
property int maxWidth property int maxWidth
@ -112,6 +113,7 @@ Item {
typeString: r.typeString ?? "" typeString: r.typeString ?? ""
url: r.url url: r.url
thumbnailUrl: r.thumbnailUrl thumbnailUrl: r.thumbnailUrl
duration: r.duration
originalWidth: r.originalWidth originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent isStateEvent: r.isStateEvent

@ -214,7 +214,7 @@ Rectangle {
Label { Label {
Layout.alignment: Qt.AlignRight 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 color: Nheko.colors.text
} }

@ -46,6 +46,10 @@
<file>icons/ui/volume-off-indicator.svg</file> <file>icons/ui/volume-off-indicator.svg</file>
<file>icons/ui/volume-up.svg</file> <file>icons/ui/volume-up.svg</file>
<file>icons/ui/world.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/activity.svg</file>
<file>icons/emoji-categories/flags.svg</file> <file>icons/emoji-categories/flags.svg</file>
<file>icons/emoji-categories/foods.svg</file> <file>icons/emoji-categories/foods.svg</file>
@ -95,6 +99,7 @@
<file>qml/MatrixText.qml</file> <file>qml/MatrixText.qml</file>
<file>qml/MatrixTextField.qml</file> <file>qml/MatrixTextField.qml</file>
<file>qml/ToggleButton.qml</file> <file>qml/ToggleButton.qml</file>
<file>qml/UploadBox.qml</file>
<file>qml/MessageInput.qml</file> <file>qml/MessageInput.qml</file>
<file>qml/MessageView.qml</file> <file>qml/MessageView.qml</file>
<file>qml/NhekoBusyIndicator.qml</file> <file>qml/NhekoBusyIndicator.qml</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 struct EventUrl
{ {
template<class Content> template<class Content>
@ -163,12 +176,28 @@ struct EventThumbnailUrl
std::string operator()(const mtx::events::Event<T> &e) std::string operator()(const mtx::events::Event<T> &e)
{ {
if constexpr (is_detected<thumbnail_url_t, T>::value) { if constexpr (is_detected<thumbnail_url_t, T>::value) {
if (auto file = EventThumbnailFile{}(e))
return file->url;
return e.content.info.thumbnail_url; return e.content.info.thumbnail_url;
} }
return ""; 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 struct EventBlurhash
{ {
template<class Content> template<class Content>
@ -410,6 +439,12 @@ mtx::accessors::file(const mtx::events::collections::TimelineEvents &event)
return std::visit(EventFile{}, 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 std::string
mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) 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); return std::visit(EventThumbnailUrl{}, event);
} }
uint64_t
mtx::accessors::duration(const mtx::events::collections::TimelineEvents &event)
{
return std::visit(EventDuration{}, event);
}
std::string std::string
mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event) mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event)
{ {

@ -78,11 +78,15 @@ formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event)
std::optional<mtx::crypto::EncryptedFile> std::optional<mtx::crypto::EncryptedFile>
file(const mtx::events::collections::TimelineEvents &event); file(const mtx::events::collections::TimelineEvents &event);
std::optional<mtx::crypto::EncryptedFile>
thumbnail_file(const mtx::events::collections::TimelineEvents &event);
std::string std::string
url(const mtx::events::collections::TimelineEvents &event); url(const mtx::events::collections::TimelineEvents &event);
std::string std::string
thumbnail_url(const mtx::events::collections::TimelineEvents &event); thumbnail_url(const mtx::events::collections::TimelineEvents &event);
uint64_t
duration(const mtx::events::collections::TimelineEvents &event);
std::string std::string
blurhash(const mtx::events::collections::TimelineEvents &event); blurhash(const mtx::events::collections::TimelineEvents &event);
std::string std::string

@ -251,6 +251,8 @@ MainWindow::registerQmlTypes()
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel"); qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType<emoji::Emoji>( qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models")); "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, qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
"im.nheko.EmojiModel", "im.nheko.EmojiModel",
1, 1,

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

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

@ -5,16 +5,18 @@
#include "InputBar.h" #include "InputBar.h"
#include <QBuffer>
#include <QClipboard> #include <QClipboard>
#include <QDropEvent> #include <QDropEvent>
#include <QFileDialog> #include <QFileDialog>
#include <QGuiApplication> #include <QGuiApplication>
#include <QInputMethod> #include <QInputMethod>
#include <QMediaMetaData>
#include <QMediaPlayer>
#include <QMimeData> #include <QMimeData>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTextBoundaryFinder> #include <QTextBoundaryFinder>
#include <QUrl>
#include <QRegularExpression> #include <QRegularExpression>
#include <mtx/responses/common.hpp> #include <mtx/responses/common.hpp>
@ -31,12 +33,97 @@
#include "TimelineViewManager.h" #include "TimelineViewManager.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "Utils.h" #include "Utils.h"
#include "dialogs/PreviewUploadOverlay.h"
#include "blurhash.hpp" #include "blurhash.hpp"
static constexpr size_t INPUT_HISTORY_SIZE = 10; 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 void
InputBar::paste(bool fromMouse) InputBar::paste(bool fromMouse)
{ {
@ -67,29 +154,23 @@ InputBar::insertMimeData(const QMimeData *md)
if (md->hasImage()) { if (md->hasImage()) {
if (formats.contains(QStringLiteral("image/svg+xml"), Qt::CaseInsensitive)) { 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 { } else {
showPreview(*md, QLatin1String(""), image); startUploadFromMimeData(*md, image.first());
} }
} else if (!audio.empty()) { } else if (!audio.empty()) {
showPreview(*md, QLatin1String(""), audio); startUploadFromMimeData(*md, audio.first());
} else if (!video.empty()) { } else if (!video.empty()) {
showPreview(*md, QLatin1String(""), video); startUploadFromMimeData(*md, video.first());
} else if (md->hasUrls()) { } else if (md->hasUrls()) {
// Generic file path for any platform. // Generic file path for any platform.
QString path;
for (auto &&u : md->urls()) { for (auto &&u : md->urls()) {
if (u.isLocalFile()) { if (u.isLocalFile()) {
path = u.toLocalFile(); startUploadFromPath(u.toLocalFile());
break;
} }
} }
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"))) { } else if (md->hasFormat(QStringLiteral("x-special/gnome-copied-files"))) {
// Special case for X11 users. See "Notes for X11 Users" in md. // Special case for X11 users. See "Notes for X11 Users" in md.
// Source: http://doc.qt.io/qt-5/qclipboard.html // Source: http://doc.qt.io/qt-5/qclipboard.html
@ -108,21 +189,12 @@ InputBar::insertMimeData(const QMimeData *md)
return; return;
} }
QString path;
for (int i = 1; i < data.size(); ++i) { for (int i = 1; i < data.size(); ++i) {
QUrl url{data[i]}; QUrl url{data[i]};
if (url.isLocalFile()) { if (url.isLocalFile()) {
path = url.toLocalFile(); startUploadFromPath(url.toLocalFile());
break;
} }
} }
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()) { } else if (md->hasText()) {
emit insertText(md->text()); emit insertText(md->text());
} else { } else {
@ -275,25 +347,7 @@ InputBar::openFileSelection()
if (fileName.isEmpty()) if (fileName.isEmpty())
return; return;
QMimeDatabase db; startUploadFromPath(fileName);
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()});
} }
void void
@ -424,6 +478,10 @@ InputBar::image(const QString &filename,
const QString &mime, const QString &mime,
uint64_t dsize, uint64_t dsize,
const QSize &dimensions, const QSize &dimensions,
const std::optional<mtx::crypto::EncryptedFile> &thumbnailEncryptedFile,
const QString &thumbnailUrl,
uint64_t thumbnailSize,
const QSize &thumbnailDimensions,
const QString &blurhash) const QString &blurhash)
{ {
mtx::events::msg::Image image; mtx::events::msg::Image image;
@ -439,6 +497,18 @@ InputBar::image(const QString &filename,
else else
image.url = url.toStdString(); 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()) { if (!room->reply().isEmpty()) {
image.relations.relations.push_back( image.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
@ -485,7 +555,8 @@ InputBar::audio(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize) uint64_t dsize,
uint64_t duration)
{ {
mtx::events::msg::Audio audio; mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString(); audio.info.mimetype = mime.toStdString();
@ -493,6 +564,9 @@ InputBar::audio(const QString &filename,
audio.body = filename.toStdString(); audio.body = filename.toStdString();
audio.url = url.toStdString(); audio.url = url.toStdString();
if (duration > 0)
audio.info.duration = duration;
if (file) if (file)
audio.file = file; audio.file = file;
else else
@ -515,18 +589,45 @@ InputBar::video(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, 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; mtx::events::msg::Video video;
video.info.mimetype = mime.toStdString(); video.info.mimetype = mime.toStdString();
video.info.size = dsize; video.info.size = dsize;
video.info.blurhash = blurhash.toStdString();
video.body = filename.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) if (file)
video.file = file; video.file = file;
else else
video.url = url.toStdString(); 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()) { if (!room->reply().isEmpty()) {
video.relations.relations.push_back( video.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
@ -661,125 +762,351 @@ InputBar::command(const QString &command, QString args)
} }
} }
void MediaUpload::MediaUpload(std::unique_ptr<QIODevice> source_,
InputBar::showPreview(const QMimeData &source, const QString &path, const QStringList &formats) 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); mimeClass_ = mimetype_.left(mimetype_.indexOf(u'/'));
previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
// Force SVG to _not_ be handled as an image, but as raw data if (!source->isOpen())
if (source.hasImage() && source->open(QIODevice::ReadOnly);
(formats.empty() || formats.front() != QLatin1String("image/svg+xml"))) {
if (!formats.empty() && formats.front().startsWith(QLatin1String("image/"))) { data = source->readAll();
// known format, keep as-is source->reset();
previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), formats.front());
} else { if (!data.size()) {
// unknown image format, default to image/png nhlog::ui()->warn("Attempted to upload zero-byte file?! Mimetype {}, filename {}",
previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()), mimetype_.toStdString(),
QStringLiteral("image/png")); originalFilename_.toStdString());
} emit uploadFailed(this);
} 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();
return; return;
} }
connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() { nhlog::ui()->debug("Mime: {}", mimetype_.toStdString());
setUploading(false); if (mimeClass_ == u"image") {
}); QImage img = utils::readImage(data);
setThumbnail(img.scaled(
std::min(800, img.width()), std::min(800, img.height()), Qt::KeepAspectRatioByExpanding));
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);
}
connect( nhlog::ui()->debug("Got image {}x{}", img.width(), img.height());
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());
return;
}
setUploading(true);
setText(QLatin1String("")); this->setThumbnail(img);
auto payload = std::string(data.data(), data.size()); if (!dimensions_.isValid())
std::optional<mtx::crypto::EncryptedFile> encryptedFile; this->dimensions_ = img.size();
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; if (img.height() > 200 && img.width() > 360)
QString blurhash; img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
auto mimeClass = mime.left(mime.indexOf(u'/')); std::vector<unsigned char> data_;
nhlog::ui()->debug("Mime: {}", mime.toStdString()); for (int y = 0; y < img.height(); y++) {
if (mimeClass == u"image") { for (int x = 0; x < img.width(); x++) {
QImage img = utils::readImage(data); auto p = img.pixel(x, y);
data_.push_back(static_cast<unsigned char>(qRed(p)));
dimensions = img.size(); data_.push_back(static_cast<unsigned char>(qGreen(p)));
if (img.height() > 200 && img.width() > 360) data_.push_back(static_cast<unsigned char>(qBlue(p)));
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;
} }
blurhash = QString::fromStdString(
blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); 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;
} }
http::client()->upload( auto url = QString::fromStdString(res.content_uri);
payload, if (encryptedFile)
encryptedFile ? "application/octet-stream" : mime.toStdString(), encryptedFile->url = res.content_uri;
QFileInfo(fn).fileName().toStdString(),
[this, emit uploadComplete(this, std::move(url));
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);
});
}); });
} }
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 void
InputBar::startTyping() InputBar::startTyping()
{ {

@ -5,10 +5,17 @@
#pragma once #pragma once
#include <QAbstractVideoSurface>
#include <QIODevice>
#include <QImage>
#include <QObject> #include <QObject>
#include <QSize>
#include <QStringList> #include <QStringList>
#include <QTimer> #include <QTimer>
#include <QUrl>
#include <QVariantList>
#include <deque> #include <deque>
#include <memory>
#include <mtx/common.hpp> #include <mtx/common.hpp>
#include <mtx/responses/messages.hpp> #include <mtx/responses/messages.hpp>
@ -25,12 +32,139 @@ enum class MarkdownOverride
OFF, 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 class InputBar : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged) Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged)
Q_PROPERTY(QString text READ text NOTIFY textChanged) Q_PROPERTY(QString text READ text NOTIFY textChanged)
Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged)
public: public:
explicit InputBar(TimelineModel *parent) explicit InputBar(TimelineModel *parent)
@ -45,6 +179,8 @@ public:
connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping);
} }
QVariantList uploads() const;
public slots: public slots:
[[nodiscard]] QString text() const; [[nodiscard]] QString text() const;
QString previousText(); QString previousText();
@ -65,15 +201,22 @@ public slots:
void reaction(const QString &reactedEvent, const QString &reactionKey); void reaction(const QString &reactedEvent, const QString &reactionKey);
void sticker(CombinedImagePackModel *model, int row); void sticker(CombinedImagePackModel *model, int row);
void acceptUploads();
void declineUploads();
private slots: private slots:
void startTyping(); void startTyping();
void stopTyping(); void stopTyping();
void finalizeUpload(MediaUpload *upload, QString url);
void removeRunUpload(MediaUpload *upload);
signals: signals:
void insertText(QString text); void insertText(QString text);
void textChanged(QString newText); void textChanged(QString newText);
void uploadingChanged(bool value); void uploadingChanged(bool value);
void containsAtRoomChanged(); void containsAtRoomChanged();
void uploadsChanged();
private: private:
void emote(const QString &body, bool rainbowify); void emote(const QString &body, bool rainbowify);
@ -85,6 +228,10 @@ private:
const QString &mime, const QString &mime,
uint64_t dsize, uint64_t dsize,
const QSize &dimensions, const QSize &dimensions,
const std::optional<mtx::crypto::EncryptedFile> &thumbnailEncryptedFile,
const QString &thumbnailUrl,
uint64_t thumbnailSize,
const QSize &thumbnailDimensions,
const QString &blurhash); const QString &blurhash);
void file(const QString &filename, void file(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile, const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
@ -95,14 +242,24 @@ private:
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, const QString &mime,
uint64_t dsize); uint64_t dsize,
uint64_t duration);
void video(const QString &filename, void video(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file, const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url, const QString &url,
const QString &mime, 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) void setUploading(bool value)
{ {
if (value != uploading_) { if (value != uploading_) {
@ -121,4 +278,16 @@ private:
int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
bool uploading_ = false; bool uploading_ = false;
bool containsAtRoom_ = 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;
}; };

@ -474,6 +474,7 @@ TimelineModel::roleNames() const
{Timestamp, "timestamp"}, {Timestamp, "timestamp"},
{Url, "url"}, {Url, "url"},
{ThumbnailUrl, "thumbnailUrl"}, {ThumbnailUrl, "thumbnailUrl"},
{Duration, "duration"},
{Blurhash, "blurhash"}, {Blurhash, "blurhash"},
{Filename, "filename"}, {Filename, "filename"},
{Filesize, "filesize"}, {Filesize, "filesize"},
@ -627,6 +628,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return QVariant(QString::fromStdString(url(event))); return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl: case ThumbnailUrl:
return QVariant(QString::fromStdString(thumbnail_url(event))); return QVariant(QString::fromStdString(thumbnail_url(event)));
case Duration:
return QVariant(static_cast<qulonglong>(duration(event)));
case Blurhash: case Blurhash:
return QVariant(QString::fromStdString(blurhash(event))); return QVariant(QString::fromStdString(blurhash(event)));
case Filename: 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[Timestamp], data(event, static_cast<int>(Timestamp)));
m.insert(names[Url], data(event, static_cast<int>(Url))); m.insert(names[Url], data(event, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); 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[Blurhash], data(event, static_cast<int>(Blurhash)));
m.insert(names[Filename], data(event, static_cast<int>(Filename))); m.insert(names[Filename], data(event, static_cast<int>(Filename)));
m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
@ -1363,6 +1367,10 @@ struct SendMessageVisitor
if (encInfo) if (encInfo)
emit model_->newEncryptedImage(encInfo.value()); emit model_->newEncryptedImage(encInfo.value());
encInfo = mtx::accessors::thumbnail_file(msg);
if (encInfo)
emit model_->newEncryptedImage(encInfo.value());
model_->sendEncryptedMessage(msg, Event); model_->sendEncryptedMessage(msg, Event);
} else { } else {
msg.type = Event; msg.type = Event;

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

Loading…
Cancel
Save