diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml
index 282cac81..cbe36e6d 100644
--- a/resources/qml/ActiveCallBar.qml
+++ b/resources/qml/ActiveCallBar.qml
@@ -12,8 +12,11 @@ Rectangle {
MouseArea {
anchors.fill: parent
- onClicked: if (TimelineManager.onVideoCall)
- stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
+ onClicked: {
+ if (TimelineManager.onVideoCall)
+ stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
+
+ }
}
RowLayout {
@@ -39,8 +42,7 @@ Rectangle {
Image {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
- source: TimelineManager.onVideoCall ?
- "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+ source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
}
Label {
@@ -69,6 +71,7 @@ Rectangle {
callTimer.startTime = Math.floor(d.getTime() / 1000);
if (TimelineManager.onVideoCall)
stackLayout.currentIndex = 1;
+
break;
case WebRTCState.DISCONNECTED:
callStateLabel.text = "";
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index b76a44f3..a1220599 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -2,7 +2,6 @@ import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
-
import im.nheko 1.0
Rectangle {
@@ -36,6 +35,20 @@ Rectangle {
image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.topMargin: 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 {
@@ -52,27 +65,27 @@ Rectangle {
placeholderTextColor: colors.buttonText
color: colors.text
wrapMode: TextEdit.Wrap
-
onTextChanged: 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)
onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
-
- Connections {
- target: TimelineManager.timeline.input
- function onInsertText(text_) { textArea.insert(textArea.cursorPosition, text_); }
- }
-
Keys.onPressed: {
if (event.matches(StandardKey.Paste)) {
- TimelineManager.timeline.input.paste(false)
- event.accepted = true
+ TimelineManager.timeline.input.paste(false);
+ 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 {
@@ -110,6 +123,10 @@ Rectangle {
Layout.topMargin: 8
Layout.bottomMargin: 8
Layout.rightMargin: 16
+ onClicked: {
+ TimelineManager.timeline.input.send();
+ textArea.clear();
+ }
}
}
diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml
new file mode 100644
index 00000000..8889989a
--- /dev/null
+++ b/resources/qml/NhekoBusyIndicator.qml
@@ -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
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index d85167af..5fce0846 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -192,13 +192,15 @@ Page {
StackLayout {
id: stackLayout
+
currentIndex: 0
Connections {
- target: TimelineManager
function onActiveTimelineChanged() {
stackLayout.currentIndex = 0;
}
+
+ target: TimelineManager
}
MessageView {
@@ -210,6 +212,7 @@ Page {
source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
+
}
TypingIndicator {
diff --git a/resources/qml/VideoCall.qml b/resources/qml/VideoCall.qml
index 69fc1a2b..14408b6e 100644
--- a/resources/qml/VideoCall.qml
+++ b/resources/qml/VideoCall.qml
@@ -1,7 +1,6 @@
import QtQuick 2.9
-
import org.freedesktop.gstreamer.GLVideoItem 1.0
GstGLVideoItem {
- objectName: "videoCallItem"
+ objectName: "videoCallItem"
}
diff --git a/resources/res.qrc b/resources/res.qrc
index efb9c907..02f31498 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -132,6 +132,7 @@
qml/Avatar.qml
qml/ImageButton.qml
qml/MatrixText.qml
+ qml/NhekoBusyIndicator.qml
qml/StatusIndicator.qml
qml/EncryptionIndicator.qml
qml/Reactions.qml
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 6e1ed8ca..e09041e7 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
this,
SIGNAL(unreadMessages(int)));
- connect(
- text_input_,
- &TextInputWidget::uploadMedia,
- this,
- [this](QSharedPointer 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 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 data;
- for (int y = 0; y < img.height(); y++) {
- for (int x = 0; x < img.width(); x++) {
- auto p = img.pixel(x, y);
- data.push_back(static_cast(qRed(p)));
- data.push_back(static_cast(qGreen(p)));
- data.push_back(static_cast(qBlue(p)));
- }
- }
- blurhash = QString::fromStdString(
- blurhash::encode(data.data(), img.width(), img.height(), 4, 3));
- }
-
- http::client()->upload(
- payload,
- encryptedFile ? "application/octet-stream" : mime.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(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 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]() {
if (callManager_->onActiveCall()) {
callManager_->hangUp();
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 9a38fd6e..41c6f276 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -126,17 +126,6 @@ signals:
void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
const QPoint widgetPos);
- void uploadFailed(const QString &msg);
- void mediaUploaded(const QString &roomid,
- const QString &filename,
- const std::optional &file,
- const QString &url,
- const QString &mimeClass,
- const QString &mime,
- qint64 dsize,
- const QSize &dimensions,
- const QString &blurhash);
-
void contentLoaded();
void closing();
void changeWindowTitle(const int);
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index dec7a574..232c0cad 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -88,10 +88,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
typingTimer_->setSingleShot(true);
connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
- connect(&previewDialog_,
- &dialogs::PreviewUploadOverlay::confirmUpload,
- this,
- &FilteredTextEdit::uploadData);
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
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(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
FilteredTextEdit::stopTyping()
{
@@ -494,28 +415,6 @@ FilteredTextEdit::textChanged()
working_history_[history_index_] = toPlainText();
}
-void
-FilteredTextEdit::uploadData(const QByteArray data,
- const QString &mediaType,
- const QString &filename)
-{
- QSharedPointer 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)
: QWidget(parent)
{
@@ -624,7 +523,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
#endif
connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
- connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
connect(emojiBtn_,
SIGNAL(emojiSelected(const QString &)),
this,
@@ -633,9 +531,6 @@ TextInputWidget::TextInputWidget(QWidget *parent)
connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
-
- connect(
- input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner);
}
void
@@ -654,47 +549,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji)
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 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
TextInputWidget::stopTyping()
{
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index f9d84871..44419547 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -57,9 +57,6 @@ signals:
void startedTyping();
void stoppedTyping();
void startedUpload();
- void message(QString msg);
- void command(QString name, QString args);
- void media(QSharedPointer data, QString mimeClass, const QString &filename);
//! Trigger the suggestion popup.
void showSuggestions(const QString &query);
@@ -73,8 +70,6 @@ public slots:
protected:
void keyPressEvent(QKeyEvent *event) override;
- bool canInsertFromMimeData(const QMimeData *source) const override;
- void insertFromMimeData(const QMimeData *source) override;
void focusOutEvent(QFocusEvent *event) override
{
suggestionsPopup_.hide();
@@ -131,9 +126,7 @@ private:
void insertCompletion(QString completion);
void textChanged();
- void uploadData(const QByteArray data, const QString &media, const QString &filename);
void afterCompletion(int);
- void showPreview(const QMimeData *source, const QStringList &formats);
};
class TextInputWidget : public QWidget
@@ -161,8 +154,6 @@ public:
}
public slots:
- void openFileSelection();
- void hideUploadSpinner();
void focusLineEdit() { input_->setFocus(); }
void changeCallButtonState(webrtc::State);
@@ -172,9 +163,6 @@ private slots:
signals:
void heightChanged(int height);
- void uploadMedia(const QSharedPointer data,
- QString mimeClass,
- const QString &filename);
void callButtonPress();
void sendJoinRoomRequest(const QString &room);
@@ -192,8 +180,6 @@ protected:
void paintEvent(QPaintEvent *) override;
private:
- void showUploadSpinner();
-
QHBoxLayout *topLayout_;
FilteredTextEdit *input_;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 38dbba22..2896e773 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -677,9 +677,10 @@ utils::restoreCombobox(QComboBox *combo, const QString &value)
}
QImage
-utils::readImage(QByteArray *data)
+utils::readImage(const QByteArray *data)
{
- QBuffer buf(data);
+ QBuffer buf;
+ buf.setData(*data);
QImageReader reader(&buf);
reader.setAutoTransform(true);
return reader.read();
diff --git a/src/Utils.h b/src/Utils.h
index 5e7fb601..f59e8673 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value);
//! Read image respecting exif orientation
QImage
-readImage(QByteArray *data);
+readImage(const QByteArray *data);
}
diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp
index 20959b0a..e03993c7 100644
--- a/src/dialogs/PreviewUploadOverlay.cpp
+++ b/src/dialogs/PreviewUploadOverlay.cpp
@@ -60,7 +60,10 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent)
emit confirmUpload(data_, mediaType_, fileName_.text());
close();
});
- connect(&cancel_, &QPushButton::clicked, this, &PreviewUploadOverlay::close);
+ connect(&cancel_, &QPushButton::clicked, this, [this]() {
+ emit aborted();
+ close();
+ });
}
void
diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h
index 11cd49bc..5139e3f2 100644
--- a/src/dialogs/PreviewUploadOverlay.h
+++ b/src/dialogs/PreviewUploadOverlay.h
@@ -40,6 +40,7 @@ public:
signals:
void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
+ void aborted();
private:
void init();
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index dcd4a106..bd8f6414 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -1,18 +1,27 @@
#include "InputBar.h"
#include
+#include
#include
#include
+#include
+#include
+#include
#include
+#include
#include "Cache.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
+#include "Olm.h"
#include "TimelineModel.h"
#include "UserSettingsPage.h"
#include "Utils.h"
+#include "dialogs/PreviewUploadOverlay.h"
+
+#include "blurhash.hpp"
static constexpr size_t INPUT_HISTORY_SIZE = 10;
@@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse)
if (!md)
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()) {
emit insertText(md->text());
} else {
@@ -78,6 +146,37 @@ InputBar::send()
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
InputBar::message(QString msg)
{
@@ -149,6 +248,112 @@ InputBar::emote(QString msg)
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
}
+void
+InputBar::image(const QString &filename,
+ const std::optional &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 &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 &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 &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
InputBar::command(QString command, QString args)
{
@@ -196,3 +401,113 @@ InputBar::command(QString command, QString args)
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(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 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 data;
+ for (int y = 0; y < img.height(); y++) {
+ for (int x = 0; x < img.width(); x++) {
+ auto p = img.pixel(x, y);
+ data.push_back(static_cast(qRed(p)));
+ data.push_back(static_cast(qGreen(p)));
+ data.push_back(static_cast(qBlue(p)));
+ }
+ }
+ blurhash = QString::fromStdString(
+ blurhash::encode(data.data(), img.width(), img.height(), 4, 3));
+ }
+
+ http::client()->upload(
+ payload,
+ encryptedFile ? "application/octet-stream" : mime.toStdString(),
+ QFileInfo(fn).fileName().toStdString(),
+ [this,
+ filename = fn,
+ encryptedFile = std::move(encryptedFile),
+ mimeClass,
+ mime,
+ size = payload.size(),
+ dimensions,
+ blurhash](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) mutable {
+ if (err) {
+ emit ChatPage::instance()->showNotification(
+ tr("Failed to upload media. Please try again."));
+ nhlog::net()->warn("failed to upload media: {} {} ({})",
+ err->matrix_error.error,
+ to_string(err->matrix_error.errcode),
+ static_cast(err->status_code));
+ setUploading(false);
+ return;
+ }
+
+ auto url = QString::fromStdString(res.content_uri);
+ if (encryptedFile)
+ encryptedFile->url = res.content_uri;
+
+ if (mimeClass == "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);
+ });
+ });
+}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index f3a38c2e..35e3f8a4 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -3,11 +3,17 @@
#include
#include
+#include
+#include
+
class TimelineModel;
+class QMimeData;
+class QStringList;
class InputBar : public QObject
{
Q_OBJECT
+ Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
public:
InputBar(TimelineModel *parent)
@@ -19,18 +25,53 @@ public slots:
void send();
void paste(bool fromMouse);
void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
+ void openFileSelection();
+ bool uploading() const { return uploading_; }
signals:
void insertText(QString text);
+ void uploadingChanged(bool value);
private:
void message(QString body);
void emote(QString body);
void command(QString name, QString args);
+ void image(const QString &filename,
+ const std::optional &file,
+ const QString &url,
+ const QString &mime,
+ uint64_t dsize,
+ const QSize &dimensions,
+ const QString &blurhash);
+ void file(const QString &filename,
+ const std::optional &encryptedFile,
+ const QString &url,
+ const QString &mime,
+ uint64_t dsize);
+ void audio(const QString &filename,
+ const std::optional &file,
+ const QString &url,
+ const QString &mime,
+ uint64_t dsize);
+ void video(const QString &filename,
+ const std::optional &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;
QString text;
std::deque history_;
std::size_t history_index_ = 0;
int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
+ bool uploading_ = false;
};
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 58a1496c..16a2565e 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -150,7 +150,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
- Q_PROPERTY(InputBar *input READ input)
+ Q_PROPERTY(InputBar *input READ input CONSTANT)
public:
explicit TimelineModel(TimelineViewManager *manager,