diff --git a/autotests/messagecontentmodeltest.cpp b/autotests/messagecontentmodeltest.cpp index f78365bc2..b6970a31f 100644 --- a/autotests/messagecontentmodeltest.cpp +++ b/autotests/messagecontentmodeltest.cpp @@ -10,7 +10,7 @@ #include #include -#include "models/messagecontentmodel.h" +#include "models/eventmessagecontentmodel.h" #include "neochatconnection.h" #include "testutils.h" @@ -39,13 +39,13 @@ void MessageContentModelTest::initTestCase() void MessageContentModelTest::missingEvent() { auto room = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s); - auto model1 = MessageContentModel(room, u"$153456789:example.org"_s); + auto model1 = EventMessageContentModel(room, u"$153456789:example.org"_s); QCOMPARE(model1.rowCount(), 1); QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s); - auto model2 = MessageContentModel(room, u"$153456789:example.org"_s, true); + auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true); QCOMPARE(model2.rowCount(), 1); QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); diff --git a/autotests/reactionmodeltest.cpp b/autotests/reactionmodeltest.cpp index caf38814d..0eae44d83 100644 --- a/autotests/reactionmodeltest.cpp +++ b/autotests/reactionmodeltest.cpp @@ -9,7 +9,7 @@ #include -#include "models/messagecontentmodel.h" +#include "models/eventmessagecontentmodel.h" #include "testutils.h" using namespace Quotient; @@ -21,7 +21,7 @@ class ReactionModelTest : public QObject private: Connection *connection = nullptr; TestUtils::TestRoom *room = nullptr; - MessageContentModel *parentModel; + EventMessageContentModel *parentModel; private Q_SLOTS: void initTestCase(); @@ -34,7 +34,7 @@ void ReactionModelTest::initTestCase() { connection = Connection::makeMockConnection(u"@bob:kde.org"_s); room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s); - parentModel = new MessageContentModel(room, "123456"_L1); + parentModel = new EventMessageContentModel(room, "123456"_L1); } void ReactionModelTest::basicReaction() diff --git a/src/chatbar/ChatBar.qml b/src/chatbar/ChatBar.qml index 7540aa1e0..8da5d8668 100644 --- a/src/chatbar/ChatBar.qml +++ b/src/chatbar/ChatBar.qml @@ -382,8 +382,6 @@ QQC2.Control { implicitHeight: replyComponent.implicitHeight ReplyComponent { id: replyComponent - replyEventId: _private.chatBarCache.replyId - replyAuthor: _private.chatBarCache.relationAuthor replyContentModel: ContentProvider.contentModelForEvent(root.currentRoom, _private.chatBarCache.replyId, true) Message.maxContentWidth: replyLoader.item.width diff --git a/src/libneochat/eventhandler.cpp b/src/libneochat/eventhandler.cpp index dedb62559..f6330a37f 100644 --- a/src/libneochat/eventhandler.cpp +++ b/src/libneochat/eventhandler.cpp @@ -198,6 +198,10 @@ bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent * Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageEvent &event) { + if (event.isRedacted() && !event.isStateEvent()) { + return Qt::RichText; + } + if (event.mimeType().name() == "text/plain"_L1) { return Qt::PlainText; } else { @@ -207,6 +211,11 @@ Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageE QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event) { + if (event.isRedacted() && !event.isStateEvent()) { + auto reason = event.redactedBecause()->reason(); + return (reason.isEmpty()) ? i18n("[This message was deleted]") : i18n("[This message was deleted: %1]", reason.toHtmlEscaped()); + } + QString body; if (event.has()) { diff --git a/src/libneochat/messagecomponent.h b/src/libneochat/messagecomponent.h index b643b295c..c84e7083a 100644 --- a/src/libneochat/messagecomponent.h +++ b/src/libneochat/messagecomponent.h @@ -7,12 +7,12 @@ struct MessageComponent { MessageComponentType::Type type = MessageComponentType::Other; - QString content; + QString display; QVariantMap attributes; bool operator==(const MessageComponent &right) const { - return type == right.type && content == right.content && attributes == right.attributes; + return type == right.type && display == right.display && attributes == right.attributes; } bool isEmpty() const diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index 9f5b962a5..a188a4c59 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -1319,6 +1319,19 @@ void NeoChatRoom::copyEventMedia(const QString &eventId) } } +FileTransferInfo NeoChatRoom::cachedFileTransferInfo(const QString &eventId) const +{ + if (eventId.isEmpty()) { + return {}; + } + + const auto eventResult = getEvent(eventId); + if (!eventResult.first) { + return {}; + } + return cachedFileTransferInfo(eventResult.first); +} + FileTransferInfo NeoChatRoom::cachedFileTransferInfo(const Quotient::RoomEvent *event) const { QString mxcUrl; diff --git a/src/libneochat/neochatroom.h b/src/libneochat/neochatroom.h index 5bd789ef6..d385e7ec1 100644 --- a/src/libneochat/neochatroom.h +++ b/src/libneochat/neochatroom.h @@ -544,7 +544,15 @@ public: * @brief Return the cached file transfer information for the event. * * If we downloaded the file previously, return a struct with Completed status - * and the local file path stored in KSharedCOnfig + * and the local file path stored in KSharedConfig + */ + Quotient::FileTransferInfo cachedFileTransferInfo(const QString &eventId) const; + + /** + * @brief Return the cached file transfer information for the event. + * + * If we downloaded the file previously, return a struct with Completed status + * and the local file path stored in KSharedConfig */ Quotient::FileTransferInfo cachedFileTransferInfo(const Quotient::RoomEvent *event) const; diff --git a/src/libneochat/texthandler.cpp b/src/libneochat/texthandler.cpp index f0b942edb..e93ecf848 100644 --- a/src/libneochat/texthandler.cpp +++ b/src/libneochat/texthandler.cpp @@ -592,7 +592,7 @@ TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const Ne if (event != nullptr && room != nullptr) { if (auto e = eventCast(event); e && e->msgtype() == Quotient::MessageEventType::Emote && components.size() == 1) { if (components[0].type == MessageComponentType::Text) { - components[0].content = emoteString(room, event) + components[0].content; + components[0].display = emoteString(room, event) + components[0].display; } else { components.prepend(MessageComponent{MessageComponentType::Text, emoteString(room, event), {}}); } diff --git a/src/messagecontent/AudioComponent.qml b/src/messagecontent/AudioComponent.qml index f3b846d8e..7c5cd614b 100644 --- a/src/messagecontent/AudioComponent.qml +++ b/src/messagecontent/AudioComponent.qml @@ -24,19 +24,9 @@ ColumnLayout { required property string eventId /** - * @brief The media info for the event. - * - * This should consist of the following: - * - source - The mxc URL for the media. - * - mimeType - The MIME type of the media (should be image/xxx for this delegate). - * - mimeIcon - The MIME icon name (should be image-xxx). - * - size - The file size in bytes. - * - width - The width in pixels of the audio media. - * - height - The height in pixels of the audio media. - * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads. - * - filename - original filename of the media + * @brief The attributes of the component. */ - required property var mediaInfo + required property var componentAttributes /** * @brief FileTransferInfo for any downloading files. @@ -130,12 +120,12 @@ ColumnLayout { spacing: 0 QQC2.Label { - text: root.mediaInfo.filename + text: root.componentAttributes.filename wrapMode: Text.Wrap Layout.fillWidth: true } QQC2.Label { - text: Format.formatDuration(root.mediaInfo.duration) + text: Format.formatDuration(root.componentAttributes.duration) color: Kirigami.Theme.disabledTextColor visible: !audio.hasAudio Layout.fillWidth: true @@ -147,7 +137,7 @@ ColumnLayout { visible: false Layout.fillWidth: true from: 0 - to: root.mediaInfo.size + to: root.componentAttributes.size value: root.fileTransferInfo.progress } RowLayout { diff --git a/src/messagecontent/CMakeLists.txt b/src/messagecontent/CMakeLists.txt index bd93c6449..ebb56633c 100644 --- a/src/messagecontent/CMakeLists.txt +++ b/src/messagecontent/CMakeLists.txt @@ -52,6 +52,7 @@ ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE models/pollanswermodel.cpp models/reactionmodel.cpp models/threadmodel.cpp + models/eventmessagecontentmodel.cpp RESOURCES images/bike.svg images/bus.svg diff --git a/src/messagecontent/ChatBarComponent.qml b/src/messagecontent/ChatBarComponent.qml index fb2d8742a..3529d82c4 100644 --- a/src/messagecontent/ChatBarComponent.qml +++ b/src/messagecontent/ChatBarComponent.qml @@ -222,8 +222,6 @@ QQC2.Control { implicitHeight: replyComponent.implicitHeight ReplyComponent { id: replyComponent - replyEventId: root.chatBarCache.replyId - replyAuthor: root.chatBarCache.relationAuthor replyContentModel: ContentProvider.contentModelForEvent(root.Message.room, root.chatBarCache.replyId, true) Message.maxContentWidth: paneLoader.item.width } diff --git a/src/messagecontent/FileComponent.qml b/src/messagecontent/FileComponent.qml index f642b51cb..413b24769 100644 --- a/src/messagecontent/FileComponent.qml +++ b/src/messagecontent/FileComponent.qml @@ -26,19 +26,9 @@ ColumnLayout { required property string eventId /** - * @brief The media info for the event. - * - * This should consist of the following: - * - source - The mxc URL for the media. - * - mimeType - The MIME type of the media (should be image/xxx for this delegate). - * - mimeIcon - The MIME icon name (should be image-xxx). - * - size - The file size in bytes. - * - width - The width in pixels of the audio media. - * - height - The height in pixels of the audio media. - * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads. - * - filename - original filename of the media + * @brief The attributes of the component. */ - required property var mediaInfo + required property var componentAttributes /** * @brief FileTransferInfo for any downloading files. @@ -134,7 +124,7 @@ ColumnLayout { ] Kirigami.Icon { - source: root.mediaInfo.mimeIcon + source: root.componentAttributes.mimeIcon fallback: "unknown" } @@ -142,14 +132,14 @@ ColumnLayout { spacing: 0 QQC2.Label { Layout.fillWidth: true - text: root.mediaInfo.filename + text: root.componentAttributes.filename wrapMode: Text.Wrap elide: Text.ElideRight } QQC2.Label { id: sizeLabel Layout.fillWidth: true - text: Format.formatByteSize(root.mediaInfo.size) + text: Format.formatByteSize(root.componentAttributes.size) opacity: 0.7 elide: Text.ElideRight maximumLineCount: 1 diff --git a/src/messagecontent/ImageComponent.qml b/src/messagecontent/ImageComponent.qml index 5175f00c9..8023edd80 100644 --- a/src/messagecontent/ImageComponent.qml +++ b/src/messagecontent/ImageComponent.qml @@ -27,19 +27,9 @@ Item { required property string display /** - * @brief The media info for the event. - * - * This should consist of the following: - * - source - The mxc URL for the media. - * - mimeType - The MIME type of the media (should be image/xxx for this delegate). - * - mimeIcon - The MIME icon name (should be image-xxx). - * - size - The file size in bytes. - * - width - The width in pixels of the audio media. - * - height - The height in pixels of the audio media. - * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads. - * - isSticker - Whether the image is a sticker or not + * @brief The attributes of the component. */ - required property var mediaInfo + required property var componentAttributes /** * @brief FileTransferInfo for any downloading files. @@ -77,9 +67,9 @@ Item { anchors.fill: parent - active: !root.mediaInfo.animated && !_private.hideImage + active: !root.componentAttributes.animated && !_private.hideImage sourceComponent: Image { - source: root.mediaInfo.source + source: root.componentAttributes.source sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio @@ -93,9 +83,9 @@ Item { anchors.fill: parent - active: (root?.mediaInfo.animated ?? false) && !_private.hideImage + active: (root?.componentAttributes.animated ?? false) && !_private.hideImage sourceComponent: AnimatedImage { - source: root.mediaInfo.source + source: root.componentAttributes.source fillMode: Image.PreserveAspectFit autoTransform: true @@ -106,7 +96,7 @@ Item { Image { anchors.fill: parent - source: visible ? (root?.mediaInfo.tempInfo?.source ?? "") : "" + source: visible ? (root?.componentAttributes.tempInfo?.source ?? "") : "" visible: _private.imageItem && _private.imageItem.status !== Image.Ready && !_private.hideImage } @@ -153,11 +143,11 @@ Item { gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds onTapped: { root.QQC2.ToolTip.hide(); - if (root.mediaInfo.animated) { + if (root.componentAttributes.animated) { _private.imageItem.paused = true; } root.Message.timeline.interactive = false; - if (!root.mediaInfo.isSticker) { + if (!root.componentAttributes.isSticker) { RoomManager.maximizeMedia(root.eventId); } } @@ -183,13 +173,13 @@ Item { id: mediaSizeHelper contentMaxWidth: root.Message.maxContentWidth contentMaxHeight: root.contentMaxHeight ?? -1 - mediaWidth: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.width ?? 0) - mediaHeight: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.height ?? 0) + mediaWidth: root?.componentAttributes.isSticker ? 256 : (root?.componentAttributes.width ?? 0) + mediaHeight: root?.componentAttributes.isSticker ? 256 : (root?.componentAttributes.height ?? 0) } QtObject { id: _private - readonly property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item + readonly property var imageItem: root.componentAttributes.animated ? animatedImageLoader.item : imageLoader.item // The space available for the component after taking away the border readonly property real downloaded: root.fileTransferInfo && root.fileTransferInfo.completed diff --git a/src/messagecontent/LocationComponent.qml b/src/messagecontent/LocationComponent.qml index bdf854a7f..89f9d6bb1 100644 --- a/src/messagecontent/LocationComponent.qml +++ b/src/messagecontent/LocationComponent.qml @@ -37,22 +37,9 @@ ColumnLayout { required property string display /** - * @brief The latitude of the location marker in the message. + * @brief The attributes of the component. */ - required property real latitude - - /** - * @brief The longitude of the location marker in the message. - */ - required property real longitude - - /** - * @brief What type of marker the location message is. - * - * The main options are m.pin for a general location or m.self for a pin to show - * a user's location. - */ - required property string asset + required property var componentAttributes Layout.fillWidth: true Layout.maximumWidth: Message.maxContentWidth @@ -63,15 +50,15 @@ ColumnLayout { Layout.preferredWidth: root.Message.maxContentWidth Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9 - map.center: QtPositioning.coordinate(root.latitude, root.longitude) + map.center: QtPositioning.coordinate(root.componentAttributes.latitude, root.componentAttributes.longitude) map.zoomLevel: 15 map.plugin: OsmLocationPlugin.plugin readonly property LocationMapItem locationMapItem: LocationMapItem { - latitude: root.latitude - longitude: root.longitude - asset: root.asset + latitude: root.componentAttributes.latitude + longitude: root.componentAttributes.longitude + asset: root.componentAttributes.asset author: root.author isLive: true heading: NaN @@ -100,7 +87,7 @@ ColumnLayout { icon.name: "open-link-symbolic" display: AbstractButton.IconOnly - onClicked: Qt.openUrlExternally("geo:" + root.latitude + "," + root.longitude) + onClicked: Qt.openUrlExternally("geo:" + root.componentAttributes.latitude + "," + root.componentAttributes.longitude) ToolTip.text: text ToolTip.visible: hovered @@ -114,12 +101,11 @@ ColumnLayout { onClicked: { let map = fullScreenMap.createObject(parent, { - latitude: root.latitude, - longitude: root.longitude, - asset: root.asset, + latitude: root.componentAttributes.latitude, + longitude: root.componentAttributes.longitude, + asset: root.componentAttributes.asset, author: root.author }); - map.open(); } ToolTip.text: text diff --git a/src/messagecontent/ReplyComponent.qml b/src/messagecontent/ReplyComponent.qml index 5ded94f6c..894f5cff6 100644 --- a/src/messagecontent/ReplyComponent.qml +++ b/src/messagecontent/ReplyComponent.qml @@ -24,20 +24,6 @@ import org.kde.neochat RowLayout { id: root - /** - * @brief The matrix ID of the reply event. - */ - required property var replyEventId - - /** - * @brief The reply author. - * - * A Quotient::RoomMember object. - * - * @sa Quotient::RoomMember - */ - required property var replyAuthor - /** * @brief The model to visualise the content of the message replied to. */ @@ -52,7 +38,7 @@ RowLayout { Layout.fillHeight: true implicitWidth: Kirigami.Units.smallSpacing - color: root.replyAuthor.color + color: root.replyContentModel.author.color radius: Kirigami.Units.cornerRadius } ColumnLayout { @@ -65,7 +51,7 @@ RowLayout { id: contentRepeater model: root.replyContentModel delegate: ReplyMessageComponentChooser { - onReplyClicked: RoomManager.goToEvent(root.replyEventId) + onReplyClicked: RoomManager.goToEvent(root.replyContentModel.eventId) } } } @@ -74,7 +60,7 @@ RowLayout { } TapHandler { acceptedButtons: Qt.LeftButton - onTapped: RoomManager.goToEvent(root.replyEventId) + onTapped: RoomManager.goToEvent(root.replyContentModel.eventId) } QtObject { id: _private diff --git a/src/messagecontent/ReplyMessageComponentChooser.qml b/src/messagecontent/ReplyMessageComponentChooser.qml index be1020419..0b1874d6f 100644 --- a/src/messagecontent/ReplyMessageComponentChooser.qml +++ b/src/messagecontent/ReplyMessageComponentChooser.qml @@ -49,12 +49,12 @@ DelegateChooser { DelegateChoice { roleValue: MessageComponentType.Video delegate: MimeComponent { - required property var mediaInfo + required property var componentAttributes - mimeIconSource: mediaInfo.mimeIcon - size: mediaInfo.size - duration: mediaInfo.duration - label: mediaInfo.filename + mimeIconSource: componentAttributes.mimeIcon + size: componentAttributes.size + duration: componentAttributes.duration + label: componentAttributes.filename } } @@ -88,12 +88,12 @@ DelegateChooser { roleValue: MessageComponentType.Audio delegate: MimeComponent { required property string display - required property var mediaInfo + required property var componentAttributes - mimeIconSource: mediaInfo.mimeIcon - size: mediaInfo.size - duration: mediaInfo.duration - label: mediaInfo.filename + mimeIconSource: componentAttributes.mimeIcon + size: componentAttributes.size + duration: componentAttributes.duration + label: componentAttributes.filename } } @@ -101,11 +101,11 @@ DelegateChooser { roleValue: MessageComponentType.File delegate: MimeComponent { required property string display - required property var mediaInfo + required property var componentAttributes - mimeIconSource: mediaInfo.mimeIcon - size: mediaInfo.size - label: mediaInfo.filename + mimeIconSource: componentAttributes.mimeIcon + size: componentAttributes.size + label: componentAttributes.filename } } diff --git a/src/messagecontent/VideoComponent.qml b/src/messagecontent/VideoComponent.qml index 3e5ff4434..7d6f39a58 100644 --- a/src/messagecontent/VideoComponent.qml +++ b/src/messagecontent/VideoComponent.qml @@ -30,18 +30,9 @@ Video { required property string display /** - * @brief The media info for the event. - * - * This should consist of the following: - * - source - The mxc URL for the media. - * - mimeType - The MIME type of the media (should be image/xxx for this delegate). - * - mimeIcon - The MIME icon name (should be image-xxx). - * - size - The file size in bytes. - * - width - The width in pixels of the audio media. - * - height - The height in pixels of the audio media. - * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads. + * @brief The attributes of the component. */ - required property var mediaInfo + required property var componentAttributes /** * @brief FileTransferInfo for any downloading files. @@ -206,7 +197,7 @@ Video { anchors.fill: parent visible: false - source: visible ? root.mediaInfo.tempInfo.source : "" + source: visible ? root.componentAttributes.tempInfo.source : "" fillMode: Image.PreserveAspectFit } @@ -437,8 +428,8 @@ Video { MediaSizeHelper { id: mediaSizeHelper contentMaxWidth: root.Message.maxContentWidth - mediaWidth: root.mediaInfo.width - mediaHeight: root.mediaInfo.height + mediaWidth: root.componentAttributes.width + mediaHeight: root.componentAttributes.height } function downloadAndPlay() { diff --git a/src/messagecontent/contentprovider.cpp b/src/messagecontent/contentprovider.cpp index 216d0543b..71631cced 100644 --- a/src/messagecontent/contentprovider.cpp +++ b/src/messagecontent/contentprovider.cpp @@ -14,14 +14,14 @@ ContentProvider &ContentProvider::self() return instance; } -MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply) +EventMessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply) { if (!room || evtOrTxnId.isEmpty()) { return nullptr; } if (!m_eventContentModels.contains(evtOrTxnId)) { - auto model = new MessageContentModel(room, evtOrTxnId, isReply); + auto model = new EventMessageContentModel(room, evtOrTxnId, isReply); QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership); m_eventContentModels.insert(evtOrTxnId, model); } @@ -29,7 +29,7 @@ MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, co return m_eventContentModels.object(evtOrTxnId); } -MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply) +EventMessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply) { if (!room) { return nullptr; @@ -53,7 +53,7 @@ MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, co auto eventId = event->id(); const auto txnId = event->transactionId(); if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) { - auto model = new MessageContentModel(room, eventId.isEmpty() ? txnId : eventId, isReply, eventId.isEmpty()); + auto model = new EventMessageContentModel(room, eventId.isEmpty() ? txnId : eventId, isReply, eventId.isEmpty()); QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership); m_eventContentModels.insert(eventId.isEmpty() ? txnId : eventId, model); } @@ -115,7 +115,7 @@ PollHandler *ContentProvider::handlerForPoll(NeoChatRoom *room, const QString &e void ContentProvider::setThreadsEnabled(bool enableThreads) { - MessageContentModel::setThreadsEnabled(enableThreads); + EventMessageContentModel::setThreadsEnabled(enableThreads); for (const auto &key : m_eventContentModels.keys()) { m_eventContentModels.object(key)->threadsEnabledChanged(); diff --git a/src/messagecontent/contentprovider.h b/src/messagecontent/contentprovider.h index bbf02224e..012641979 100644 --- a/src/messagecontent/contentprovider.h +++ b/src/messagecontent/contentprovider.h @@ -7,8 +7,8 @@ #include #include -#include "models/messagecontentmodel.h" #include "models/threadmodel.h" +#include "models/eventmessagecontentmodel.h" #include "neochatroom.h" #include "pollhandler.h" @@ -46,7 +46,7 @@ public: * * @warning Do NOT use for pending events as this function has no way to differentiate. */ - Q_INVOKABLE MessageContentModel *contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply = false); + Q_INVOKABLE EventMessageContentModel *contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply = false); /** * @brief Returns the content model for the given event. @@ -61,7 +61,7 @@ public: * * @note This version must be used for pending events as it can differentiate. */ - MessageContentModel *contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false); + EventMessageContentModel *contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false); /** * @brief Returns the thread model for the given thread root event ID. @@ -86,7 +86,7 @@ public: private: explicit ContentProvider(QObject *parent = nullptr); - QCache m_eventContentModels; + QCache m_eventContentModels; QCache m_threadModels; QCache m_pollHandlers; }; diff --git a/src/messagecontent/models/eventmessagecontentmodel.cpp b/src/messagecontent/models/eventmessagecontentmodel.cpp new file mode 100644 index 000000000..b0dc8d791 --- /dev/null +++ b/src/messagecontent/models/eventmessagecontentmodel.cpp @@ -0,0 +1,525 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "eventmessagecontentmodel.h" + +#include +#include +#include +#include +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) +#include +#endif + +#include +#include + +#include "chatbarcache.h" +#include "contentprovider.h" +#include "eventhandler.h" +#include "models/reactionmodel.h" +#include "neochatroom.h" +#include "texthandler.h" + +using namespace Quotient; + +bool EventMessageContentModel::m_threadsEnabled = false; + +EventMessageContentModel::EventMessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent) + : MessageContentModel(room, parent, eventId) + , m_currentState(isPending ? Pending : Unknown) + , m_isReply(isReply) +{ + initializeModel(); +} + +void EventMessageContentModel::initializeModel() +{ + Q_ASSERT(m_room != nullptr); + Q_ASSERT(!m_eventId.isEmpty()); + + connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() { + if (m_room != nullptr && m_currentState == Unknown) { + initializeEvent(); + resetModel(); + } + }); + connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) { + if (m_room != nullptr) { + if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) { + m_eventId = serverEvent->id(); + } + } + }); + connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() { + if (m_room != nullptr && m_currentState == Pending) { + initializeEvent(); + resetModel(); + } + }); + connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) { + if (!m_room) { + return; + } + for (int i = fromIndex; i <= toIndex; i++) { + if (m_room->findInTimeline(i)->event()->id() == m_eventId) { + initializeEvent(); + resetModel(); + } + } + }); + connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) { + if (m_room != nullptr) { + if (m_eventId == newEvent->id()) { + initializeEvent(); + resetContent(); + } + } + }); + connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) { + if (oldEventId == m_eventId || newEventId == m_eventId) { + resetContent(newEventId == m_eventId); + } + }); + connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) { + if (oldThreadId == m_eventId || newThreadId == m_eventId) { + resetContent(false, newThreadId == m_eventId); + } + }); + connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() { + resetContent(); + }); + connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) { + if (m_room != nullptr) { + if (authorId().isEmpty() || authorId() == member.id()) { + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole}); + Q_EMIT authorChanged(); + } + } + }); + connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) { + if (m_room != nullptr) { + if (authorId().isEmpty() || authorId() == member.id()) { + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole}); + Q_EMIT authorChanged(); + } + } + }); + connect(this, &EventMessageContentModel::threadsEnabledChanged, this, [this]() { + resetModel(); + }); + connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) { + if (eventId == m_eventId) { + updateReactionModel(); + } + }); + + initializeEvent(); + resetModel(); +} + +QDateTime EventMessageContentModel::time() const +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + return MessageContentModel::time(); + }; + return EventHandler::time(m_room, event.first, m_currentState == Pending); +} + +QString EventMessageContentModel::timeString() const +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + return MessageContentModel::timeString(); + }; + return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending); +} + +QString EventMessageContentModel::authorId() const +{ + const auto eventResult = m_room->getEvent(m_eventId); + if (eventResult.first == nullptr) { + return {}; + } + auto authorId = eventResult.first->senderId(); + if (authorId.isEmpty()) { + return MessageContentModel::authorId(); + } + return authorId; +} + +QString EventMessageContentModel::threadRootId() const +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + return {}; + } + auto roomMessageEvent = eventCast(event.first); +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) + if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) { +#else + if (roomMessageEvent && roomMessageEvent->isThreaded()) { +#endif + return roomMessageEvent->threadRootEventId(); + } + return {}; +} + +void EventMessageContentModel::initializeEvent() +{ + if (m_currentState == UnAvailable) { + return; + } + + const auto eventResult = m_room->getEvent(m_eventId); + if (eventResult.first == nullptr) { + if (m_currentState != Pending) { + getEvent(); + } + return; + } + if (eventResult.second) { + m_currentState = Pending; + } else { + m_currentState = Available; + } + Q_EMIT eventUpdated(); +} + +void EventMessageContentModel::getEvent() +{ + Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) { + if (m_room != nullptr) { + if (eventId == m_eventId) { + initializeEvent(); + resetModel(); + return true; + } + } + return false; + }); + Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) { + if (m_room != nullptr) { + if (eventId == m_eventId) { + m_currentState = UnAvailable; + resetModel(); + return true; + } + } + return false; + }); + + m_room->downloadEventFromServer(m_eventId); +} + +MessageComponent EventMessageContentModel::unavailableMessageComponent() const +{ + const auto theme = static_cast(qmlAttachedPropertiesObject(this, true)); + + QString disabledTextColor; + if (theme != nullptr) { + disabledTextColor = theme->disabledTextColor().name(); + } else { + disabledTextColor = u"#000000"_s; + } + + return MessageComponent{ + .type = MessageComponentType::Text, + .display = u""_s.arg(disabledTextColor) + + i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user") + u""_s, + .attributes = {}, + }; +} + +void EventMessageContentModel::resetModel() +{ + beginResetModel(); + m_components.clear(); + + if (m_room->connection()->isIgnored(authorId()) || m_currentState == UnAvailable) { + m_components += unavailableMessageComponent(); + endResetModel(); + return; + } + + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + m_components += MessageComponent{MessageComponentType::Loading, m_isReply ? i18n("Loading reply") : i18n("Loading"), {}}; + endResetModel(); + return; + } + + m_components += MessageComponent{MessageComponentType::Author, + QString(), + { + {u"time"_s, EventHandler::time(m_room, event.first, m_currentState == Pending)}, + {u"timeString"_s, EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending)}, + }}; + + m_components += messageContentComponents(); + endResetModel(); + + updateReplyModel(); + updateReactionModel(); + updateItineraryModel(); + + Q_EMIT componentsUpdated(); +} + +void EventMessageContentModel::resetContent(bool isEditing, bool isThreading) +{ + const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0; + beginRemoveRows({}, startRow, rowCount() - 1); + m_components.remove(startRow, rowCount() - startRow); + endRemoveRows(); + + const auto newComponents = messageContentComponents(isEditing, isThreading); + if (newComponents.size() == 0) { + return; + } + beginInsertRows({}, startRow, startRow + newComponents.size() - 1); + m_components += newComponents; + endInsertRows(); + + updateReplyModel(); + updateReactionModel(); + updateItineraryModel(); + + Q_EMIT componentsUpdated(); +} + +QList EventMessageContentModel::messageContentComponents(bool isEditing, bool isThreading) +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + return {}; + } + + QList newComponents; + + if (isEditing) { + newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; + } else { + newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first, m_isReply))); + } + + const auto roomMessageEvent = eventCast(event.first); +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) + if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id())) + && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) { +#else + if (m_threadsEnabled && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) { +#endif + newComponents += MessageComponent{MessageComponentType::Separator, {}, {}}; + newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}}; + } + + // If the event is already threaded the ThreadModel will handle displaying a chat bar. +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) + if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) { +#else + if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) { +#endif + newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; + } + + return newComponents; +} + +void EventMessageContentModel::updateReplyModel() +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr || m_isReply) { + return; + } + + const auto roomMessageEvent = eventCast(event.first); + if (roomMessageEvent == nullptr) { + return; + } + if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) { + if (m_replyModel) { + m_replyModel->disconnect(this); + m_replyModel->deleteLater(); + } + return; + } + + m_replyModel = new EventMessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this); + + bool hasModel = hasComponentType(MessageComponentType::Reply); + if (m_replyModel && !hasModel) { + int insertRow = 0; + if (m_components.first().type == MessageComponentType::Author) { + insertRow = 1; + } + beginInsertRows({}, insertRow, insertRow); + m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}}); + } else if (!m_replyModel && hasModel) { + int removeRow = 0; + if (m_components.first().type == MessageComponentType::Author) { + removeRow = 1; + } + beginRemoveRows({}, removeRow, removeRow); + m_components.removeAt(removeRow); + endRemoveRows(); + } +} + +QList EventMessageContentModel::componentsForType(MessageComponentType::Type type) +{ + const auto event = m_room->getEvent(m_eventId); + if (event.first == nullptr) { + return {}; + } + + switch (type) { + case MessageComponentType::Verification: { + return {MessageComponent{MessageComponentType::Verification, QString(), {}}}; + } + case MessageComponentType::Text: { + if (const auto roomMessageEvent = eventCast(event.first)) { + return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent), + EventHandler::messageBodyInputFormat(*roomMessageEvent), + m_room, + roomMessageEvent, + roomMessageEvent->isReplaced()); + } else { + return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false); + } + + const auto roomMessageEvent = eventCast(event.first); + return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent), + EventHandler::messageBodyInputFormat(*roomMessageEvent), + m_room, + roomMessageEvent, + roomMessageEvent->isReplaced()); + } + case MessageComponentType::File: { + QList components; + components += MessageComponent{MessageComponentType::File, {}, EventHandler::mediaInfo(m_room, event.first)}; + const auto roomMessageEvent = eventCast(event.first); + auto body = EventHandler::rawMessageBody(*roomMessageEvent); + if (!body.isEmpty()) { + components += TextHandler().textComponents(body, + EventHandler::messageBodyInputFormat(*roomMessageEvent), + m_room, + roomMessageEvent, + roomMessageEvent->isReplaced()); + } + return components; + } + case MessageComponentType::Image: + case MessageComponentType::Audio: + case MessageComponentType::Video: { + QList components = { + MessageComponent{type, EventHandler::richBody(m_room, event.first), EventHandler::mediaInfo(m_room, event.first)}}; + + if (!event.first->is()) { + const auto roomMessageEvent = eventCast(event.first); + const auto fileContent = roomMessageEvent->get(); + if (fileContent != nullptr) { + const auto fileInfo = fileContent->commonInfo(); + const auto body = EventHandler::rawMessageBody(*roomMessageEvent); + // Do not attach the description to the image, if it's the same as the original filename. + if (fileInfo.originalName != body) { + components += TextHandler().textComponents(body, + EventHandler::messageBodyInputFormat(*roomMessageEvent), + m_room, + roomMessageEvent, + roomMessageEvent->isReplaced()); + } + } + } + return components; + } + case MessageComponentType::Location: + return {MessageComponent{type, + QString(), + { + {u"latitude"_s, EventHandler::latitude(event.first)}, + {u"longitude"_s, EventHandler::longitude(event.first)}, + {u"asset"_s, EventHandler::locationAssetType(event.first)}, + }}}; + default: + return {MessageComponent{type, QString(), {}}}; + } +} + +void EventMessageContentModel::updateItineraryModel() +{ + if (!hasComponentType(MessageComponentType::File) || !m_room) { + return; + } + + const auto roomMessageEvent = eventCast(m_room->getEvent(m_eventId).first); + if (!roomMessageEvent || !roomMessageEvent->has()) { + return; + } + + auto filePath = m_room->cachedFileTransferInfo(roomMessageEvent).localPath; + if (filePath.isEmpty() && m_itineraryModel != nullptr) { + delete m_itineraryModel; + m_itineraryModel = nullptr; + } else if (!filePath.isEmpty()) { + if (m_itineraryModel == nullptr) { + m_itineraryModel = new ItineraryModel(this); + connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() { + if (m_itineraryModel->rowCount() == 0) { + m_emptyItinerary = true; + m_itineraryModel->deleteLater(); + m_itineraryModel = nullptr; + } + Q_EMIT itineraryUpdated(); + }); + connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() { + m_emptyItinerary = true; + m_itineraryModel->deleteLater(); + m_itineraryModel = nullptr; + Q_EMIT itineraryUpdated(); + }); + } + m_itineraryModel->setPath(filePath.toString()); + } +} + +void EventMessageContentModel::updateReactionModel() +{ + if (m_reactionModel && m_reactionModel->rowCount() > 0) { + return; + } + + if (m_reactionModel == nullptr) { + m_reactionModel = new ReactionModel(this, m_eventId, m_room); + connect(m_reactionModel, &ReactionModel::reactionsUpdated, this, &EventMessageContentModel::updateReactionModel); + } + + if (m_reactionModel->rowCount() <= 0) { + m_reactionModel->disconnect(this); + m_reactionModel->deleteLater(); + m_reactionModel = nullptr; + } + + if (m_reactionModel && m_components.last().type != MessageComponentType::Reaction) { + beginInsertRows({}, rowCount(), rowCount()); + m_components += MessageComponent{MessageComponentType::Reaction, QString(), {}}; + endInsertRows(); + } else if (rowCount() > 0 && m_components.last().type == MessageComponentType::Reaction) { + beginRemoveRows({}, rowCount() - 1, rowCount() - 1); + m_components.removeLast(); + endRemoveRows(); + } +} + +ThreadModel *EventMessageContentModel::modelForThread(const QString &threadRootId) +{ + return ContentProvider::self().modelForThread(m_room, threadRootId); +} + +void EventMessageContentModel::setThreadsEnabled(bool enableThreads) +{ + m_threadsEnabled = enableThreads; +} + +#include "moc_eventmessagecontentmodel.cpp" diff --git a/src/messagecontent/models/eventmessagecontentmodel.h b/src/messagecontent/models/eventmessagecontentmodel.h new file mode 100644 index 000000000..666e41a6c --- /dev/null +++ b/src/messagecontent/models/eventmessagecontentmodel.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#include "models/messagecontentmodel.h" +#include "models/threadmodel.h" + +/** + * @class EventMessageContentModel + * + * Inherited from MessageContentModel this visulaises the content of a Quotient::RoomMessageEvent. + */ +class EventMessageContentModel : public MessageContentModel +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + enum MessageState { + Unknown, /**< The message state is unknown. */ + Pending, /**< The message is a new pending message which the server has not yet acknowledged. */ + Available, /**< The message is available and acknowledged by the server. */ + UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */ + }; + Q_ENUM(MessageState) + + explicit EventMessageContentModel(NeoChatRoom *room, + const QString &eventId, + bool isReply = false, + bool isPending = false, + MessageContentModel *parent = nullptr); + + /** + * @brief Returns the thread model for the given thread root event ID. + * + * A model is created if one doesn't exist. Will return nullptr if threadRootId + * is empty. + */ + Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId); + + static void setThreadsEnabled(bool enableThreads); + +Q_SIGNALS: + void eventUpdated(); + void threadsEnabledChanged(); + +private: + void initializeModel(); + + QDateTime time() const override; + QString timeString() const override; + QString authorId() const override; + QString threadRootId() const override; + + MessageState m_currentState = Unknown; + bool m_isReply; + + void initializeEvent(); + void getEvent(); + + MessageComponent unavailableMessageComponent() const; + void resetModel(); + void resetContent(bool isEditing = false, bool isThreading = false); + QList messageContentComponents(bool isEditing = false, bool isThreading = false); + + void updateReplyModel(); + + QList componentsForType(MessageComponentType::Type type); + + void updateItineraryModel(); + + void updateReactionModel(); + + static bool m_threadsEnabled; +}; diff --git a/src/messagecontent/models/messagecontentmodel.cpp b/src/messagecontent/models/messagecontentmodel.cpp index b49a512a7..47462ee87 100644 --- a/src/messagecontent/models/messagecontentmodel.cpp +++ b/src/messagecontent/models/messagecontentmodel.cpp @@ -2,48 +2,20 @@ // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "messagecontentmodel.h" -#include "contentprovider.h" -#include "enums/messagecomponenttype.h" -#include "eventhandler.h" -#include "messagecomponent.h" - -#include - -#include -#include -#include -#include -#include -#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) -#include -#endif #include -#include - -#ifndef Q_OS_ANDROID -#include -#include -#endif #include "chatbarcache.h" #include "contentprovider.h" -#include "filetype.h" -#include "models/reactionmodel.h" +#include "enums/messagecomponenttype.h" #include "neochatconnection.h" -#include "neochatroom.h" -#include "texthandler.h" using namespace Quotient; -bool MessageContentModel::m_threadsEnabled = false; - -MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent) +MessageContentModel::MessageContentModel(NeoChatRoom *room, MessageContentModel *parent, const QString &eventId) : QAbstractListModel(parent) , m_room(room) , m_eventId(eventId) - , m_currentState(isPending ? Pending : Unknown) - , m_isReply(isReply) { initializeModel(); } @@ -51,43 +23,18 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event void MessageContentModel::initializeModel() { Q_ASSERT(m_room != nullptr); - Q_ASSERT(!m_eventId.isEmpty()); - connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() { - if (m_room != nullptr && m_currentState == Unknown) { - initializeEvent(); - resetModel(); + connect(this, &MessageContentModel::componentsUpdated, this, [this]() { + if (m_room->urlPreviewEnabled()) { + forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewAddFunction); + } else { + forEachComponentOfType({MessageComponentType::LinkPreview, MessageComponentType::LinkPreviewLoad}, m_linkPreviewRemoveFunction); } + m_components.squeeze(); }); - connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) { - if (m_room != nullptr) { - if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) { - m_eventId = serverEvent->id(); - } - } - }); - connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() { - if (m_room != nullptr && m_currentState == Pending) { - initializeEvent(); - resetModel(); - } - }); - connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) { - if (m_room != nullptr) { - for (int i = fromIndex; i <= toIndex; i++) { - if (m_room->findInTimeline(i)->event()->id() == m_eventId) { - initializeEvent(); - resetModel(); - } - } - } - }); - connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) { - if (m_room != nullptr) { - if (m_eventId == newEvent->id()) { - initializeEvent(); - resetContent(); - } + connect(this, &MessageContentModel::itineraryUpdated, this, [this]() { + if (hasComponentType(MessageComponentType::File)) { + forEachComponentOfType(MessageComponentType::File, m_fileFunction); } }); connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) { @@ -120,117 +67,38 @@ void MessageContentModel::initializeModel() } } }); - connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) { - if (oldEventId == m_eventId || newEventId == m_eventId) { - resetContent(newEventId == m_eventId); - } - }); - connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) { - if (oldThreadId == m_eventId || newThreadId == m_eventId) { - resetContent(false, newThreadId == m_eventId); - } - }); - connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() { - resetContent(); - }); - connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) { - if (m_room != nullptr) { - if (senderId().isEmpty() || senderId() == member.id()) { - Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole}); - } - } - }); - connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) { - if (m_room != nullptr) { - if (senderId().isEmpty() || senderId() == member.id()) { - Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole}); - } - } - }); - - connect(this, &MessageContentModel::threadsEnabledChanged, this, [this]() { - resetModel(); - }); - connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) { - if (eventId == m_eventId) { - updateReactionModel(); - } - }); - - initializeEvent(); - resetModel(); + connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated); } -void MessageContentModel::initializeEvent() +QString MessageContentModel::eventId() const { - if (m_currentState == UnAvailable) { - return; - } - - const auto eventResult = m_room->getEvent(m_eventId); - if (eventResult.first == nullptr) { - if (m_currentState != Pending) { - getEvent(); - } - return; - } - if (eventResult.second) { - m_currentState = Pending; - } else { - m_currentState = Available; - } - Q_EMIT eventUpdated(); + return m_eventId; } -void MessageContentModel::getEvent() +QDateTime MessageContentModel::time() const { - Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) { - if (m_room != nullptr) { - if (eventId == m_eventId) { - initializeEvent(); - resetModel(); - return true; - } - } - return false; - }); - Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) { - if (m_room != nullptr) { - if (eventId == m_eventId) { - m_currentState = UnAvailable; - resetModel(); - return true; - } - } - return false; - }); - - m_room->downloadEventFromServer(m_eventId); + return QDateTime::currentDateTime(); } -QString MessageContentModel::senderId() const +QString MessageContentModel::timeString() const { - const auto eventResult = m_room->getEvent(m_eventId); - if (eventResult.first == nullptr) { - return {}; - } - auto senderId = eventResult.first->senderId(); - if (senderId.isEmpty()) { - senderId = m_room->localMember().id(); - } - return senderId; + return time().toLocalTime().toString(u"hh:mm"_s); + ; } -NeochatRoomMember *MessageContentModel::senderObject() const +QString MessageContentModel::authorId() const { - const auto eventResult = m_room->getEvent(m_eventId); - if (eventResult.first == nullptr) { - return nullptr; - } - if (eventResult.first->senderId().isEmpty()) { - return m_room->qmlSafeMember(m_room->localMember().id()); - } - return m_room->qmlSafeMember(eventResult.first->senderId()); + return m_room->localMember().id(); +} + +NeochatRoomMember *MessageContentModel::author() const +{ + return m_room->qmlSafeMember(authorId()); +} + +QString MessageContentModel::threadRootId() const +{ + return {}; } static LinkPreviewer *emptyLinkPreview = new LinkPreviewer; @@ -248,47 +116,8 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const const auto component = m_components[index.row()]; - const auto event = m_room->getEvent(m_eventId); - if (event.first == nullptr) { - if (role == DisplayRole) { - if (m_isReply) { - return i18n("Loading reply"); - } else { - return i18n("Loading"); - } - } - if (role == ComponentTypeRole) { - return component.type; - } - return {}; - } - if (role == DisplayRole) { - if (m_currentState == UnAvailable || m_room->connection()->isIgnored(senderId())) { - Kirigami::Platform::PlatformTheme *theme = - static_cast(qmlAttachedPropertiesObject(this, true)); - - QString disabledTextColor; - if (theme != nullptr) { - disabledTextColor = theme->disabledTextColor().name(); - } else { - disabledTextColor = u"#000000"_s; - } - return QString(u""_s.arg(disabledTextColor) - + i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user") - + u""_s); - } - if (component.type == MessageComponentType::Loading) { - if (m_isReply) { - return i18n("Loading reply"); - } else { - return i18n("Loading"); - } - } - if (!component.content.isEmpty()) { - return component.content; - } - return EventHandler::richBody(m_room, event.first); + return component.display; } if (role == ComponentTypeRole) { return component.type; @@ -297,63 +126,34 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const return component.attributes; } if (role == EventIdRole) { - return event.first->displayId(); + return eventId(); } if (role == TimeRole) { - return EventHandler::time(m_room, event.first, m_currentState == Pending); + return time(); } if (role == TimeStringRole) { - return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending); + return timeString(); } if (role == AuthorRole) { - return QVariant::fromValue(senderObject()); - } - if (role == MediaInfoRole) { - return EventHandler::mediaInfo(m_room, event.first); + return QVariant::fromValue(author()); } if (role == FileTransferInfoRole) { - return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first)); + return QVariant::fromValue(m_room->cachedFileTransferInfo(m_eventId)); } if (role == ItineraryModelRole) { return QVariant::fromValue(m_itineraryModel); } - if (role == LatitudeRole) { - return EventHandler::latitude(event.first); - } - if (role == LongitudeRole) { - return EventHandler::longitude(event.first); - } - if (role == AssetRole) { - return EventHandler::locationAssetType(event.first); - } if (role == PollHandlerRole) { return QVariant::fromValue(ContentProvider::self().handlerForPoll(m_room, m_eventId)); } - if (role == ReplyEventIdRole) { - if (const auto roomMessageEvent = eventCast(event.first)) { - return roomMessageEvent->replyEventId(); - } - } - if (role == ReplyAuthorRole) { - return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first)); - } if (role == ReplyContentModelRole) { return QVariant::fromValue(m_replyModel); } if (role == ReactionModelRole) { return QVariant::fromValue(m_reactionModel); - ; } if (role == ThreadRootRole) { - auto roomMessageEvent = eventCast(event.first); -#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) - if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) { -#else - if (roomMessageEvent && roomMessageEvent->isThreaded()) { -#endif - return roomMessageEvent->threadRootEventId(); - } - return {}; + return threadRootId(); } if (role == LinkPreviewerRole) { if (component.type == MessageComponentType::LinkPreview) { @@ -394,15 +194,9 @@ QHash MessageContentModel::roleNamesStatic() roles[MessageContentModel::TimeRole] = "time"; roles[MessageContentModel::TimeStringRole] = "timeString"; roles[MessageContentModel::AuthorRole] = "author"; - roles[MessageContentModel::MediaInfoRole] = "mediaInfo"; roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo"; roles[MessageContentModel::ItineraryModelRole] = "itineraryModel"; - roles[MessageContentModel::LatitudeRole] = "latitude"; - roles[MessageContentModel::LongitudeRole] = "longitude"; - roles[MessageContentModel::AssetRole] = "asset"; roles[MessageContentModel::PollHandlerRole] = "pollHandler"; - roles[MessageContentModel::ReplyEventIdRole] = "replyEventId"; - roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor"; roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel"; roles[MessageContentModel::ReactionModelRole] = "reactionModel"; roles[MessageContentModel::ThreadRootRole] = "threadRoot"; @@ -443,256 +237,6 @@ void MessageContentModel::forEachComponentOfType(QListconnection()->isIgnored(senderId()) || m_currentState == UnAvailable) { - m_components += MessageComponent{MessageComponentType::Text, QString(), {}}; - endResetModel(); - return; - } - - const auto event = m_room->getEvent(m_eventId); - if (event.first == nullptr) { - m_components += MessageComponent{MessageComponentType::Loading, QString(), {}}; - endResetModel(); - return; - } - - m_components += MessageComponent{MessageComponentType::Author, QString(), {}}; - - m_components += messageContentComponents(); - endResetModel(); - - if (m_room->urlPreviewEnabled()) { - forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction); - } - - updateReplyModel(); - updateReactionModel(); -} - -void MessageContentModel::resetContent(bool isEditing, bool isThreading) -{ - const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0; - beginRemoveRows({}, startRow, rowCount() - 1); - m_components.remove(startRow, rowCount() - startRow); - endRemoveRows(); - - const auto newComponents = messageContentComponents(isEditing, isThreading); - if (newComponents.size() == 0) { - return; - } - beginInsertRows({}, startRow, startRow + newComponents.size() - 1); - m_components += newComponents; - endInsertRows(); - - if (m_room->urlPreviewEnabled()) { - forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewFunction); - } - - updateReplyModel(); - updateReactionModel(); -} - -QList MessageContentModel::messageContentComponents(bool isEditing, bool isThreading) -{ - const auto event = m_room->getEvent(m_eventId); - if (event.first == nullptr) { - return {}; - } - - QList newComponents; - - if (isEditing) { - newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; - } else { - newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first, m_isReply))); - } - - const auto roomMessageEvent = eventCast(event.first); -#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) - if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id())) - && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) { -#else - if (m_threadsEnabled && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) { -#endif - newComponents += MessageComponent{MessageComponentType::Separator, {}, {}}; - newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}}; - } - - // If the event is already threaded the ThreadModel will handle displaying a chat bar. -#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) - if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) { -#else - if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) { -#endif - newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; - } - - return newComponents; -} - -void MessageContentModel::updateReplyModel() -{ - const auto event = m_room->getEvent(m_eventId); - if (event.first == nullptr || m_isReply) { - return; - } - - const auto roomMessageEvent = eventCast(event.first); - if (roomMessageEvent == nullptr) { - return; - } - if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) { - if (m_replyModel) { - m_replyModel->disconnect(this); - m_replyModel->deleteLater(); - } - return; - } - - if (m_replyModel != nullptr) { - return; - } - - m_replyModel = new MessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this); - - connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() { - Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole}); - }); - - bool hasModel = hasComponentType(MessageComponentType::Reply); - if (m_replyModel && !hasModel) { - int insertRow = 0; - if (m_components.first().type == MessageComponentType::Author) { - insertRow = 1; - } - beginInsertRows({}, insertRow, insertRow); - m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}}); - } else if (!m_replyModel && hasModel) { - int removeRow = 0; - if (m_components.first().type == MessageComponentType::Author) { - removeRow = 1; - } - beginRemoveRows({}, removeRow, removeRow); - m_components.removeAt(removeRow); - endRemoveRows(); - } -} - -QList MessageContentModel::componentsForType(MessageComponentType::Type type) -{ - const auto event = m_room->getEvent(m_eventId); - if (event.first == nullptr) { - return {}; - } - - switch (type) { - case MessageComponentType::Verification: { - return {MessageComponent{MessageComponentType::Verification, QString(), {}}}; - } - case MessageComponentType::Text: { - if (const auto roomMessageEvent = eventCast(event.first)) { - return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent), - EventHandler::messageBodyInputFormat(*roomMessageEvent), - m_room, - roomMessageEvent, - roomMessageEvent->isReplaced()); - } else { - return TextHandler().textComponents(EventHandler::plainBody(m_room, event.first), Qt::TextFormat::PlainText, m_room, event.first, false); - } - - const auto roomMessageEvent = eventCast(event.first); - return TextHandler().textComponents(EventHandler::rawMessageBody(*roomMessageEvent), - EventHandler::messageBodyInputFormat(*roomMessageEvent), - m_room, - roomMessageEvent, - roomMessageEvent->isReplaced()); - } - case MessageComponentType::File: { - QList components; - components += MessageComponent{MessageComponentType::File, QString(), {}}; - const auto roomMessageEvent = eventCast(event.first); - - if (m_emptyItinerary) { - if (!m_isReply) { - auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first); - -#ifndef Q_OS_ANDROID - Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has()); - const QMimeType mimeType = roomMessageEvent->get()->mimeType; - if (mimeType.name() == u"text/plain"_s || mimeType.parentMimeTypes().contains(u"text/plain"_s)) { - QString originalName = roomMessageEvent->get()->originalName; - if (originalName.isEmpty()) { - originalName = roomMessageEvent->plainBody(); - } - KSyntaxHighlighting::Repository repository; - KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName); - if (!definitionForFile.isValid()) { - definitionForFile = repository.definitionForMimeType(mimeType.name()); - } - - QFile file(fileTransferInfo.localPath.path()); - file.open(QIODevice::ReadOnly); - components += MessageComponent{MessageComponentType::Code, - QString::fromStdString(file.readAll().toStdString()), - {{u"class"_s, definitionForFile.name()}}}; - } -#endif - - if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) { - QImageReader reader(fileTransferInfo.localPath.path()); - components += MessageComponent{MessageComponentType::Pdf, QString(), {{u"size"_s, reader.size()}}}; - } - } - } else if (m_itineraryModel != nullptr) { - components += MessageComponent{MessageComponentType::Itinerary, QString(), {}}; - if (m_itineraryModel->rowCount() > 0) { - updateItineraryModel(); - } - } else { - updateItineraryModel(); - } - auto body = EventHandler::rawMessageBody(*roomMessageEvent); - components += TextHandler().textComponents(body, - EventHandler::messageBodyInputFormat(*roomMessageEvent), - m_room, - roomMessageEvent, - roomMessageEvent->isReplaced()); - return components; - } - case MessageComponentType::Image: - case MessageComponentType::Audio: - case MessageComponentType::Video: { - if (!event.first->is()) { - const auto roomMessageEvent = eventCast(event.first); - const auto fileContent = roomMessageEvent->get(); - if (fileContent != nullptr) { - const auto fileInfo = fileContent->commonInfo(); - const auto body = EventHandler::rawMessageBody(*roomMessageEvent); - // Do not attach the description to the image, if it's the same as the original filename. - if (fileInfo.originalName != body) { - QList components; - components += MessageComponent{type, QString(), {}}; - components += TextHandler().textComponents(body, - EventHandler::messageBodyInputFormat(*roomMessageEvent), - m_room, - roomMessageEvent, - roomMessageEvent->isReplaced()); - return components; - } - } - } - } - [[fallthrough]]; - default: - return {MessageComponent{type, QString(), {}}}; - } -} - MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link) { const auto linkPreviewer = dynamic_cast(m_room->connection())->previewerForLink(link); @@ -733,79 +277,4 @@ void MessageContentModel::closeLinkPreview(int row) } } -void MessageContentModel::updateItineraryModel() -{ - const auto event = m_room->getEvent(m_eventId); - if (m_room == nullptr || event.first == nullptr) { - return; - } - - if (auto roomMessageEvent = eventCast(event.first)) { - if (roomMessageEvent->has()) { - auto filePath = m_room->cachedFileTransferInfo(event.first).localPath; - if (filePath.isEmpty() && m_itineraryModel != nullptr) { - delete m_itineraryModel; - m_itineraryModel = nullptr; - } else if (!filePath.isEmpty()) { - if (m_itineraryModel == nullptr) { - m_itineraryModel = new ItineraryModel(this); - connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() { - if (m_itineraryModel->rowCount() == 0) { - m_emptyItinerary = true; - m_itineraryModel->deleteLater(); - m_itineraryModel = nullptr; - resetContent(); - } - }); - connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() { - m_emptyItinerary = true; - m_itineraryModel->deleteLater(); - m_itineraryModel = nullptr; - resetContent(); - }); - } - m_itineraryModel->setPath(filePath.toString()); - } - } - } -} - -void MessageContentModel::updateReactionModel() -{ - if (m_reactionModel && m_reactionModel->rowCount() > 0) { - return; - } - - if (m_reactionModel == nullptr) { - m_reactionModel = new ReactionModel(this, m_eventId, m_room); - connect(m_reactionModel, &ReactionModel::reactionsUpdated, this, &MessageContentModel::updateReactionModel); - } - - if (m_reactionModel->rowCount() <= 0) { - m_reactionModel->disconnect(this); - m_reactionModel->deleteLater(); - m_reactionModel = nullptr; - } - - if (m_reactionModel && m_components.last().type != MessageComponentType::Reaction) { - beginInsertRows({}, rowCount(), rowCount()); - m_components += MessageComponent{MessageComponentType::Reaction, QString(), {}}; - endInsertRows(); - } else if (rowCount() > 0 && m_components.last().type == MessageComponentType::Reaction) { - beginRemoveRows({}, rowCount() - 1, rowCount() - 1); - m_components.removeLast(); - endRemoveRows(); - } -} - -ThreadModel *MessageContentModel::modelForThread(const QString &threadRootId) -{ - return ContentProvider::self().modelForThread(m_room, threadRootId); -} - -void MessageContentModel::setThreadsEnabled(bool enableThreads) -{ - m_threadsEnabled = enableThreads; -} - #include "moc_messagecontentmodel.cpp" diff --git a/src/messagecontent/models/messagecontentmodel.h b/src/messagecontent/models/messagecontentmodel.h index 153d93a46..e2dc5f7e3 100644 --- a/src/messagecontent/models/messagecontentmodel.h +++ b/src/messagecontent/models/messagecontentmodel.h @@ -5,22 +5,29 @@ #include #include +#include -#include +#ifndef Q_OS_ANDROID +#include +#include +#endif #include "enums/messagecomponenttype.h" +#include "filetype.h" #include "linkpreviewer.h" #include "messagecomponent.h" #include "models/itinerarymodel.h" #include "models/reactionmodel.h" +#include "neochatroom.h" #include "neochatroommember.h" -class ThreadModel; - /** * @class MessageContentModel * - * A model to visualise the components of a single RoomMessageEvent. + * A model to visualise the content of a message. + * + * This is a base model designed to be extended. The inherited class needs to define + * how the MessageComponents are added. */ class MessageContentModel : public QAbstractListModel { @@ -28,15 +35,9 @@ class MessageContentModel : public QAbstractListModel QML_ELEMENT QML_UNCREATABLE("") -public: - enum MessageState { - Unknown, /**< The message state is unknown. */ - Pending, /**< The message is a new pending message which the server has not yet acknowledged. */ - Available, /**< The message is available and acknowledged by the server. */ - UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */ - }; - Q_ENUM(MessageState) + Q_PROPERTY(NeochatRoomMember *author READ author NOTIFY authorChanged) +public: /** * @brief Defines the model roles. */ @@ -48,32 +49,18 @@ public: TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */ TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */ AuthorRole, /**< The author of the event. */ - MediaInfoRole, /**< The media info for the event. */ FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */ ItineraryModelRole, /**< The itinerary model for a file. */ - LatitudeRole, /**< Latitude for a location event. */ - LongitudeRole, /**< Longitude for a location event. */ - AssetRole, /**< Type of location event, e.g. self pin of the user location. */ PollHandlerRole, /**< The PollHandler for the event, if any. */ - - ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */ - ReplyAuthorRole, /**< The author of the event that was replied to. */ ReplyContentModelRole, /**< The MessageContentModel for the reply event. */ - ReactionModelRole, /**< Reaction model for this event. */ - ThreadRootRole, /**< The thread root event ID for the event. */ - LinkPreviewerRole, /**< The link preview details. */ ChatBarCacheRole, /**< The ChatBarCache to use. */ }; Q_ENUM(Roles) - explicit MessageContentModel(NeoChatRoom *room, - const QString &eventId, - bool isReply = false, - bool isPending = false, - MessageContentModel *parent = nullptr); + explicit MessageContentModel(NeoChatRoom *room, MessageContentModel *parent = nullptr, const QString &eventId = {}); /** * @brief Get the given role value at the given index. @@ -95,9 +82,18 @@ public: * @sa Roles, QAbstractItemModel::roleNames() */ [[nodiscard]] QHash roleNames() const override; - static QHash roleNamesStatic(); + /** + * @brief The Matrix event ID of the message. + */ + Q_INVOKABLE QString eventId() const; + + /** + * @brief The author of the message. + */ + Q_INVOKABLE NeochatRoomMember *author() const; + /** * @brief Close the link preview at the given index. * @@ -105,34 +101,50 @@ public: */ Q_INVOKABLE void closeLinkPreview(int row); - /** - * @brief Returns the thread model for the given thread root event ID. - * - * A model is created is one doesn't exist. Will return nullptr if threadRootId - * is empty. - */ - Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId); - - static void setThreadsEnabled(bool enableThreads); - Q_SIGNALS: - void showAuthorChanged(); - void eventUpdated(); + void authorChanged(); - void threadsEnabledChanged(); + /** + * @brief Emit whenever new components are added. + */ + void componentsUpdated(); -private: + /** + * @brief Emit whenever itinerary model is updated. + */ + void itineraryUpdated(); + +protected: QPointer m_room; QString m_eventId; - QString senderId() const; - NeochatRoomMember *senderObject() const; - MessageState m_currentState = Unknown; - bool m_isReply; + /** + * @brief QDateTime for the message. + * + * The default implementation returns the current time. + */ + virtual QDateTime time() const; - void initializeModel(); - void initializeEvent(); - void getEvent(); + /** + * @brief Time for the message as a string in the from "hh:mm". + * + * The default implementation returns the current time. + */ + virtual QString timeString() const; + + /** + * @brief The author of the message. + * + * The default implementation returns the local user. + */ + virtual QString authorId() const; + + /** + * @brief Thread root ID for the message if in a thread. + * + * The default implementation returns an empty string. + */ + virtual QString threadRootId() const; using ComponentIt = QList::iterator; @@ -141,14 +153,61 @@ private: void forEachComponentOfType(MessageComponentType::Type type, std::function function); void forEachComponentOfType(QList types, std::function function); + QPointer m_replyModel; + QPointer m_reactionModel = nullptr; + QPointer m_itineraryModel = nullptr; + bool m_emptyItinerary = false; + +private: + void initializeModel(); + std::function m_fileInfoFunction = [this](ComponentIt it) { Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {MessageContentModel::FileTransferInfoRole}); return ++it; }; - std::function m_linkPreviewFunction = [this](ComponentIt it) { + std::function m_fileFunction = [this](ComponentIt it) { + if (m_itineraryModel && m_itineraryModel->rowCount() > 0) { + beginInsertRows({}, std::distance(m_components.begin(), it) + 1, std::distance(m_components.begin(), it) + 1); + it = m_components.insert(it + 1, MessageComponent{MessageComponentType::Itinerary, QString(), {}}); + endInsertRows(); + return it; + } else if (m_emptyItinerary) { + auto fileTransferInfo = m_room->cachedFileTransferInfo(m_eventId); +#ifndef Q_OS_ANDROID + const QMimeType mimeType = FileType::instance().mimeTypeForFile(fileTransferInfo.localPath.toString()); + if (mimeType.inherits(u"text/plain"_s)) { + KSyntaxHighlighting::Repository repository; + KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(fileTransferInfo.localPath.toString()); + if (!definitionForFile.isValid()) { + definitionForFile = repository.definitionForMimeType(mimeType.name()); + } + + QFile file(fileTransferInfo.localPath.path()); + file.open(QIODevice::ReadOnly); + beginInsertRows({}, std::distance(m_components.begin(), it) + 1, std::distance(m_components.begin(), it) + 1); + it = m_components.insert(it + 1, MessageComponent{MessageComponentType::Code, QString::fromStdString(file.readAll().toStdString()), {{u"class"_s, definitionForFile.name()}}}); + endInsertRows(); + return it; + } +#endif + + if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) { + QImageReader reader(fileTransferInfo.localPath.path()); + beginInsertRows({}, std::distance(m_components.begin(), it) + 1, std::distance(m_components.begin(), it) + 1); + it = m_components.insert(it + 1, MessageComponent{MessageComponentType::Pdf, QString(), {{u"size"_s, reader.size()}}}); + endInsertRows(); + } + } + return ++it; + }; + std::function m_linkPreviewAddFunction = [this](ComponentIt it) { + if (!m_room->urlPreviewEnabled()) { + return it; + } + bool previewAdded = false; - if (LinkPreviewer::hasPreviewableLinks(it->content)) { - const auto links = LinkPreviewer::linkPreviews(it->content); + if (LinkPreviewer::hasPreviewableLinks(it->display)) { + const auto links = LinkPreviewer::linkPreviews(it->display); for (qsizetype j = 0; j < links.size(); ++j) { const auto linkPreview = linkPreviewComponent(links[j]); if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) { @@ -161,26 +220,16 @@ private: } return previewAdded ? it : ++it; }; - - void resetModel(); - void resetContent(bool isEditing = false, bool isThreading = false); - QList messageContentComponents(bool isEditing = false, bool isThreading = false); - - QPointer m_replyModel; - void updateReplyModel(); - - ReactionModel *m_reactionModel = nullptr; - ItineraryModel *m_itineraryModel = nullptr; - - QList componentsForType(MessageComponentType::Type type); - MessageComponent linkPreviewComponent(const QUrl &link); + std::function m_linkPreviewRemoveFunction = [this](ComponentIt it) { + if (m_room->urlPreviewEnabled()) { + return it; + } + beginRemoveRows({}, std::distance(m_components.begin(), it), std::distance(m_components.begin(), it)); + it = m_components.erase(it); + endRemoveRows(); + return it; + }; QList m_removedLinkPreviews; - - void updateItineraryModel(); - bool m_emptyItinerary = false; - - void updateReactionModel(); - - static bool m_threadsEnabled; + MessageComponent linkPreviewComponent(const QUrl &link); }; diff --git a/src/timeline/models/messagemodel.cpp b/src/timeline/models/messagemodel.cpp index b80eea6bd..9ed037417 100644 --- a/src/timeline/models/messagemodel.cpp +++ b/src/timeline/models/messagemodel.cpp @@ -20,6 +20,7 @@ #include "eventhandler.h" #include "events/pollevent.h" #include "models/reactionmodel.h" +#include "models/eventmessagecontentmodel.h" #include "neochatroommember.h" using namespace Quotient; @@ -42,7 +43,7 @@ MessageModel::MessageModel(QObject *parent) }); connect(this, &MessageModel::threadsEnabledChanged, this, [this]() { - Q_EMIT dataChanged(index(0), index(rowCount() - 1), {IsThreadedRole}); + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {ContentModelRole, IsThreadedRole}); }); } @@ -142,14 +143,15 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const if (role == ContentModelRole) { if (event->get().is() || event->get().is() || event->get().is()) { - return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, event->get().id())); + return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, event->get().id())); } auto roomMessageEvent = eventCast(&event.value().get()); if (m_threadsEnabled && roomMessageEvent && roomMessageEvent->isThreaded()) { - return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, roomMessageEvent->threadRootEventId())); + return QVariant::fromValue( + ContentProvider::self().contentModelForEvent(m_room, roomMessageEvent->threadRootEventId())); } - return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, &event->get())); + return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, &event->get())); } if (role == GenericDisplayRole) {