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 @@
+
\ 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();
}
@@ -107,14 +118,37 @@ Window {
anchors.margins: Nheko.paddingLarge
spacing: Nheko.paddingMedium
+ ImageButton {
+ 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 @@
icons/ui/checkmark.svg
icons/ui/clock.svg
icons/ui/collapsed.svg
+ icons/ui/copy.svg
icons/ui/delete.svg
icons/ui/dismiss.svg
icons/ui/dismiss_edit.svg
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
#include
#include
+#include
#include
#include
#include
@@ -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(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 &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
+#include
#include
+#include
#include
#include
@@ -29,8 +31,6 @@
#include "voip/CallManager.h"
#include "voip/WebRTCSession.h"
-namespace msgs = mtx::events::msg;
-
namespace {
template class Op, class... Args>
using is_detected = typename nheko::detail::detector::value_t;
@@ -318,6 +318,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(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 &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(); }