summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt1
-rw-r--r--resources/qml/TimelineView.qml13
-rw-r--r--resources/qml/delegates/ImageMessage.qml2
-rw-r--r--src/MatrixClient.h10
-rw-r--r--src/timeline2/TimelineModel.cpp55
-rw-r--r--src/timeline2/TimelineModel.h2
-rw-r--r--src/timeline2/TimelineViewManager.cpp98
-rw-r--r--src/timeline2/TimelineViewManager.h25
9 files changed, 174 insertions, 33 deletions
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());
-
-                auto imgDialog = new dialogs::ImageOverlay(pixmap);
-                imgDialog->show();
-                // connect(imgDialog, &dialogs::ImageOverlay::saving, this,
-                // &ImageItem::saveAs);
-                Q_UNUSED(imgDialog);
-        });
+          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;
+
+                          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);