diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index f42ec02f..681cbe09 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -35,6 +35,7 @@
#include "dialogs/ImageOverlay.h"
#include "emoji/EmojiModel.h"
#include "emoji/Provider.h"
+#include "ui/MxcAnimatedImage.h"
#include "ui/MxcMediaProxy.h"
#include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h"
@@ -177,6 +178,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
+ qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
qmlRegisterUncreatableType<DeviceVerificationFlow>(
"im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp
new file mode 100644
index 00000000..cfc03827
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.cpp
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "MxcAnimatedImage.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QMimeDatabase>
+#include <QQuickWindow>
+#include <QSGImageNode>
+#include <QStandardPaths>
+
+#include "EventAccessors.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "timeline/TimelineModel.h"
+
+void
+MxcAnimatedImage::startDownload()
+{
+ if (!room_)
+ return;
+ if (eventId_.isEmpty())
+ return;
+
+ auto event = room_->eventById(eventId_);
+ if (!event) {
+ nhlog::ui()->error("Failed to load media for event {}, event not found.",
+ eventId_.toStdString());
+ return;
+ }
+
+ QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
+
+ animatable_ = QMovie::supportedFormats().contains(mimeType.split('/').back());
+ animatableChanged();
+
+ if (!animatable_)
+ return;
+
+ QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
+ QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+
+ auto encryptionInfo = mtx::accessors::file(*event);
+
+ // If the message is a link to a non mxcUrl, don't download it
+ if (!mxcUrl.startsWith("mxc://")) {
+ return;
+ }
+
+ QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
+
+ const auto url = mxcUrl.toStdString();
+ const auto name = QString(mxcUrl).remove("mxc://");
+ QFileInfo filename(QString("%1/media_cache/media/%2.%3")
+ .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+ .arg(name)
+ .arg(suffix));
+ if (QDir::cleanPath(name) != name) {
+ nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
+ return;
+ }
+
+ QDir().mkpath(filename.path());
+
+ QPointer<MxcAnimatedImage> self = this;
+
+ auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) {
+ if (!self)
+ return;
+
+ if (buffer.isOpen()) {
+ movie.stop();
+ movie.setDevice(nullptr);
+ buffer.close();
+ }
+
+ if (encryptionInfo) {
+ QByteArray ba = device.readAll();
+ std::string temp(ba.constData(), ba.size());
+ temp = mtx::crypto::to_string(
+ mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+ buffer.setData(temp.data(), temp.size());
+ } else {
+ buffer.setData(device.readAll());
+ }
+ buffer.open(QIODevice::ReadOnly);
+ buffer.reset();
+
+ QTimer::singleShot(0, this, [this, mimeType] {
+ nhlog::ui()->info("Playing movie with size: {}, {}",
+ buffer.bytesAvailable(),
+ buffer.isOpen());
+ movie.setFormat(mimeType);
+ movie.setDevice(&buffer);
+ movie.start();
+ emit loadedChanged();
+ });
+ };
+
+ if (filename.isReadable()) {
+ QFile f(filename.filePath());
+ if (f.open(QIODevice::ReadOnly)) {
+ processBuffer(f);
+ return;
+ }
+ }
+
+ http::client()->download(
+ url,
+ [filename, url, processBuffer](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 {
+ QFile file(filename.filePath());
+
+ if (!file.open(QIODevice::WriteOnly))
+ return;
+
+ QByteArray ba(data.data(), (int)data.size());
+ file.write(ba);
+ file.close();
+
+ QBuffer buf(&ba);
+ buf.open(QBuffer::ReadOnly);
+ processBuffer(buf);
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
+ }
+ });
+}
+
+QSGNode *
+MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
+{
+ imageDirty = false;
+ QSGImageNode *n = static_cast<QSGImageNode *>(oldNode);
+ if (!n)
+ n = window()->createImageNode();
+
+ // n->setTexture(nullptr);
+ auto img = movie.currentImage();
+ if (!img.isNull())
+ n->setTexture(window()->createTextureFromImage(img));
+ else
+ return nullptr;
+
+ n->setSourceRect(img.rect());
+ n->setRect(QRect(0, 0, width(), height()));
+ n->setFiltering(QSGTexture::Linear);
+ n->setMipmapFiltering(QSGTexture::Linear);
+
+ return n;
+}
diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h
new file mode 100644
index 00000000..7b9502e0
--- /dev/null
+++ b/src/ui/MxcAnimatedImage.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QBuffer>
+#include <QMovie>
+#include <QObject>
+#include <QQuickItem>
+
+class TimelineModel;
+
+// This is an AnimatedImage, that can draw encrypted images
+class MxcAnimatedImage : public QQuickItem
+{
+ Q_OBJECT
+ Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
+ Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
+ Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged)
+ Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
+public:
+ MxcAnimatedImage(QQuickItem *parent = nullptr)
+ : QQuickItem(parent)
+ {
+ connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
+ connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload);
+ connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame);
+ setFlag(QQuickItem::ItemHasContents);
+ // setAcceptHoverEvents(true);
+ }
+
+ bool animatable() const { return animatable_; }
+ bool loaded() const { return buffer.size() > 0; }
+ QString eventId() const { return eventId_; }
+ TimelineModel *room() const { return room_; }
+ void setEventId(QString newEventId)
+ {
+ if (eventId_ != newEventId) {
+ eventId_ = newEventId;
+ emit eventIdChanged();
+ }
+ }
+ void setRoom(TimelineModel *room)
+ {
+ if (room_ != room) {
+ room_ = room;
+ emit roomChanged();
+ }
+ }
+
+ QSGNode *updatePaintNode(QSGNode *oldNode,
+ QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
+
+signals:
+ void roomChanged();
+ void eventIdChanged();
+ void animatableChanged();
+ void loadedChanged();
+
+private slots:
+ void startDownload();
+ void newFrame(int frame)
+ {
+ currentFrame = frame;
+ imageDirty = true;
+ update();
+ }
+
+private:
+ TimelineModel *room_ = nullptr;
+ QString eventId_;
+ QString filename_;
+ bool animatable_ = false;
+ QBuffer buffer;
+ QMovie movie;
+ int currentFrame = 0;
+ bool imageDirty = true;
+};
diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp
index c1de2c31..dc65de7c 100644
--- a/src/ui/MxcMediaProxy.cpp
+++ b/src/ui/MxcMediaProxy.cpp
@@ -91,11 +91,11 @@ MxcMediaProxy::startDownload()
buffer.open(QIODevice::ReadOnly);
buffer.reset();
- QTimer::singleShot(0, this, [this, self, filename] {
+ QTimer::singleShot(0, this, [this, filename] {
nhlog::ui()->info("Playing buffer with size: {}, {}",
buffer.bytesAvailable(),
buffer.isOpen());
- self->setMedia(QMediaContent(filename.fileName()), &buffer);
+ this->setMedia(QMediaContent(filename.fileName()), &buffer);
emit loadedChanged();
});
};
|