mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-10-30 17:40:47 +03:00
Add file uploading
This commit is contained in:
parent
0bb4885632
commit
a31d3d0816
17 changed files with 475 additions and 318 deletions
|
@ -12,8 +12,11 @@ Rectangle {
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: if (TimelineManager.onVideoCall)
|
onClicked: {
|
||||||
|
if (TimelineManager.onVideoCall)
|
||||||
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
|
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
@ -39,8 +42,7 @@ Rectangle {
|
||||||
Image {
|
Image {
|
||||||
Layout.preferredWidth: 24
|
Layout.preferredWidth: 24
|
||||||
Layout.preferredHeight: 24
|
Layout.preferredHeight: 24
|
||||||
source: TimelineManager.onVideoCall ?
|
source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||||
"qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -69,6 +71,7 @@ Rectangle {
|
||||||
callTimer.startTime = Math.floor(d.getTime() / 1000);
|
callTimer.startTime = Math.floor(d.getTime() / 1000);
|
||||||
if (TimelineManager.onVideoCall)
|
if (TimelineManager.onVideoCall)
|
||||||
stackLayout.currentIndex = 1;
|
stackLayout.currentIndex = 1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WebRTCState.DISCONNECTED:
|
case WebRTCState.DISCONNECTED:
|
||||||
callStateLabel.text = "";
|
callStateLabel.text = "";
|
||||||
|
|
|
@ -2,7 +2,6 @@ import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import im.nheko 1.0
|
import im.nheko 1.0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -36,6 +35,20 @@ Rectangle {
|
||||||
image: ":/icons/icons/ui/paper-clip-outline.png"
|
image: ":/icons/icons/ui/paper-clip-outline.png"
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
Layout.bottomMargin: 8
|
Layout.bottomMargin: 8
|
||||||
|
onClicked: TimelineManager.timeline.input.openFileSelection()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: colors.window
|
||||||
|
visible: TimelineManager.timeline.input.uploading
|
||||||
|
|
||||||
|
NhekoBusyIndicator {
|
||||||
|
anchors.fill: parent
|
||||||
|
running: parent.visible
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -52,27 +65,27 @@ Rectangle {
|
||||||
placeholderTextColor: colors.buttonText
|
placeholderTextColor: colors.buttonText
|
||||||
color: colors.text
|
color: colors.text
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
|
|
||||||
onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||||
onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||||
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||||
onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: TimelineManager.timeline.input
|
|
||||||
function onInsertText(text_) { textArea.insert(textArea.cursorPosition, text_); }
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: {
|
Keys.onPressed: {
|
||||||
if (event.matches(StandardKey.Paste)) {
|
if (event.matches(StandardKey.Paste)) {
|
||||||
TimelineManager.timeline.input.paste(false)
|
TimelineManager.timeline.input.paste(false);
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
|
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||||
|
TimelineManager.timeline.input.send();
|
||||||
|
textArea.clear();
|
||||||
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
|
||||||
TimelineManager.timeline.input.send()
|
|
||||||
textArea.clear()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onInsertText(text_) {
|
||||||
|
textArea.insert(textArea.cursorPosition, text_);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: TimelineManager.timeline.input
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
@ -110,6 +123,10 @@ Rectangle {
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
Layout.bottomMargin: 8
|
Layout.bottomMargin: 8
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
onClicked: {
|
||||||
|
TimelineManager.timeline.input.send();
|
||||||
|
textArea.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
64
resources/qml/NhekoBusyIndicator.qml
Normal file
64
resources/qml/NhekoBusyIndicator.qml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: control
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
implicitWidth: Math.min(parent.height, parent.width)
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: item
|
||||||
|
|
||||||
|
height: Math.min(parent.height, parent.width)
|
||||||
|
width: height
|
||||||
|
opacity: control.running ? 1 : 0
|
||||||
|
|
||||||
|
RotationAnimator {
|
||||||
|
target: item
|
||||||
|
running: control.visible && control.running
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
loops: Animation.Infinite
|
||||||
|
duration: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
|
||||||
|
model: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitWidth: radius * 2
|
||||||
|
implicitHeight: radius * 2
|
||||||
|
radius: item.height / 6
|
||||||
|
color: colors.text
|
||||||
|
opacity: (index + 2) / (repeater.count + 2)
|
||||||
|
transform: [
|
||||||
|
Translate {
|
||||||
|
y: -Math.min(item.width, item.height) * 0.5 + item.height / 6
|
||||||
|
},
|
||||||
|
Rotation {
|
||||||
|
angle: index / repeater.count * 360
|
||||||
|
origin.x: item.height / 2
|
||||||
|
origin.y: item.height / 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
OpacityAnimator {
|
||||||
|
duration: 250
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -192,13 +192,15 @@ Page {
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: stackLayout
|
id: stackLayout
|
||||||
|
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: TimelineManager
|
|
||||||
function onActiveTimelineChanged() {
|
function onActiveTimelineChanged() {
|
||||||
stackLayout.currentIndex = 0;
|
stackLayout.currentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: TimelineManager
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageView {
|
MessageView {
|
||||||
|
@ -210,6 +212,7 @@ Page {
|
||||||
source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
|
source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
|
||||||
onLoaded: TimelineManager.setVideoCallItem()
|
onLoaded: TimelineManager.setVideoCallItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TypingIndicator {
|
TypingIndicator {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
|
|
||||||
import org.freedesktop.gstreamer.GLVideoItem 1.0
|
import org.freedesktop.gstreamer.GLVideoItem 1.0
|
||||||
|
|
||||||
GstGLVideoItem {
|
GstGLVideoItem {
|
||||||
|
|
|
@ -132,6 +132,7 @@
|
||||||
<file>qml/Avatar.qml</file>
|
<file>qml/Avatar.qml</file>
|
||||||
<file>qml/ImageButton.qml</file>
|
<file>qml/ImageButton.qml</file>
|
||||||
<file>qml/MatrixText.qml</file>
|
<file>qml/MatrixText.qml</file>
|
||||||
|
<file>qml/NhekoBusyIndicator.qml</file>
|
||||||
<file>qml/StatusIndicator.qml</file>
|
<file>qml/StatusIndicator.qml</file>
|
||||||
<file>qml/EncryptionIndicator.qml</file>
|
<file>qml/EncryptionIndicator.qml</file>
|
||||||
<file>qml/Reactions.qml</file>
|
<file>qml/Reactions.qml</file>
|
||||||
|
|
120
src/ChatPage.cpp
120
src/ChatPage.cpp
|
@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
||||||
this,
|
this,
|
||||||
SIGNAL(unreadMessages(int)));
|
SIGNAL(unreadMessages(int)));
|
||||||
|
|
||||||
connect(
|
|
||||||
text_input_,
|
|
||||||
&TextInputWidget::uploadMedia,
|
|
||||||
this,
|
|
||||||
[this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) {
|
|
||||||
if (!dev->open(QIODevice::ReadOnly)) {
|
|
||||||
emit uploadFailed(
|
|
||||||
QString("Error while reading media: %1").arg(dev->errorString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto bin = dev->readAll();
|
|
||||||
QMimeDatabase db;
|
|
||||||
QMimeType mime = db.mimeTypeForData(bin);
|
|
||||||
|
|
||||||
auto payload = std::string(bin.data(), bin.size());
|
|
||||||
std::optional<mtx::crypto::EncryptedFile> encryptedFile;
|
|
||||||
if (cache::isRoomEncrypted(current_room_.toStdString())) {
|
|
||||||
mtx::crypto::BinaryBuf buf;
|
|
||||||
std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
|
|
||||||
payload = mtx::crypto::to_string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize dimensions;
|
|
||||||
QString blurhash;
|
|
||||||
if (mimeClass == "image") {
|
|
||||||
QImage img = utils::readImage(&bin);
|
|
||||||
|
|
||||||
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.name().toStdString(),
|
|
||||||
QFileInfo(fn).fileName().toStdString(),
|
|
||||||
[this,
|
|
||||||
room_id = current_room_,
|
|
||||||
filename = fn,
|
|
||||||
encryptedFile,
|
|
||||||
mimeClass,
|
|
||||||
mime = mime.name(),
|
|
||||||
size = payload.size(),
|
|
||||||
dimensions,
|
|
||||||
blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
|
|
||||||
if (err) {
|
|
||||||
emit uploadFailed(
|
|
||||||
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));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit mediaUploaded(room_id,
|
|
||||||
filename,
|
|
||||||
encryptedFile,
|
|
||||||
QString::fromStdString(res.content_uri),
|
|
||||||
mimeClass,
|
|
||||||
mime,
|
|
||||||
size,
|
|
||||||
dimensions,
|
|
||||||
blurhash);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
|
|
||||||
text_input_->hideUploadSpinner();
|
|
||||||
emit showNotification(msg);
|
|
||||||
});
|
|
||||||
connect(this,
|
|
||||||
&ChatPage::mediaUploaded,
|
|
||||||
this,
|
|
||||||
[this](QString roomid,
|
|
||||||
QString filename,
|
|
||||||
std::optional<mtx::crypto::EncryptedFile> encryptedFile,
|
|
||||||
QString url,
|
|
||||||
QString mimeClass,
|
|
||||||
QString mime,
|
|
||||||
qint64 dsize,
|
|
||||||
QSize dimensions,
|
|
||||||
QString blurhash) {
|
|
||||||
text_input_->hideUploadSpinner();
|
|
||||||
|
|
||||||
if (encryptedFile)
|
|
||||||
encryptedFile->url = url.toStdString();
|
|
||||||
|
|
||||||
if (mimeClass == "image")
|
|
||||||
view_manager_->queueImageMessage(roomid,
|
|
||||||
filename,
|
|
||||||
encryptedFile,
|
|
||||||
url,
|
|
||||||
mime,
|
|
||||||
dsize,
|
|
||||||
dimensions,
|
|
||||||
blurhash);
|
|
||||||
else if (mimeClass == "audio")
|
|
||||||
view_manager_->queueAudioMessage(
|
|
||||||
roomid, filename, encryptedFile, url, mime, dsize);
|
|
||||||
else if (mimeClass == "video")
|
|
||||||
view_manager_->queueVideoMessage(
|
|
||||||
roomid, filename, encryptedFile, url, mime, dsize);
|
|
||||||
else
|
|
||||||
view_manager_->queueFileMessage(
|
|
||||||
roomid, filename, encryptedFile, url, mime, dsize);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() {
|
connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() {
|
||||||
if (callManager_->onActiveCall()) {
|
if (callManager_->onActiveCall()) {
|
||||||
callManager_->hangUp();
|
callManager_->hangUp();
|
||||||
|
|
|
@ -126,17 +126,6 @@ signals:
|
||||||
void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
|
void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
|
||||||
const QPoint widgetPos);
|
const QPoint widgetPos);
|
||||||
|
|
||||||
void uploadFailed(const QString &msg);
|
|
||||||
void mediaUploaded(const QString &roomid,
|
|
||||||
const QString &filename,
|
|
||||||
const std::optional<mtx::crypto::EncryptedFile> &file,
|
|
||||||
const QString &url,
|
|
||||||
const QString &mimeClass,
|
|
||||||
const QString &mime,
|
|
||||||
qint64 dsize,
|
|
||||||
const QSize &dimensions,
|
|
||||||
const QString &blurhash);
|
|
||||||
|
|
||||||
void contentLoaded();
|
void contentLoaded();
|
||||||
void closing();
|
void closing();
|
||||||
void changeWindowTitle(const int);
|
void changeWindowTitle(const int);
|
||||||
|
|
|
@ -88,10 +88,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
||||||
typingTimer_->setSingleShot(true);
|
typingTimer_->setSingleShot(true);
|
||||||
|
|
||||||
connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
|
connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
|
||||||
connect(&previewDialog_,
|
|
||||||
&dialogs::PreviewUploadOverlay::confirmUpload,
|
|
||||||
this,
|
|
||||||
&FilteredTextEdit::uploadData);
|
|
||||||
|
|
||||||
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
|
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
|
||||||
connect(
|
connect(
|
||||||
|
@ -355,81 +351,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
|
||||||
FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const
|
|
||||||
{
|
|
||||||
return (source->hasImage() || QTextEdit::canInsertFromMimeData(source));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FilteredTextEdit::insertFromMimeData(const QMimeData *source)
|
|
||||||
{
|
|
||||||
qInfo() << "Got mime formats: \n" << source->formats();
|
|
||||||
const auto formats = source->formats().filter("/");
|
|
||||||
const auto image = formats.filter("image/", Qt::CaseInsensitive);
|
|
||||||
const auto audio = formats.filter("audio/", Qt::CaseInsensitive);
|
|
||||||
const auto video = formats.filter("video/", Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
if (!image.empty() && source->hasImage()) {
|
|
||||||
QImage img = qvariant_cast<QImage>(source->imageData());
|
|
||||||
previewDialog_.setPreview(img, image.front());
|
|
||||||
} else if (!audio.empty()) {
|
|
||||||
showPreview(source, audio);
|
|
||||||
} else if (!video.empty()) {
|
|
||||||
showPreview(source, video);
|
|
||||||
} else if (source->hasUrls()) {
|
|
||||||
// Generic file path for any platform.
|
|
||||||
QString path;
|
|
||||||
for (auto &&u : source->urls()) {
|
|
||||||
if (u.isLocalFile()) {
|
|
||||||
path = u.toLocalFile();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.isEmpty() && QFileInfo{path}.exists()) {
|
|
||||||
previewDialog_.setPreview(path);
|
|
||||||
} else {
|
|
||||||
qWarning()
|
|
||||||
<< "Clipboard does not contain any valid file paths:" << source->urls();
|
|
||||||
}
|
|
||||||
} else if (source->hasFormat("x-special/gnome-copied-files")) {
|
|
||||||
// Special case for X11 users. See "Notes for X11 Users" in source.
|
|
||||||
// Source: http://doc.qt.io/qt-5/qclipboard.html
|
|
||||||
|
|
||||||
// This MIME type returns a string with multiple lines separated by '\n'. The first
|
|
||||||
// line is the command to perform with the clipboard (not useful to us). The
|
|
||||||
// following lines are the file URIs.
|
|
||||||
//
|
|
||||||
// Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
|
|
||||||
// nautilus_clipboard_get_uri_list_from_selection_data()
|
|
||||||
// https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
|
|
||||||
|
|
||||||
auto data = source->data("x-special/gnome-copied-files").split('\n');
|
|
||||||
if (data.size() < 2) {
|
|
||||||
qWarning() << "MIME format is malformed, cannot perform paste.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString path;
|
|
||||||
for (int i = 1; i < data.size(); ++i) {
|
|
||||||
QUrl url{data[i]};
|
|
||||||
if (url.isLocalFile()) {
|
|
||||||
path = url.toLocalFile();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.isEmpty()) {
|
|
||||||
previewDialog_.setPreview(path);
|
|
||||||
} else {
|
|
||||||
qWarning() << "Clipboard does not contain any valid file paths:" << data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
QTextEdit::insertFromMimeData(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
FilteredTextEdit::stopTyping()
|
FilteredTextEdit::stopTyping()
|
||||||
{
|
{
|
||||||
|
@ -494,28 +415,6 @@ FilteredTextEdit::textChanged()
|
||||||
working_history_[history_index_] = toPlainText();
|
working_history_[history_index_] = toPlainText();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
FilteredTextEdit::uploadData(const QByteArray data,
|
|
||||||
const QString &mediaType,
|
|
||||||
const QString &filename)
|
|
||||||
{
|
|
||||||
QSharedPointer<QBuffer> buffer{new QBuffer{this}};
|
|
||||||
buffer->setData(data);
|
|
||||||
|
|
||||||
emit startedUpload();
|
|
||||||
|
|
||||||
emit media(buffer, mediaType, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats)
|
|
||||||
{
|
|
||||||
// Retrieve data as MIME type.
|
|
||||||
auto const &mime = formats.first();
|
|
||||||
QByteArray data = source->data(mime);
|
|
||||||
previewDialog_.setPreview(data, mime);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextInputWidget::TextInputWidget(QWidget *parent)
|
TextInputWidget::TextInputWidget(QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
|
@ -624,7 +523,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
|
||||||
#endif
|
#endif
|
||||||
connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
|
connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
|
||||||
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
|
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
|
||||||
connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
|
|
||||||
connect(emojiBtn_,
|
connect(emojiBtn_,
|
||||||
SIGNAL(emojiSelected(const QString &)),
|
SIGNAL(emojiSelected(const QString &)),
|
||||||
this,
|
this,
|
||||||
|
@ -633,9 +531,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
|
||||||
connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
|
connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
|
||||||
|
|
||||||
connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
|
connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
|
||||||
|
|
||||||
connect(
|
|
||||||
input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -654,47 +549,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji)
|
||||||
input_->show();
|
input_->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
TextInputWidget::openFileSelection()
|
|
||||||
{
|
|
||||||
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
|
||||||
const auto fileName =
|
|
||||||
QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)"));
|
|
||||||
|
|
||||||
if (fileName.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QMimeDatabase db;
|
|
||||||
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
|
|
||||||
|
|
||||||
const auto format = mime.name().split("/")[0];
|
|
||||||
|
|
||||||
QSharedPointer<QFile> file{new QFile{fileName, this}};
|
|
||||||
|
|
||||||
emit uploadMedia(file, format, QFileInfo(fileName).fileName());
|
|
||||||
|
|
||||||
showUploadSpinner();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TextInputWidget::showUploadSpinner()
|
|
||||||
{
|
|
||||||
topLayout_->removeWidget(sendFileBtn_);
|
|
||||||
sendFileBtn_->hide();
|
|
||||||
|
|
||||||
topLayout_->insertWidget(1, spinner_);
|
|
||||||
spinner_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TextInputWidget::hideUploadSpinner()
|
|
||||||
{
|
|
||||||
topLayout_->removeWidget(spinner_);
|
|
||||||
topLayout_->insertWidget(1, sendFileBtn_);
|
|
||||||
sendFileBtn_->show();
|
|
||||||
spinner_->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
TextInputWidget::stopTyping()
|
TextInputWidget::stopTyping()
|
||||||
{
|
{
|
||||||
|
|
|
@ -57,9 +57,6 @@ signals:
|
||||||
void startedTyping();
|
void startedTyping();
|
||||||
void stoppedTyping();
|
void stoppedTyping();
|
||||||
void startedUpload();
|
void startedUpload();
|
||||||
void message(QString msg);
|
|
||||||
void command(QString name, QString args);
|
|
||||||
void media(QSharedPointer<QIODevice> data, QString mimeClass, const QString &filename);
|
|
||||||
|
|
||||||
//! Trigger the suggestion popup.
|
//! Trigger the suggestion popup.
|
||||||
void showSuggestions(const QString &query);
|
void showSuggestions(const QString &query);
|
||||||
|
@ -73,8 +70,6 @@ public slots:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
bool canInsertFromMimeData(const QMimeData *source) const override;
|
|
||||||
void insertFromMimeData(const QMimeData *source) override;
|
|
||||||
void focusOutEvent(QFocusEvent *event) override
|
void focusOutEvent(QFocusEvent *event) override
|
||||||
{
|
{
|
||||||
suggestionsPopup_.hide();
|
suggestionsPopup_.hide();
|
||||||
|
@ -131,9 +126,7 @@ private:
|
||||||
|
|
||||||
void insertCompletion(QString completion);
|
void insertCompletion(QString completion);
|
||||||
void textChanged();
|
void textChanged();
|
||||||
void uploadData(const QByteArray data, const QString &media, const QString &filename);
|
|
||||||
void afterCompletion(int);
|
void afterCompletion(int);
|
||||||
void showPreview(const QMimeData *source, const QStringList &formats);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TextInputWidget : public QWidget
|
class TextInputWidget : public QWidget
|
||||||
|
@ -161,8 +154,6 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void openFileSelection();
|
|
||||||
void hideUploadSpinner();
|
|
||||||
void focusLineEdit() { input_->setFocus(); }
|
void focusLineEdit() { input_->setFocus(); }
|
||||||
void changeCallButtonState(webrtc::State);
|
void changeCallButtonState(webrtc::State);
|
||||||
|
|
||||||
|
@ -172,9 +163,6 @@ private slots:
|
||||||
signals:
|
signals:
|
||||||
void heightChanged(int height);
|
void heightChanged(int height);
|
||||||
|
|
||||||
void uploadMedia(const QSharedPointer<QIODevice> data,
|
|
||||||
QString mimeClass,
|
|
||||||
const QString &filename);
|
|
||||||
void callButtonPress();
|
void callButtonPress();
|
||||||
|
|
||||||
void sendJoinRoomRequest(const QString &room);
|
void sendJoinRoomRequest(const QString &room);
|
||||||
|
@ -192,8 +180,6 @@ protected:
|
||||||
void paintEvent(QPaintEvent *) override;
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void showUploadSpinner();
|
|
||||||
|
|
||||||
QHBoxLayout *topLayout_;
|
QHBoxLayout *topLayout_;
|
||||||
FilteredTextEdit *input_;
|
FilteredTextEdit *input_;
|
||||||
|
|
||||||
|
|
|
@ -677,9 +677,10 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
QImage
|
||||||
utils::readImage(QByteArray *data)
|
utils::readImage(const QByteArray *data)
|
||||||
{
|
{
|
||||||
QBuffer buf(data);
|
QBuffer buf;
|
||||||
|
buf.setData(*data);
|
||||||
QImageReader reader(&buf);
|
QImageReader reader(&buf);
|
||||||
reader.setAutoTransform(true);
|
reader.setAutoTransform(true);
|
||||||
return reader.read();
|
return reader.read();
|
||||||
|
|
|
@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value);
|
||||||
|
|
||||||
//! Read image respecting exif orientation
|
//! Read image respecting exif orientation
|
||||||
QImage
|
QImage
|
||||||
readImage(QByteArray *data);
|
readImage(const QByteArray *data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,10 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
|
||||||
emit confirmUpload(data_, mediaType_, fileName_.text());
|
emit confirmUpload(data_, mediaType_, fileName_.text());
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
connect(&cancel_, &QPushButton::clicked, this, &PreviewUploadOverlay::close);
|
connect(&cancel_, &QPushButton::clicked, this, [this]() {
|
||||||
|
emit aborted();
|
||||||
|
close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -40,6 +40,7 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
|
void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
|
||||||
|
void aborted();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void init();
|
void init();
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
#include "InputBar.h"
|
#include "InputBar.h"
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include <mtx/responses/common.hpp>
|
#include <mtx/responses/common.hpp>
|
||||||
|
#include <mtx/responses/media.hpp>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
|
#include "Olm.h"
|
||||||
#include "TimelineModel.h"
|
#include "TimelineModel.h"
|
||||||
#include "UserSettingsPage.h"
|
#include "UserSettingsPage.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
#include "dialogs/PreviewUploadOverlay.h"
|
||||||
|
|
||||||
|
#include "blurhash.hpp"
|
||||||
|
|
||||||
static constexpr size_t INPUT_HISTORY_SIZE = 10;
|
static constexpr size_t INPUT_HISTORY_SIZE = 10;
|
||||||
|
|
||||||
|
@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse)
|
||||||
if (!md)
|
if (!md)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (md->hasImage()) {
|
nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString());
|
||||||
|
const auto formats = md->formats().filter("/");
|
||||||
|
const auto image = formats.filter("image/", Qt::CaseInsensitive);
|
||||||
|
const auto audio = formats.filter("audio/", Qt::CaseInsensitive);
|
||||||
|
const auto video = formats.filter("video/", Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
if (!image.empty() && md->hasImage()) {
|
||||||
|
showPreview(*md, "", image);
|
||||||
|
} else if (!audio.empty()) {
|
||||||
|
showPreview(*md, "", audio);
|
||||||
|
} else if (!video.empty()) {
|
||||||
|
showPreview(*md, "", video);
|
||||||
|
} else if (md->hasUrls()) {
|
||||||
|
// Generic file path for any platform.
|
||||||
|
QString path;
|
||||||
|
for (auto &&u : md->urls()) {
|
||||||
|
if (u.isLocalFile()) {
|
||||||
|
path = u.toLocalFile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.isEmpty() && QFileInfo{path}.exists()) {
|
||||||
|
showPreview(*md, path, formats);
|
||||||
|
} else {
|
||||||
|
nhlog::ui()->warn("Clipboard does not contain any valid file paths.");
|
||||||
|
}
|
||||||
|
} else if (md->hasFormat("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
|
||||||
|
|
||||||
|
// This MIME type returns a string with multiple lines separated by '\n'. The first
|
||||||
|
// line is the command to perform with the clipboard (not useful to us). The
|
||||||
|
// following lines are the file URIs.
|
||||||
|
//
|
||||||
|
// Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
|
||||||
|
// nautilus_clipboard_get_uri_list_from_selection_data()
|
||||||
|
// https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
|
||||||
|
|
||||||
|
auto data = md->data("x-special/gnome-copied-files").split('\n');
|
||||||
|
if (data.size() < 2) {
|
||||||
|
nhlog::ui()->warn("MIME format is malformed, cannot perform paste.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString path;
|
||||||
|
for (int i = 1; i < data.size(); ++i) {
|
||||||
|
QUrl url{data[i]};
|
||||||
|
if (url.isLocalFile()) {
|
||||||
|
path = 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 {
|
||||||
|
@ -78,6 +146,37 @@ InputBar::send()
|
||||||
nhlog::ui()->debug("Send: {}", text.toStdString());
|
nhlog::ui()->debug("Send: {}", text.toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::openFileSelection()
|
||||||
|
{
|
||||||
|
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||||||
|
const auto fileName = QFileDialog::getOpenFileName(
|
||||||
|
ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
|
||||||
|
|
||||||
|
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(
|
||||||
|
QString("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
|
||||||
InputBar::message(QString msg)
|
InputBar::message(QString msg)
|
||||||
{
|
{
|
||||||
|
@ -149,6 +248,112 @@ InputBar::emote(QString msg)
|
||||||
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::image(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize,
|
||||||
|
const QSize &dimensions,
|
||||||
|
const QString &blurhash)
|
||||||
|
{
|
||||||
|
mtx::events::msg::Image image;
|
||||||
|
image.info.mimetype = mime.toStdString();
|
||||||
|
image.info.size = dsize;
|
||||||
|
image.info.blurhash = blurhash.toStdString();
|
||||||
|
image.body = filename.toStdString();
|
||||||
|
image.info.h = dimensions.height();
|
||||||
|
image.info.w = dimensions.width();
|
||||||
|
|
||||||
|
if (file)
|
||||||
|
image.file = file;
|
||||||
|
else
|
||||||
|
image.url = url.toStdString();
|
||||||
|
|
||||||
|
if (!room->reply().isEmpty()) {
|
||||||
|
image.relates_to.in_reply_to.event_id = room->reply().toStdString();
|
||||||
|
room->resetReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::file(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize)
|
||||||
|
{
|
||||||
|
mtx::events::msg::File file;
|
||||||
|
file.info.mimetype = mime.toStdString();
|
||||||
|
file.info.size = dsize;
|
||||||
|
file.body = filename.toStdString();
|
||||||
|
|
||||||
|
if (encryptedFile)
|
||||||
|
file.file = encryptedFile;
|
||||||
|
else
|
||||||
|
file.url = url.toStdString();
|
||||||
|
|
||||||
|
if (!room->reply().isEmpty()) {
|
||||||
|
file.relates_to.in_reply_to.event_id = room->reply().toStdString();
|
||||||
|
room->resetReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::audio(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize)
|
||||||
|
{
|
||||||
|
mtx::events::msg::Audio audio;
|
||||||
|
audio.info.mimetype = mime.toStdString();
|
||||||
|
audio.info.size = dsize;
|
||||||
|
audio.body = filename.toStdString();
|
||||||
|
audio.url = url.toStdString();
|
||||||
|
|
||||||
|
if (file)
|
||||||
|
audio.file = file;
|
||||||
|
else
|
||||||
|
audio.url = url.toStdString();
|
||||||
|
|
||||||
|
if (!room->reply().isEmpty()) {
|
||||||
|
audio.relates_to.in_reply_to.event_id = room->reply().toStdString();
|
||||||
|
room->resetReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::video(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize)
|
||||||
|
{
|
||||||
|
mtx::events::msg::Video video;
|
||||||
|
video.info.mimetype = mime.toStdString();
|
||||||
|
video.info.size = dsize;
|
||||||
|
video.body = filename.toStdString();
|
||||||
|
|
||||||
|
if (file)
|
||||||
|
video.file = file;
|
||||||
|
else
|
||||||
|
video.url = url.toStdString();
|
||||||
|
|
||||||
|
if (!room->reply().isEmpty()) {
|
||||||
|
video.relates_to.in_reply_to.event_id = room->reply().toStdString();
|
||||||
|
room->resetReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
InputBar::command(QString command, QString args)
|
InputBar::command(QString command, QString args)
|
||||||
{
|
{
|
||||||
|
@ -196,3 +401,113 @@ InputBar::command(QString command, QString args)
|
||||||
cache::dropOutboundMegolmSession(room->roomId().toStdString());
|
cache::dropOutboundMegolmSession(room->roomId().toStdString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats)
|
||||||
|
{
|
||||||
|
dialogs::PreviewUploadOverlay *previewDialog_ =
|
||||||
|
new dialogs::PreviewUploadOverlay(ChatPage::instance());
|
||||||
|
previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
if (source.hasImage())
|
||||||
|
previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()),
|
||||||
|
formats.front());
|
||||||
|
else if (!path.isEmpty())
|
||||||
|
previewDialog_->setPreview(path);
|
||||||
|
else if (!formats.isEmpty()) {
|
||||||
|
auto mime = formats.first();
|
||||||
|
previewDialog_->setPreview(source.data(mime), mime);
|
||||||
|
} else {
|
||||||
|
setUploading(false);
|
||||||
|
previewDialog_->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() {
|
||||||
|
setUploading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(
|
||||||
|
previewDialog_,
|
||||||
|
&dialogs::PreviewUploadOverlay::confirmUpload,
|
||||||
|
this,
|
||||||
|
[this](const QByteArray data, const QString &mime, const QString &fn) {
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
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.split("/")[0];
|
||||||
|
if (mimeClass == "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 == "image")
|
||||||
|
image(filename,
|
||||||
|
encryptedFile,
|
||||||
|
url,
|
||||||
|
mime,
|
||||||
|
size,
|
||||||
|
dimensions,
|
||||||
|
blurhash);
|
||||||
|
else if (mimeClass == "audio")
|
||||||
|
audio(filename, encryptedFile, url, mime, size);
|
||||||
|
else if (mimeClass == "video")
|
||||||
|
video(filename, encryptedFile, url, mime, size);
|
||||||
|
else
|
||||||
|
file(filename, encryptedFile, url, mime, size);
|
||||||
|
|
||||||
|
setUploading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -3,11 +3,17 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
|
||||||
|
#include <mtx/common.hpp>
|
||||||
|
#include <mtx/responses/messages.hpp>
|
||||||
|
|
||||||
class TimelineModel;
|
class TimelineModel;
|
||||||
|
class QMimeData;
|
||||||
|
class QStringList;
|
||||||
|
|
||||||
class InputBar : public QObject
|
class InputBar : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
InputBar(TimelineModel *parent)
|
InputBar(TimelineModel *parent)
|
||||||
|
@ -19,18 +25,53 @@ public slots:
|
||||||
void send();
|
void send();
|
||||||
void paste(bool fromMouse);
|
void paste(bool fromMouse);
|
||||||
void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
|
void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
|
||||||
|
void openFileSelection();
|
||||||
|
bool uploading() const { return uploading_; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void insertText(QString text);
|
void insertText(QString text);
|
||||||
|
void uploadingChanged(bool value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void message(QString body);
|
void message(QString body);
|
||||||
void emote(QString body);
|
void emote(QString body);
|
||||||
void command(QString name, QString args);
|
void command(QString name, QString args);
|
||||||
|
void image(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize,
|
||||||
|
const QSize &dimensions,
|
||||||
|
const QString &blurhash);
|
||||||
|
void file(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize);
|
||||||
|
void audio(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize);
|
||||||
|
void video(const QString &filename,
|
||||||
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
||||||
|
const QString &url,
|
||||||
|
const QString &mime,
|
||||||
|
uint64_t dsize);
|
||||||
|
|
||||||
|
void showPreview(const QMimeData &source, QString path, const QStringList &formats);
|
||||||
|
void setUploading(bool value)
|
||||||
|
{
|
||||||
|
if (value != uploading_) {
|
||||||
|
uploading_ = value;
|
||||||
|
emit uploadingChanged(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TimelineModel *room;
|
TimelineModel *room;
|
||||||
QString text;
|
QString text;
|
||||||
std::deque<QString> history_;
|
std::deque<QString> history_;
|
||||||
std::size_t history_index_ = 0;
|
std::size_t history_index_ = 0;
|
||||||
int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
|
int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
|
||||||
|
bool uploading_ = false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -150,7 +150,7 @@ class TimelineModel : public QAbstractListModel
|
||||||
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
|
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
|
||||||
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
|
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
|
||||||
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
|
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
|
||||||
Q_PROPERTY(InputBar *input READ input)
|
Q_PROPERTY(InputBar *input READ input CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TimelineModel(TimelineViewManager *manager,
|
explicit TimelineModel(TimelineViewManager *manager,
|
||||||
|
|
Loading…
Reference in a new issue