Add file uploading

pull/335/head
Nicolas Werner 4 years ago
parent 0bb4885632
commit a31d3d0816
  1. 11
      resources/qml/ActiveCallBar.qml
  2. 45
      resources/qml/MessageInput.qml
  3. 64
      resources/qml/NhekoBusyIndicator.qml
  4. 5
      resources/qml/TimelineView.qml
  5. 3
      resources/qml/VideoCall.qml
  6. 1
      resources/res.qrc
  7. 120
      src/ChatPage.cpp
  8. 11
      src/ChatPage.h
  9. 146
      src/TextInputWidget.cpp
  10. 14
      src/TextInputWidget.h
  11. 5
      src/Utils.cpp
  12. 2
      src/Utils.h
  13. 5
      src/dialogs/PreviewUploadOverlay.cpp
  14. 1
      src/dialogs/PreviewUploadOverlay.h
  15. 317
      src/timeline/InputBar.cpp
  16. 41
      src/timeline/InputBar.h
  17. 2
      src/timeline/TimelineModel.h

@ -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 = "";

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

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

@ -1,7 +1,6 @@
import QtQuick 2.9
import org.freedesktop.gstreamer.GLVideoItem 1.0
GstGLVideoItem {
objectName: "videoCallItem"
objectName: "videoCallItem"
}

@ -132,6 +132,7 @@
<file>qml/Avatar.qml</file>
<file>qml/ImageButton.qml</file>
<file>qml/MatrixText.qml</file>
<file>qml/NhekoBusyIndicator.qml</file>
<file>qml/StatusIndicator.qml</file>
<file>qml/EncryptionIndicator.qml</file>
<file>qml/Reactions.qml</file>

@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
this,
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]() {
if (callManager_->onActiveCall()) {
callManager_->hangUp();

@ -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<mtx::crypto::EncryptedFile> &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);

@ -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<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
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<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)
: 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<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
TextInputWidget::stopTyping()
{

@ -57,9 +57,6 @@ signals:
void startedTyping();
void stoppedTyping();
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.
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<QIODevice> 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_;

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

@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value);
//! Read image respecting exif orientation
QImage
readImage(QByteArray *data);
readImage(const QByteArray *data);
}

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

@ -40,6 +40,7 @@ public:
signals:
void confirmUpload(const QByteArray data, const QString &media, const QString &filename);
void aborted();
private:
void init();

@ -1,18 +1,27 @@
#include "InputBar.h"
#include <QClipboard>
#include <QFileDialog>
#include <QGuiApplication>
#include <QMimeData>
#include <QMimeDatabase>
#include <QStandardPaths>
#include <QUrl>
#include <mtx/responses/common.hpp>
#include <mtx/responses/media.hpp>
#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<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
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<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 <deque>
#include <mtx/common.hpp>
#include <mtx/responses/messages.hpp>
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<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;
QString text;
std::deque<QString> history_;
std::size_t history_index_ = 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 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,

Loading…
Cancel
Save