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 }