summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/icons/ui/copy.svg1
-rw-r--r--resources/qml/dialogs/ImageOverlay.qml36
-rw-r--r--resources/res.qrc1
-rw-r--r--src/timeline/InputBar.cpp1
-rw-r--r--src/timeline/TimelineModel.cpp55
-rw-r--r--src/timeline/TimelineModel.h1
-rw-r--r--src/timeline/TimelineViewManager.cpp35
-rw-r--r--src/timeline/TimelineViewManager.h1
8 files changed, 129 insertions, 2 deletions
diff --git a/resources/icons/ui/copy.svg b/resources/icons/ui/copy.svg
new file mode 100644
index 00000000..ae358603
--- /dev/null
+++ b/resources/icons/ui/copy.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.503 4.627 5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.251 2.251 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123ZM17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2h9Zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75Z" fill="#212121"/></svg>
\ No newline at end of file
diff --git a/resources/qml/dialogs/ImageOverlay.qml b/resources/qml/dialogs/ImageOverlay.qml
index 6f56e79a..fa874529 100644
--- a/resources/qml/dialogs/ImageOverlay.qml
+++ b/resources/qml/dialogs/ImageOverlay.qml
@@ -29,6 +29,17 @@ Window {
         onActivated: imageOverlay.close()
     }
 
+    Shortcut {
+        sequence: StandardKey.Copy
+        onActivated: {
+            if (room) {
+                room.copyMedia(eventId);
+            } else {
+                TimelineManager.copyImage(url);
+            }
+        }
+    }
+
     TapHandler {
         onSingleTapped: imageOverlay.close();
     }
@@ -111,10 +122,33 @@ Window {
             height: 48
             width: 48
             hoverEnabled: true
+            image: ":/icons/icons/ui/copy.svg"
+
+            //ToolTip.visible: hovered
+            //ToolTip.delay: Nheko.tooltipDelay
+            //ToolTip.text: qsTr("Copy to clipboard")
+
+            onClicked: {
+                imageOverlay.hide();
+                if (room) {
+                    room.copyMedia(eventId);
+                } else {
+                    TimelineManager.copyImage(url);
+                }
+                imageOverlay.close();
+            }
+        }
+
+        ImageButton {
+            height: 48
+            width: 48
+            hoverEnabled: true
             image: ":/icons/icons/ui/download.svg"
+
             //ToolTip.visible: hovered
             //ToolTip.delay: Nheko.tooltipDelay
             //ToolTip.text: qsTr("Download")
+
             onClicked: {
                 imageOverlay.hide();
                 if (room) {
@@ -130,9 +164,11 @@ Window {
             width: 48
             hoverEnabled: true
             image: ":/icons/icons/ui/dismiss.svg"
+
             //ToolTip.visible: hovered
             //ToolTip.delay: Nheko.tooltipDelay
             //ToolTip.text: qsTr("Close")
+            
             onClicked: imageOverlay.close()
         }
     }
diff --git a/resources/res.qrc b/resources/res.qrc
index 3f1b2b65..412cdb83 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -9,6 +9,7 @@
         <file>icons/ui/checkmark.svg</file>
         <file>icons/ui/clock.svg</file>
         <file>icons/ui/collapsed.svg</file>
+        <file>icons/ui/copy.svg</file>
         <file>icons/ui/delete.svg</file>
         <file>icons/ui/dismiss.svg</file>
         <file>icons/ui/dismiss_edit.svg</file>
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 879ec7cc..3a626a3c 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -148,6 +148,7 @@ InputBar::insertMimeData(const QMimeData *md)
 
     nhlog::ui()->debug("Got mime formats: {}",
                        md->formats().join(QStringLiteral(", ")).toStdString());
+    nhlog::ui()->debug("Has image: {}", md->hasImage());
     const auto formats = md->formats().filter(QStringLiteral("/"));
     const auto image   = formats.filter(QStringLiteral("image/"), Qt::CaseInsensitive);
     const auto audio   = formats.filter(QStringLiteral("audio/"), Qt::CaseInsensitive);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index f80f2ee9..5996bea8 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -12,6 +12,7 @@
 #include <QDesktopServices>
 #include <QFileDialog>
 #include <QGuiApplication>
+#include <QMimeData>
 #include <QMimeDatabase>
 #include <QRegularExpression>
 #include <QStandardPaths>
@@ -1860,6 +1861,60 @@ TimelineModel::saveMedia(const QString &eventId) const
     return true;
 }
 
+bool
+TimelineModel::copyMedia(const QString &eventId) const
+{
+    auto event = events.get(eventId.toStdString(), "");
+    if (!event)
+        return false;
+
+    QString mxcUrl                      = QString::fromStdString(mtx::accessors::url(*event));
+    QString mimeType                    = QString::fromStdString(mtx::accessors::mimetype(*event));
+    qml_mtx_events::EventType eventType = toRoomEventType(*event);
+
+    auto encryptionInfo = mtx::accessors::file(*event);
+
+    const auto url = mxcUrl.toStdString();
+
+    http::client()->download(
+      url,
+      [url, mimeType, eventType, encryptionInfo](const std::string &data,
+                                                 const std::string &,
+                                                 const std::string &,
+                                                 mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve media {}: {} {}",
+                                 url,
+                                 err->matrix_error.error,
+                                 static_cast<int>(err->status_code));
+              return;
+          }
+
+          try {
+              auto temp = data;
+              if (encryptionInfo)
+                  temp =
+                    mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+
+              auto by                 = QByteArray(temp.data(), (qsizetype)temp.size());
+              QMimeData *clipContents = new QMimeData();
+              clipContents->setData(mimeType, by);
+
+              if (eventType == qml_mtx_events::EventType::ImageMessage) {
+                  auto img = utils::readImage(QByteArray(data.data(), (qsizetype)data.size()));
+                  clipContents->setImageData(img);
+              }
+
+              QGuiApplication::clipboard()->setMimeData(clipContents);
+
+              return;
+          } catch (const std::exception &e) {
+              nhlog::ui()->warn("Error while copying file to clipboard: {}", e.what());
+          }
+      });
+    return true;
+}
+
 void
 TimelineModel::cacheMedia(const QString &eventId,
                           const std::function<void(const QString)> &callback)
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 0244c1b1..b0d81441 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -320,6 +320,7 @@ public:
     Q_INVOKABLE void openMedia(const QString &eventId);
     Q_INVOKABLE void cacheMedia(const QString &eventId);
     Q_INVOKABLE bool saveMedia(const QString &eventId) const;
+    Q_INVOKABLE bool copyMedia(const QString &eventId) const;
     Q_INVOKABLE void showEvent(QString eventId);
     Q_INVOKABLE void copyLinkToEvent(const QString &eventId) const;
 
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b949e4c3..44f288c6 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -5,7 +5,9 @@
 #include "TimelineViewManager.h"
 
 #include <QApplication>
+#include <QClipboard>
 #include <QFileDialog>
+#include <QMimeData>
 #include <QStandardPaths>
 #include <QString>
 
@@ -29,8 +31,6 @@
 #include "voip/CallManager.h"
 #include "voip/WebRTCSession.h"
 
-namespace msgs = mtx::events::msg;
-
 namespace {
 template<template<class...> class Op, class... Args>
 using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::value_t;
@@ -319,6 +319,37 @@ TimelineViewManager::saveMedia(QString mxcUrl)
 }
 
 void
+TimelineViewManager::copyImage(const QString &mxcUrl) const
+{
+    const auto url = mxcUrl.toStdString();
+    QString mimeType;
+
+    http::client()->download(
+      url,
+      [url, mimeType](const std::string &data,
+                      const std::string &,
+                      const std::string &,
+                      mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve media {}: {} {}",
+                                 url,
+                                 err->matrix_error.error,
+                                 static_cast<int>(err->status_code));
+              return;
+          }
+
+          try {
+              auto img = utils::readImage(QByteArray(data.data(), (qsizetype)data.size()));
+              QGuiApplication::clipboard()->setImage(img);
+
+              return;
+          } catch (const std::exception &e) {
+              nhlog::ui()->warn("Error while copying file to clipboard: {}", e.what());
+          }
+      });
+}
+
+void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector<QString> &event_ids)
 {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index ee5cf031..e3279e21 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -56,6 +56,7 @@ public:
                                       double proportionalHeight);
     Q_INVOKABLE void openImagePackSettings(QString roomid);
     Q_INVOKABLE void saveMedia(QString mxcUrl);
+    Q_INVOKABLE void copyImage(const QString &mxcUrl) const;
     Q_INVOKABLE QColor userColor(QString id, QColor background);
     Q_INVOKABLE QString escapeEmoji(QString str) const;
     Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }