From 10794628ede22bf4807e7d05af9f33a55361a7c7 Mon Sep 17 00:00:00 2001 From: James Graham Date: Wed, 3 May 2023 17:50:48 +0000 Subject: [PATCH] MessageEventModel media info improvements Create a `messageeventmodel` role for media info and reply media info that is a QMap with all the required data. This replaces the MediaUrlRole, FileMimeTypeRole and the ContentTypeRole. The reply role no longer needs the content role. This also ensures mxc urls are now generated for replies. All the media parameters will now have default values assigned in the model so the QML no longer needs to do this. --- src/models/messageeventmodel.cpp | 181 +++++++++++------- src/models/messageeventmodel.h | 9 +- .../Component/NeochatMaximizeComponent.qml | 6 +- src/qml/Component/Timeline/AudioDelegate.qml | 2 +- src/qml/Component/Timeline/FileDelegate.qml | 4 +- src/qml/Component/Timeline/ImageDelegate.qml | 20 +- src/qml/Component/Timeline/ReplyComponent.qml | 32 +++- .../Component/Timeline/TimelineContainer.qml | 3 +- src/qml/Component/Timeline/VideoDelegate.qml | 14 +- 9 files changed, 164 insertions(+), 107 deletions(-) diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index 8bfd9a8aa..64ef1ad94 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -40,16 +40,16 @@ QHash MessageEventModel::roleNames() const roles[SectionRole] = "section"; roles[AuthorRole] = "author"; roles[ContentRole] = "content"; - roles[ContentTypeRole] = "contentType"; roles[HighlightRole] = "isHighlighted"; roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; - roles[FileMimetypeIcon] = "fileMimetypeIcon"; roles[EventResolvedTypeRole] = "eventResolvedType"; + roles[MediaInfoRole] = "mediaInfo"; roles[IsReplyRole] = "isReply"; roles[ReplyAuthor] = "replyAuthor"; roles[ReplyRole] = "reply"; roles[ReplyIdRole] = "replyId"; + roles[ReplyMediaInfoRole] = "replyMediaInfo"; roles[ShowAuthorRole] = "showAuthor"; roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; @@ -60,7 +60,6 @@ QHash MessageEventModel::roleNames() const roles[MimeTypeRole] = "mimeType"; roles[FormattedBodyRole] = "formattedBody"; roles[AuthorIdRole] = "authorId"; - roles[MediaUrlRole] = "mediaUrl"; roles[VerifiedRole] = "verified"; roles[DisplayNameForInitialsRole] = "displayNameForInitials"; roles[AuthorDisplayNameRole] = "authorDisplayName"; @@ -567,14 +566,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return userInContext(author, m_currentRoom); } - if (role == ContentTypeRole) { - if (auto e = eventCast(&evt)) { - const auto &contentType = e->mimeType().name(); - return contentType == "text/plain" ? QStringLiteral("text/html") : contentType; - } - return QStringLiteral("text/plain"); - } - if (role == ContentRole) { if (evt.isRedacted()) { auto reason = evt.redactedBecause()->reason(); @@ -601,15 +592,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt); } - if (role == FileMimetypeIcon) { - auto e = eventCast(&evt); - if (!e || !e->hasFileContent()) { - return QVariant(); - } - - return e->content()->fileInfo()->mimeType.iconName(); - } - if (role == MimeTypeRole) { if (auto e = eventCast(&evt)) { if (!e || !e->hasFileContent()) { @@ -695,6 +677,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return role == TimeRole ? QVariant(ts) : renderDate(ts); } + if (role == MediaInfoRole) { + return getMediaInfoForEvent(evt); + } + if (role == IsReplyRole) { return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty(); } @@ -714,6 +700,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } } + if (role == ReplyMediaInfoRole) { + auto replyPtr = getReplyForEvent(evt); + if (!replyPtr) { + return {}; + } + return getMediaInfoForEvent(*replyPtr); + } + if (role == ReplyRole) { auto replyPtr = getReplyForEvent(evt); if (!replyPtr) { @@ -752,22 +746,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const type = DelegateType::Other; } - QVariant content; - if (auto e = eventCast(replyPtr)) { - // Cannot use e.contentJson() here because some - // EventContent classes inject values into the copy of the - // content JSON stored in EventContent::Base - content = e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); - }; - - if (auto e = eventCast(replyPtr)) { - content = QVariant::fromValue(e->image().originalJson); - } - return QVariantMap{ - {"eventId", replyPtr->id()}, {"display", m_currentRoom->eventToString(*replyPtr, Qt::RichText)}, - {"content", content}, {"type", type}, }; } @@ -926,38 +906,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return evt.senderId(); } - if (role == MediaUrlRole) { -#ifdef QUOTIENT_07 - if (auto e = eventCast(&evt)) { - if (!e->hasFileContent()) { - return QVariant(); - } - if (e->content()->originalJson.contains(QStringLiteral("file")) && e->content()->originalJson["file"].toObject().contains(QStringLiteral("url"))) { - return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["file"]["url"].toString()); - } - if (e->content()->originalJson.contains(QStringLiteral("url"))) { - return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["url"].toString()); - } - } - - // Requires https://github.com/quotient-im/libQuotient/pull/570 - // if (auto e = eventCast(&evt)) { - // return m_currentRoom->makeMediaUrl(e->id(), e->url()); - // } -#endif - - // Construct link in the same form as urlToDownload as that function doesn't work for stickers - if (auto e = eventCast(&evt)) { - auto url = QUrl(m_currentRoom->connection()->homeserver().toString() + "/_matrix/media/r0/download/" + e->url().toString().remove("mxc://")); - QUrlQuery q(url.query()); - q.addQueryItem("allow_remote", "true"); - url.setQuery(q); - return url; - } - - return m_currentRoom->urlToDownload(evt.id()); - } - if (role == VerifiedRole) { #ifdef QUOTIENT_07 #ifdef Quotient_E2EE_ENABLED @@ -1120,3 +1068,104 @@ const RoomEvent *MessageEventModel::getReplyForEvent(const RoomEvent &event) con } return replyPtr; } + +QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const +{ + QVariantMap mediaInfo; + + QString eventId = event.id(); + + // Get the file info for the event. + const EventContent::FileInfo *fileInfo; +#ifdef QUOTIENT_07 + if (event.is()) { + auto roomMessageEvent = eventCast(&event); +#else + if (auto roomMessageEvent = eventCast(&event)) { +#endif + if (!roomMessageEvent->hasFileContent()) { + return {}; + } + fileInfo = roomMessageEvent->content()->fileInfo(); +#ifdef QUOTIENT_07 + } else if (event.is()) { + auto stickerEvent = eventCast(&event); +#else + } else if (auto stickerEvent = eventCast(&event)) { +#endif + fileInfo = &stickerEvent->image(); + } else { + return {}; + } + + return getMediaInfoFromFileInfo(fileInfo, eventId); +} + +QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const +{ + QVariantMap mediaInfo; + + // Get the mxc URL for the media. +#ifdef QUOTIENT_07 + QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url()); + + if (source.isValid() && source.scheme() == QStringLiteral("mxc")) { + mediaInfo["source"] = source; + } else { + mediaInfo["source"] = QUrl(); + } +#else + auto url = QUrl(m_currentRoom->connection()->homeserver().toString() + "/_matrix/media/r0/download/" + fileInfo->url.toString().remove("mxc://")); + QUrlQuery q(url.query()); + q.addQueryItem("allow_remote", "true"); + url.setQuery(q); + mediaInfo["source"] = url; +#endif + + auto mimeType = fileInfo->mimeType; + // Add the MIME type for the media if available. + mediaInfo["mimeType"] = mimeType.name(); + + // Add the MIME type icon if available. + mediaInfo["mimeIcon"] = mimeType.iconName(); + + // Add media size if available. + mediaInfo["size"] = fileInfo->payloadSize; + + // Add parameter depending on media type. + if (mimeType.name().contains(QStringLiteral("image"))) { + if (auto castInfo = static_cast(fileInfo)) { + mediaInfo["width"] = castInfo->imageSize.width(); + mediaInfo["height"] = castInfo->imageSize.height(); + + if (!isThumbnail) { + mediaInfo["thumbnailInfo"] = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true); + } + + QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"].toString(); + if (blurhash.isEmpty()) { + mediaInfo["blurhash"] = QUrl(); + } else { + mediaInfo["blurhash"] = QUrl("image://blurhash/" + blurhash); + } + } + } + if (mimeType.name().contains(QStringLiteral("video"))) { + if (auto castInfo = static_cast(fileInfo)) { + mediaInfo["width"] = castInfo->imageSize.width(); + mediaInfo["height"] = castInfo->imageSize.height(); + mediaInfo["duration"] = castInfo->duration; + + if (!isThumbnail) { + mediaInfo["thumbnailInfo"] = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true); + } + } + } + if (mimeType.name().contains(QStringLiteral("audio"))) { + if (auto castInfo = static_cast(fileInfo)) { + mediaInfo["duration"] = castInfo->duration; + } + } + + return mediaInfo; +} diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index 46328c7bd..10f8e86a9 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -64,20 +64,20 @@ public: SectionRole, /**< The date of the event as a string. */ AuthorRole, /**< The author of the event. */ ContentRole, /**< The full message content. */ - ContentTypeRole, /**< The content mime type. */ HighlightRole, /**< Whether the event should be highlighted. */ SpecialMarksRole, /**< Whether the event is hidden or not. */ LongOperationRole, /**< Progress info when downloading files. */ FormattedBodyRole, /**< The formatted body of a rich message. */ GenericDisplayRole, /**< A generic string based upon the message type. */ + MediaInfoRole, /**< The media info for the event. */ MimeTypeRole, /**< The mime type of the message's file or media. */ - FileMimetypeIcon, /**< The icon name for the mime type of a file. */ IsReplyRole, /**< Is the message a reply to another event. */ ReplyAuthor, /**< The author of the event that was replied to. */ - ReplyRole, /**< The content data of the message that was replied to. */ ReplyIdRole, /**< The matrix ID of the message that was replied to. */ + ReplyMediaInfoRole, /**< The media info of the message that was replied to. */ + ReplyRole, /**< The content data of the message that was replied to. */ ShowAuthorRole, /**< Whether the author's name should be shown. */ ShowSectionRole, /**< Whether the section header should be shown. */ @@ -87,7 +87,6 @@ public: ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ ReactionRole, /**< List of reactions to this event. */ SourceRole, /**< The full message source JSON. */ - MediaUrlRole, /**< The source URL for any media in the message. */ // For debugging EventResolvedTypeRole, /**< The event type the message. */ @@ -194,6 +193,8 @@ private: void moveReadMarker(const QString &toEventId); const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const; + QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const; + QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const; std::vector> m_extraEvents; diff --git a/src/qml/Component/NeochatMaximizeComponent.qml b/src/qml/Component/NeochatMaximizeComponent.qml index ba1bcc62e..edc103849 100644 --- a/src/qml/Component/NeochatMaximizeComponent.qml +++ b/src/qml/Component/NeochatMaximizeComponent.qml @@ -18,9 +18,9 @@ Components.AlbumMaximizeComponent { property list items: [ Components.AlbumModelItem { - type: root.modelData.delegateType === MessageEventModel.Image ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video - source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaUrl - tempSource: modelData.content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + modelData.content.info["xyz.amorgan.blurhash"]) : "" + type: root.modelData.delegateType === MessageEventModel.Image || root.modelData.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video + source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaInfo.source + tempSource: modelData.mediaInfo.blurhash caption: modelData.display } ] diff --git a/src/qml/Component/Timeline/AudioDelegate.qml b/src/qml/Component/Timeline/AudioDelegate.qml index 9c22d2a8c..93bd68943 100644 --- a/src/qml/Component/Timeline/AudioDelegate.qml +++ b/src/qml/Component/Timeline/AudioDelegate.qml @@ -94,7 +94,7 @@ TimelineContainer { visible: false Layout.fillWidth: true from: 0 - to: model.content.info.size + to: model.mediaInfo.size value: model.progressInfo.progress } RowLayout { diff --git a/src/qml/Component/Timeline/FileDelegate.qml b/src/qml/Component/Timeline/FileDelegate.qml index 368224647..b36987e02 100644 --- a/src/qml/Component/Timeline/FileDelegate.qml +++ b/src/qml/Component/Timeline/FileDelegate.qml @@ -103,7 +103,7 @@ TimelineContainer { ] Kirigami.Icon { - source: model.fileMimetypeIcon + source: model.mediaInfo.mimeIcon fallback: "unknown" } @@ -118,7 +118,7 @@ TimelineContainer { QQC2.Label { id: sizeLabel Layout.fillWidth: true - text: Controller.formatByteSize(content.info ? content.info.size : 0) + text: Controller.formatByteSize(model.mediaInfo.size) opacity: 0.7 elide: Text.ElideRight maximumLineCount: 1 diff --git a/src/qml/Component/Timeline/ImageDelegate.qml b/src/qml/Component/Timeline/ImageDelegate.qml index 1b4b0ced1..542bb5c97 100644 --- a/src/qml/Component/Timeline/ImageDelegate.qml +++ b/src/qml/Component/Timeline/ImageDelegate.qml @@ -16,17 +16,9 @@ TimelineContainer { onOpenContextMenu: openFileContext(model, imageDelegate) - property var content: model.content - readonly property bool isAnimated: contentType === "image/gif" - property bool openOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed - readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) - // readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info - readonly property var info: content.info - readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId - readonly property var maxWidth: Kirigami.Units.gridUnit * 30 readonly property var maxHeight: Kirigami.Units.gridUnit * 30 @@ -34,8 +26,8 @@ TimelineContainer { id: img property var imageWidth: { - if (imageDelegate.info && imageDelegate.info.w && imageDelegate.info.w > 0) { - return imageDelegate.info.w; + if (model.mediaInfo.width > 0) { + return model.mediaInfo.width; } else if (sourceSize.width && sourceSize.width > 0) { return sourceSize.width; } else { @@ -43,8 +35,8 @@ TimelineContainer { } } property var imageHeight: { - if (imageDelegate.info && imageDelegate.info.h && imageDelegate.info.h > 0) { - return imageDelegate.info.h; + if (model.mediaInfo.height > 0) { + return model.mediaInfo.height; } else if (sourceSize.height && sourceSize.height > 0) { return sourceSize.height; } else { @@ -78,11 +70,11 @@ TimelineContainer { Layout.maximumHeight: maxSize.height Layout.preferredWidth: imageWidth Layout.preferredHeight: imageHeight - source: model.mediaUrl + source: model.mediaInfo.source Image { anchors.fill: parent - source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : "" + source: model.mediaInfo.blurhash visible: parent.status !== Image.Ready } diff --git a/src/qml/Component/Timeline/ReplyComponent.qml b/src/qml/Component/Timeline/ReplyComponent.qml index deb60e90b..7c97b743d 100644 --- a/src/qml/Component/Timeline/ReplyComponent.qml +++ b/src/qml/Component/Timeline/ReplyComponent.qml @@ -18,6 +18,7 @@ Item { property var name property alias avatar: replyAvatar.source property var color + property var mediaInfo implicitWidth: mainLayout.implicitWidth implicitHeight: mainLayout.implicitHeight @@ -61,6 +62,7 @@ Item { id: loader Layout.fillWidth: true + Layout.maximumHeight: loader.item && (reply.type == MessageEventModel.Image || reply.type == MessageEventModel.Sticker) ? loader.item.height : -1 Layout.columnSpan: 2 sourceComponent: { @@ -112,19 +114,35 @@ Item { id: imageComponent Image { id: image - readonly property var content: reply.content - readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) - readonly property var info: content.info - readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId - source: "image://mxc/" + mediaId + + property var imageWidth: { + if (replyComponent.mediaInfo.width > 0) { + return replyComponent.mediaInfo.width; + } else { + return sourceSize.width; + } + } + property var imageHeight: { + if (replyComponent.mediaInfo.height > 0) { + return replyComponent.mediaInfo.height; + } else { + return sourceSize.height; + } + } + + readonly property var aspectRatio: imageWidth / imageHeight + + height: width / aspectRatio + fillMode: Image.PreserveAspectFit + source: mediaInfo.source } } Component { id: mimeComponent MimeComponent { - mimeIconSource: reply.content.info.mimetype.replace("/", "-") + mimeIconSource: replyComponent.mediaInfo.mimeIcon label: reply.display - subLabel: reply.type === MessageEventModel.File ? Controller.formatByteSize(reply.content.info ? reply.content.info.size : 0) : Controller.formatDuration(reply.content.info.duration) + subLabel: reply.type === MessageEventModel.File ? Controller.formatByteSize(replyComponent.mediaInfo.size) : Controller.formatDuration(replyComponent.mediaInfo.duration) } } Component { diff --git a/src/qml/Component/Timeline/TimelineContainer.qml b/src/qml/Component/Timeline/TimelineContainer.qml index f3988981c..4a8ed471d 100644 --- a/src/qml/Component/Timeline/TimelineContainer.qml +++ b/src/qml/Component/Timeline/TimelineContainer.qml @@ -253,13 +253,14 @@ ColumnLayout { Layout.maximumWidth: contentMaxWidth - active: model.reply !== undefined + active: model.isReply visible: active sourceComponent: ReplyComponent { name: currentRoom.htmlSafeMemberName(model.replyAuthor.id) avatar: model.replyAuthor.avatarSource color: model.replyAuthor.color + mediaInfo: model.replyMediaInfo } Connections { diff --git a/src/qml/Component/Timeline/VideoDelegate.qml b/src/qml/Component/Timeline/VideoDelegate.qml index 8f64860b4..7fa261071 100644 --- a/src/qml/Component/Timeline/VideoDelegate.qml +++ b/src/qml/Component/Timeline/VideoDelegate.qml @@ -22,8 +22,6 @@ TimelineContainer { readonly property var maxWidth: Kirigami.Units.gridUnit * 30 readonly property var maxHeight: Kirigami.Units.gridUnit * 30 - readonly property var info: model.content.info - onOpenContextMenu: openFileContext(model, vid) onDownloadedChanged: { @@ -41,8 +39,8 @@ TimelineContainer { id: vid property var videoWidth: { - if (videoDelegate.info && videoDelegate.info.w && videoDelegate.info.w > 0) { - return videoDelegate.info.w; + if (model.mediaInfo.width > 0) { + return model.mediaInfo.width; } else if (metaData.resolution && metaData.resolution.width) { return metaData.resolution.width; } else { @@ -50,8 +48,8 @@ TimelineContainer { } } property var videoHeight: { - if (videoDelegate.info && videoDelegate.info.h && videoDelegate.info.h > 0) { - return videoDelegate.info.h; + if (model.mediaInfo.height > 0) { + return model.mediaInfo.height; } else if (metaData.resolution && metaData.resolution.height) { return metaData.resolution.height; } else { @@ -154,11 +152,9 @@ TimelineContainer { Image { id: mediaThumbnail anchors.fill: parent - visible: false - source: model.content.thumbnailMediaId ? "image://mxc/" + model.content.thumbnailMediaId : "" - + source: model.mediaInfo.thumbnailInfo.source fillMode: Image.PreserveAspectFit }