diff --git a/.gitignore b/.gitignore
index 0f61a911..2d772e58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ ui_*.h
# Vim
*.swp
+*.swo
#####=== CMake ===#####
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d386efbf..1cf34c32 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -375,7 +375,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/CommunitiesList.h
src/LoginPage.h
src/MainWindow.h
- src/MatrixClient.h
src/InviteeItem.h
src/QuickSwitcher.h
src/RegisterPage.h
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index ee4b53b9..051ea915 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -187,18 +187,23 @@ Rectangle {
id: contextMenu
MenuItem {
- text: "Read receipts"
+ text: qsTr("Read receipts")
onTriggered: chat.model.readReceiptsAction(model.id)
}
MenuItem {
- text: "Mark as read"
+ text: qsTr("Mark as read")
}
MenuItem {
- text: "View raw message"
+ text: qsTr("View raw message")
onTriggered: chat.model.viewRawMessage(model.id)
}
MenuItem {
- text: "Redact message"
+ text: qsTr("Redact message")
+ }
+ MenuItem {
+ visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage
+ text: qsTr("Save as")
+ onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type)
}
}
}
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index f4f5e369..3f5c00bf 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -14,7 +14,7 @@ Item {
MouseArea {
anchors.fill: parent
- onClicked: timelineManager.openImageOverlay(img.source)
+ onClicked: timelineManager.openImageOverlay(eventData.url, eventData.filename, eventData.mimetype, eventData.type)
}
}
}
diff --git a/src/MatrixClient.h b/src/MatrixClient.h
index 2af57267..c77b1183 100644
--- a/src/MatrixClient.h
+++ b/src/MatrixClient.h
@@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json)
Q_DECLARE_METATYPE(std::vector<std::string>)
Q_DECLARE_METATYPE(std::vector<QString>)
-class MediaProxy : public QObject
-{
- Q_OBJECT
-
-signals:
- void imageDownloaded(const QPixmap &);
- void imageSaved(const QString &, const QByteArray &);
- void fileDownloaded(const QByteArray &);
-};
-
namespace http {
mtx::http::Client *
client();
diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp
index 36b768ba..b702686e 100644
--- a/src/timeline2/TimelineModel.cpp
+++ b/src/timeline2/TimelineModel.cpp
@@ -105,6 +105,53 @@ eventUrl(const mtx::events::RoomEvent<T> &e)
}
template<class T>
+QString
+eventFilename(const T &)
+{
+ return "";
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::File> &e)
+{
+ // body may be the original filename
+ if (!e.content.filename.empty())
+ return QString::fromStdString(e.content.filename);
+ return QString::fromStdString(e.content.body);
+}
+
+template<class T>
+QString
+eventMimeType(const T &)
+{
+ return QString();
+}
+template<class T>
+auto
+eventMimeType(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.info.mimetype), std::string>::value, QString>
+{
+ return QString::fromStdString(e.content.info.mimetype);
+}
+
+template<class T>
qml_mtx_events::EventType
toRoomEventType(const mtx::events::Event<T> &e)
{
@@ -288,6 +335,8 @@ TimelineModel::roleNames() const
{UserName, "userName"},
{Timestamp, "timestamp"},
{Url, "url"},
+ {Filename, "filename"},
+ {MimeType, "mimetype"},
{Height, "height"},
{Width, "width"},
{ProportionalHeight, "proportionalHeight"},
@@ -366,6 +415,12 @@ TimelineModel::data(const QModelIndex &index, int role) const
case Url:
return QVariant(boost::apply_visitor(
[](const auto &e) -> QString { return eventUrl(e); }, event));
+ case Filename:
+ return QVariant(boost::apply_visitor(
+ [](const auto &e) -> QString { return eventFilename(e); }, event));
+ case MimeType:
+ return QVariant(boost::apply_visitor(
+ [](const auto &e) -> QString { return eventMimeType(e); }, event));
case Height:
return QVariant(boost::apply_visitor(
[](const auto &e) -> qulonglong { return eventHeight(e); }, event));
diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h
index 3d55f206..e10a0b6e 100644
--- a/src/timeline2/TimelineModel.h
+++ b/src/timeline2/TimelineModel.h
@@ -127,6 +127,8 @@ public:
UserName,
Timestamp,
Url,
+ Filename,
+ MimeType,
Height,
Width,
ProportionalHeight,
diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp
index bf09ef5a..eed0682d 100644
--- a/src/timeline2/TimelineViewManager.cpp
+++ b/src/timeline2/TimelineViewManager.cpp
@@ -1,6 +1,8 @@
#include "TimelineViewManager.h"
+#include <QFileDialog>
#include <QMetaType>
+#include <QMimeDatabase>
#include <QQmlContext>
#include "Logging.h"
@@ -55,24 +57,88 @@ TimelineViewManager::setHistoryView(const QString &room_id)
}
void
-TimelineViewManager::openImageOverlay(QString url) const
+TimelineViewManager::openImageOverlay(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ qml_mtx_events::EventType eventType) const
{
QQuickImageResponse *imgResponse =
- imgProvider->requestImageResponse(url.remove("image://mxcimage/"), QSize());
- connect(imgResponse, &QQuickImageResponse::finished, this, [imgResponse]() {
- if (!imgResponse->errorString().isEmpty()) {
- nhlog::ui()->error("Error when retrieving image for overlay: {}",
- imgResponse->errorString().toStdString());
- return;
- }
- auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
+ imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
+ connect(imgResponse,
+ &QQuickImageResponse::finished,
+ this,
+ [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() {
+ if (!imgResponse->errorString().isEmpty()) {
+ nhlog::ui()->error("Error when retrieving image for overlay: {}",
+ imgResponse->errorString().toStdString());
+ return;
+ }
+ auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
+
+ auto imgDialog = new dialogs::ImageOverlay(pixmap);
+ imgDialog->show();
+ connect(imgDialog,
+ &dialogs::ImageOverlay::saving,
+ this,
+ [this, mxcUrl, originalFilename, mimeType, eventType]() {
+ saveMedia(mxcUrl, originalFilename, mimeType, eventType);
+ });
+ });
+}
+
+void
+TimelineViewManager::saveMedia(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ qml_mtx_events::EventType eventType) const
+{
+ QString dialogTitle;
+ if (eventType == qml_mtx_events::EventType::ImageMessage) {
+ dialogTitle = tr("Save image");
+ } else if (eventType == qml_mtx_events::EventType::VideoMessage) {
+ dialogTitle = tr("Save video");
+ } else if (eventType == qml_mtx_events::EventType::AudioMessage) {
+ dialogTitle = tr("Save audio");
+ } else {
+ dialogTitle = tr("Save file");
+ }
+
+ QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
+
+ auto filename =
+ QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
+
+ if (filename.isEmpty())
+ return;
+
+ const auto url = mxcUrl.toStdString();
+
+ http::client()->download(
+ url,
+ [filename, url](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to retrieve image {}: {} {}",
+ url,
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ return;
+ }
+
+ try {
+ QFile file(filename);
+
+ if (!file.open(QIODevice::WriteOnly))
+ return;
- auto imgDialog = new dialogs::ImageOverlay(pixmap);
- imgDialog->show();
- // connect(imgDialog, &dialogs::ImageOverlay::saving, this,
- // &ImageItem::saveAs);
- Q_UNUSED(imgDialog);
- });
+ file.write(QByteArray(data.data(), data.size()));
+ file.close();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
+ }
+ });
}
void
diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h
index 68f6ddb0..687ae24e 100644
--- a/src/timeline2/TimelineViewManager.h
+++ b/src/timeline2/TimelineViewManager.h
@@ -35,7 +35,30 @@ public:
void clearAll() { models.clear(); }
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
- Q_INVOKABLE void openImageOverlay(QString url) const;
+ void openImageOverlay(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ qml_mtx_events::EventType eventType) const;
+ void saveMedia(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ qml_mtx_events::EventType eventType) const;
+ // Qml can only pass enum as int
+ Q_INVOKABLE void openImageOverlay(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ int eventType) const
+ {
+ openImageOverlay(
+ mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
+ }
+ Q_INVOKABLE void saveMedia(QString mxcUrl,
+ QString originalFilename,
+ QString mimeType,
+ int eventType) const
+ {
+ saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
+ }
signals:
void clearRoomMessageCount(QString roomid);
|