diff --git a/imports/Spectral/Component/Timeline/FileDelegate.qml b/imports/Spectral/Component/Timeline/FileDelegate.qml index 0dc52ce..87186b6 100644 --- a/imports/Spectral/Component/Timeline/FileDelegate.qml +++ b/imports/Spectral/Component/Timeline/FileDelegate.qml @@ -51,7 +51,7 @@ ColumnLayout { visible: avatarVisible hint: author.displayName - source: author.avatarUrl + source: author.avatarMediaId } Label { diff --git a/imports/Spectral/Component/Timeline/ImageDelegate.qml b/imports/Spectral/Component/Timeline/ImageDelegate.qml index 2b3b1d6..8efe6a3 100644 --- a/imports/Spectral/Component/Timeline/ImageDelegate.qml +++ b/imports/Spectral/Component/Timeline/ImageDelegate.qml @@ -51,7 +51,7 @@ ColumnLayout { visible: avatarVisible hint: author.displayName - source: author.avatarUrl + source: author.avatarMediaId } Label { @@ -74,15 +74,19 @@ ColumnLayout { id: img - source: "image://mxc/" + (content.thumbnail_url ? content.thumbnail_url : content.url) + source: downloaded ? progressInfo.localPath : "image://mxc/" + + (content.info && content.info.thumbnail_info ? + content.thumbnailMediaId : content.mediaId) + sourceSize.width: 200 + sourceSize.height: 200 layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { - width: img.width - height: img.height - radius: 24 - } + width: img.width + height: img.height + radius: 24 + } } AutoMouseArea { diff --git a/imports/Spectral/Component/Timeline/MessageDelegate.qml b/imports/Spectral/Component/Timeline/MessageDelegate.qml index 62551d7..a881937 100644 --- a/imports/Spectral/Component/Timeline/MessageDelegate.qml +++ b/imports/Spectral/Component/Timeline/MessageDelegate.qml @@ -47,7 +47,7 @@ ColumnLayout { visible: avatarVisible hint: author.displayName - source: author.avatarUrl + source: author.avatarMediaId } Label { @@ -146,7 +146,7 @@ ColumnLayout { Layout.preferredHeight: 36 Layout.alignment: Qt.AlignTop - source: replyAuthor ? replyAuthor.avatarUrl : "" + source: replyAuthor ? replyAuthor.avatarMediaId : "" hint: replyAuthor ? replyAuthor.displayName : "H" } diff --git a/imports/Spectral/Panel/RoomDrawer.qml b/imports/Spectral/Panel/RoomDrawer.qml index 113005f..475c5f2 100644 --- a/imports/Spectral/Panel/RoomDrawer.qml +++ b/imports/Spectral/Panel/RoomDrawer.qml @@ -24,7 +24,7 @@ Drawer { Layout.alignment: Qt.AlignHCenter hint: room ? room.displayName : "No name" - source: room ? room.avatarUrl : null + source: room ? room.avatarMediaId : null } Label { diff --git a/imports/Spectral/Panel/RoomHeader.qml b/imports/Spectral/Panel/RoomHeader.qml index 91f04a1..1933426 100644 --- a/imports/Spectral/Panel/RoomHeader.qml +++ b/imports/Spectral/Panel/RoomHeader.qml @@ -39,7 +39,7 @@ Control { id: headerImage - source: currentRoom.avatarUrl + source: currentRoom.avatarMediaId hint: currentRoom ? currentRoom.displayName : "No name" } diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index dcd62c4..31548d1 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -113,7 +113,7 @@ Item { Layout.margins: 12 Layout.alignment: Qt.AlignHCenter - source: root.user ? root.user.avatarUrl : null + source: root.user ? root.user.avatarMediaId : null hint: root.user ? root.user.displayName : "?" } @@ -648,7 +648,7 @@ Item { visible: !searchField.active - source: root.user ? root.user.avatarUrl : null + source: root.user ? root.user.avatarMediaId : null hint: root.user ? root.user.displayName : "?" MouseArea { diff --git a/imports/Spectral/Panel/RoomPanel.qml b/imports/Spectral/Panel/RoomPanel.qml index e00a7a4..d7f7a26 100644 --- a/imports/Spectral/Panel/RoomPanel.qml +++ b/imports/Spectral/Panel/RoomPanel.qml @@ -62,7 +62,7 @@ Item { id: roomHeader - avatar: currentRoom ? currentRoom.avatarUrl : "" + avatar: currentRoom ? currentRoom.avatarMediaId : "" topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" atTop: messageListView.atYBeginning @@ -386,7 +386,7 @@ Item { Layout.preferredWidth: 24 Layout.preferredHeight: 24 - source: modelData.avatarUrl + source: modelData.avatarMediaId hint: modelData.displayName } } diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index 73d5054..a172e7a 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -54,7 +54,7 @@ Control { Layout.preferredWidth: 32 Layout.preferredHeight: 32 - source: replyUser ? replyUser.avatarUrl : "" + source: replyUser ? replyUser.avatarMediaId : "" hint: replyUser ? replyUser.displayName : "No name" } @@ -129,7 +129,7 @@ Control { width: 20 height: 20 visible: !isEmoji - source: modelData.avatarUrl || null + source: modelData.avatarMediaId || null } Label { height: parent.height diff --git a/src/imageprovider.cpp b/src/imageprovider.cpp index 72d8030..c067743 100644 --- a/src/imageprovider.cpp +++ b/src/imageprovider.cpp @@ -1,76 +1,87 @@ #include "imageprovider.h" -#include -#include -#include +#include +#include + #include -#include +#include -#include "jobs/mediathumbnailjob.h" +using QMatrixClient::BaseJob; +using QMatrixClient::Connection; -#include "connection.h" - -using QMatrixClient::MediaThumbnailJob; - -ImageProvider::ImageProvider(QObject* parent) - : QObject(parent), - QQuickImageProvider( - QQmlImageProviderBase::Image, - QQmlImageProviderBase::ForceAsynchronousImageLoading) {} - -QImage ImageProvider::requestImage(const QString& id, QSize* pSize, - const QSize& requestedSize) { - if (!id.startsWith("mxc://")) { - qWarning() << "ImageProvider: won't fetch an invalid id:" << id - << "doesn't follow server/mediaId pattern"; - return {}; +ThumbnailResponse::ThumbnailResponse(Connection* c, QString mediaId, + const QSize& requestedSize) + : c(c), + mediaId(std::move(mediaId)), + requestedSize(requestedSize), + errorStr("Image request hasn't started") { + moveToThread(c->thread()); + if (requestedSize.isEmpty()) { + errorStr.clear(); + emit finished(); + return; } - - QUrl mxcUri{id}; - - QImage result = image(mxcUri, requestedSize); - if (result.isNull()) return {}; - if (!requestedSize.isEmpty() && result.size() != requestedSize) { - QImage scaled = result.scaled(requestedSize, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - if (pSize != nullptr) *pSize = scaled.size(); - return scaled; - } - if (pSize != nullptr) *pSize = result.size(); - return result; + // Execute a request on the main thread asynchronously + QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, + Qt::QueuedConnection); } -QImage ImageProvider::image(const QUrl& mxc, const QSize& size) { - QUrl tempfilePath = QUrl::fromLocalFile( - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + - mxc.fileName() + ".png"); - QImage cachedImage; - if (cachedImage.load(tempfilePath.toLocalFile())) { - return cachedImage; +void ThumbnailResponse::startRequest() { + // Runs in the main thread, not QML thread + if (mediaId.count('/') != 1) { + errorStr = + QStringLiteral("Media id '%1' doesn't follow server/mediaId pattern") + .arg(mediaId); + emit finished(); + return; } - MediaThumbnailJob* job = nullptr; - QReadLocker locker(&m_lock); + QWriteLocker _(&lock); + job = c->getThumbnail(mediaId, requestedSize); + // Connect to any possible outcome including abandonment + // to make sure the QML thread is not left stuck forever. + connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult); +} - QMetaObject::invokeMethod( - m_connection, [=] { return m_connection->getThumbnail(mxc, size); }, - Qt::BlockingQueuedConnection, &job); - - if (!job) { - qDebug() << "ImageProvider: failed to send a request"; - return {}; - } - QImage result; +void ThumbnailResponse::prepareResult() { { - QWaitCondition condition; // The most compact way to block on a signal - job->connect(job, &MediaThumbnailJob::finished, job, [&] { - result = job->thumbnail(); - condition.wakeAll(); - }); - condition.wait(&m_lock); + QWriteLocker _(&lock); + Q_ASSERT(job->error() != BaseJob::Pending); + + if (job->error() == BaseJob::Success) { + image = job->thumbnail(); + errorStr.clear(); + } else { + errorStr = job->errorString(); + qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" + << errorStr; + } + job = nullptr; } - - result.save(tempfilePath.toLocalFile()); - - return result; + emit finished(); +} + +QQuickTextureFactory* ThumbnailResponse::textureFactory() const { + QReadLocker _(&lock); + return QQuickTextureFactory::textureFactoryForImage(image); +} + +QString ThumbnailResponse::errorString() const { + QReadLocker _(&lock); + return errorStr; +} + +void ThumbnailResponse::cancel() { + QWriteLocker _(&lock); + if (job) { + job->abandon(); + job = nullptr; + } + errorStr = "Image request has been cancelled"; +} + +QQuickImageResponse* ImageProvider::requestImageResponse( + const QString& id, const QSize& requestedSize) { + qDebug() << "ImageProvider: requesting " << id; + return new ThumbnailResponse(m_connection.load(), id, requestedSize); } diff --git a/src/imageprovider.h b/src/imageprovider.h index 815c4fc..08d6a36 100644 --- a/src/imageprovider.h +++ b/src/imageprovider.h @@ -1,40 +1,61 @@ #ifndef IMAGEPROVIDER_H #define IMAGEPROVIDER_H +#pragma once -#include +#include +#include +#include #include -#include +#include -#include "connection.h" +namespace QMatrixClient { +class Connection; +} -class ImageProvider : public QObject, public QQuickImageProvider { +class ThumbnailResponse : public QQuickImageResponse { + public: + ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId, + const QSize& requestedSize); + ~ThumbnailResponse() override = default; + + void startRequest(); + + private: + QMatrixClient::Connection* c; + const QString mediaId; + const QSize requestedSize; + QMatrixClient::MediaThumbnailJob* job = nullptr; + + QImage image; + QString errorStr; + mutable QReadWriteLock lock; + + void prepareResult(); + QQuickTextureFactory* textureFactory() const override; + QString errorString() const override; + void cancel() override; +}; + +class ImageProvider : public QObject, public QQuickAsyncImageProvider { Q_OBJECT Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) public: - explicit ImageProvider(QObject* parent = nullptr); + explicit ImageProvider() : QObject(), QQuickAsyncImageProvider() {} - QImage requestImage(const QString& id, QSize* pSize, - const QSize& requestedSize) override; - - void initializeEngine(QQmlEngine* engine, const char* uri); + QQuickImageResponse* requestImageResponse( + const QString& id, const QSize& requestedSize) override; QMatrixClient::Connection* connection() { return m_connection; } - void setConnection(QMatrixClient::Connection* newConnection) { - if (m_connection != newConnection) { - m_connection = newConnection; - emit connectionChanged(); - } + void setConnection(QMatrixClient::Connection* connection) { + m_connection.store(connection); } signals: void connectionChanged(); private: - QReadWriteLock m_lock; - QMatrixClient::Connection* m_connection = nullptr; - - QImage image(const QUrl& mxc, const QSize& size); + QAtomicPointer m_connection; }; #endif // IMAGEPROVIDER_H diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 6ec1730..d5e9799 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -14,10 +14,6 @@ #include "utils.h" -static QString parseAvatarUrl(QUrl url) { - return url.host() + "/" + url.path(); -} - QHash MessageEventModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EventTypeRole] = "eventType"; diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index 01696d2..28068c0 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -82,7 +82,6 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) { if (event->isStateEvent()) return; User* sender = room->user(event->senderId()); if (sender == room->localUser()) return; - QUrl _url = room->avatarUrl(); emit newMessage( room->id(), event->id(), room->displayName(), sender->displayname(), utils::eventToString(*event), @@ -151,7 +150,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const { } SpectralRoom* room = m_rooms.at(index.row()); if (role == NameRole) return room->displayName(); - if (role == AvatarRole) return room->avatarUrl(); + if (role == AvatarRole) return room->avatarMediaId(); if (role == TopicRole) return room->topic(); if (role == CategoryRole) { if (room->joinState() == JoinState::Invite) return RoomType::Invited; diff --git a/src/userlistmodel.cpp b/src/userlistmodel.cpp index 851cd09..15cff77 100644 --- a/src/userlistmodel.cpp +++ b/src/userlistmodel.cpp @@ -69,7 +69,7 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const { return user->id(); } if (role == AvatarRole) { - return user->avatarUrl(); + return user->avatarMediaId(); } return QVariant();