diff --git a/autotests/reactionmodeltest.cpp b/autotests/reactionmodeltest.cpp index d0065a52e..caf38814d 100644 --- a/autotests/reactionmodeltest.cpp +++ b/autotests/reactionmodeltest.cpp @@ -9,6 +9,7 @@ #include +#include "models/messagecontentmodel.h" #include "testutils.h" using namespace Quotient; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7495016c2..e72b3a57c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,6 +198,8 @@ add_library(neochat STATIC models/commonroomsmodel.h models/pollanswermodel.cpp models/pollanswermodel.h + contentprovider.cpp + contentprovider.h ) set_source_files_properties(qml/TextToSpeechWrapper.qml PROPERTIES diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp index 2b227abaf..d3b8222d0 100644 --- a/src/chatbarcache.cpp +++ b/src/chatbarcache.cpp @@ -6,6 +6,7 @@ #include #include "chatdocumenthandler.h" +#include "contentprovider.h" #include "eventhandler.h" #include "models/actionsmodel.h" #include "neochatroom.h" @@ -170,17 +171,13 @@ MessageContentModel *ChatBarCache::relationEventContentModel() if (m_relationId.isEmpty()) { return nullptr; } - if (m_relationContentModel != nullptr) { - return m_relationContentModel; - } - auto room = dynamic_cast(parent()); if (room == nullptr) { qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."; return nullptr; } - m_relationContentModel = new MessageContentModel(room, m_relationId, true); - return m_relationContentModel; + + return ContentProvider::self().contentModelForEvent(room, m_relationId); } bool ChatBarCache::isThreaded() const diff --git a/src/contentprovider.cpp b/src/contentprovider.cpp new file mode 100644 index 000000000..785b96b72 --- /dev/null +++ b/src/contentprovider.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "contentprovider.h" + +ContentProvider::ContentProvider() +{ +} + +ContentProvider &ContentProvider::self() +{ + static ContentProvider instance; + return instance; +} + +MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply) +{ + if (!room || evtOrTxnId.isEmpty()) { + return nullptr; + } + + if (!m_eventContentModels.contains(evtOrTxnId)) { + m_eventContentModels.insert(evtOrTxnId, new MessageContentModel(room, evtOrTxnId, isReply)); + } + + return m_eventContentModels.object(evtOrTxnId); +} + +MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply) +{ + if (!room) { + return nullptr; + } + const auto roomMessageEvent = eventCast(event); + if (roomMessageEvent == nullptr) { + // If for some reason a model is there remove. + if (m_eventContentModels.contains(event->id())) { + m_eventContentModels.remove(event->id()); + } + if (m_eventContentModels.contains(event->transactionId())) { + m_eventContentModels.remove(event->transactionId()); + } + return nullptr; + } + + if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) { + return nullptr; + } + + auto eventId = event->id(); + const auto txnId = event->transactionId(); + if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) { + m_eventContentModels.insert(eventId.isEmpty() ? txnId : eventId, + new MessageContentModel(room, eventId.isEmpty() ? txnId : eventId, isReply, eventId.isEmpty())); + } + + if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) { + return m_eventContentModels.object(eventId); + } + + if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) { + if (eventId.isEmpty()) { + return m_eventContentModels.object(txnId); + } + + // If we now have an event ID use that as the map key instead of transaction ID. + auto txnModel = m_eventContentModels.take(txnId); + m_eventContentModels.insert(eventId, txnModel); + return m_eventContentModels.object(eventId); + } + + return nullptr; +} + +ThreadModel *ContentProvider::modelForThread(NeoChatRoom *room, const QString &threadRootId) +{ + if (!room || threadRootId.isEmpty()) { + return nullptr; + } + + if (!m_threadModels.contains(threadRootId)) { + m_threadModels.insert(threadRootId, new ThreadModel(threadRootId, room)); + } + + return m_threadModels.object(threadRootId); +} diff --git a/src/contentprovider.h b/src/contentprovider.h new file mode 100644 index 000000000..a0217bdbf --- /dev/null +++ b/src/contentprovider.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include + +#include "models/messagecontentmodel.h" +#include "models/threadmodel.h" +#include "neochatroom.h" + +/** + * @class ContentProvider + * + * Store and retrieve models for message content. + */ +class ContentProvider +{ +public: + /** + * Get the global instance of ContentProvider. + */ + static ContentProvider &self(); + + /** + * @brief Returns the content model for the given event ID. + * + * A model is created if one doesn't exist. Will return nullptr if evtOrTxnId + * is empty. + * + * @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent + * event ID. The caller must ensure that the ID is a real event. A model will be + * returned unconditionally. + * + * @warning Do NOT use for pending events as this function has no way to differentiate. + */ + MessageContentModel *contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply = false); + + /** + * @brief Returns the content model for the given event. + * + * A model is created if one doesn't exist. Will return nullptr if event is: + * - nullptr + * - not a Quotient::RoomMessageEvent (e.g a state event) + * + * @note This method is preferred to the version using just an event ID as it + * can perform some basic checks. If a copy of the event is not available, + * you may have to use the version that takes an event ID. + * + * @note This version must be used for pending events as it can differentiate. + */ + MessageContentModel *contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false); + + /** + * @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. + */ + ThreadModel *modelForThread(NeoChatRoom *room, const QString &threadRootId); + +private: + ContentProvider(); + + QCache m_eventContentModels; + QCache m_threadModels; +}; diff --git a/src/messageattached.cpp b/src/messageattached.cpp index 35775818d..9798dc8bf 100644 --- a/src/messageattached.cpp +++ b/src/messageattached.cpp @@ -50,6 +50,22 @@ void MessageAttached::setTimeline(QQuickItem *timeline) Q_EMIT timelineChanged(); } +MessageContentModel *MessageAttached::contentModel() const +{ + return m_contentModel; +} + +void MessageAttached::setContentModel(MessageContentModel *contentModel) +{ + m_explicitContentModel = true; + if (m_contentModel == contentModel) { + return; + } + m_contentModel = contentModel; + propagateMessage(this); + Q_EMIT contentModelChanged(); +} + int MessageAttached::index() const { return m_index; @@ -126,6 +142,11 @@ void MessageAttached::propagateMessage(MessageAttached *message) Q_EMIT timelineChanged(); } + if (!m_explicitContentModel && m_contentModel != message->contentModel()) { + m_contentModel = message->contentModel(); + Q_EMIT contentModelChanged(); + } + if (m_explicitIndex || m_index != message->index()) { m_index = message->index(); Q_EMIT indexChanged(); diff --git a/src/messageattached.h b/src/messageattached.h index 4bb5f231c..4e084d2aa 100644 --- a/src/messageattached.h +++ b/src/messageattached.h @@ -7,6 +7,7 @@ #include #include +#include "messagecontentmodel.h" #include "neochatroom.h" class MessageAttached : public QQuickAttachedPropertyPropagator @@ -26,6 +27,11 @@ class MessageAttached : public QQuickAttachedPropertyPropagator */ Q_PROPERTY(QQuickItem *timeline READ timeline WRITE setTimeline NOTIFY timelineChanged FINAL) + /** + * @brief The content model for the current message. + */ + Q_PROPERTY(MessageContentModel *contentModel READ contentModel WRITE setContentModel NOTIFY contentModelChanged FINAL) + /** * @brief The index of the message in the timeline */ @@ -57,6 +63,9 @@ public: QQuickItem *timeline() const; void setTimeline(QQuickItem *timeline); + MessageContentModel *contentModel() const; + void setContentModel(MessageContentModel *contentModel); + int index() const; void setIndex(int index); @@ -72,6 +81,7 @@ public: Q_SIGNALS: void roomChanged(); void timelineChanged(); + void contentModelChanged(); void indexChanged(); void maxContentWidthChanged(); void selectedTextChanged(); @@ -88,6 +98,9 @@ private: QPointer m_timeline; bool m_explicitTimeline = false; + QPointer m_contentModel; + bool m_explicitContentModel = false; + int m_index = -1; bool m_explicitIndex = false; diff --git a/src/models/mediamessagefiltermodel.cpp b/src/models/mediamessagefiltermodel.cpp index 46c9b8feb..f8817bcbf 100644 --- a/src/models/mediamessagefiltermodel.cpp +++ b/src/models/mediamessagefiltermodel.cpp @@ -6,7 +6,6 @@ #include #include -#include "messagecontentmodel.h" #include "messagefiltermodel.h" #include "timelinemessagemodel.h" diff --git a/src/models/messagecontentmodel.cpp b/src/models/messagecontentmodel.cpp index 62d398591..439e053de 100644 --- a/src/models/messagecontentmodel.cpp +++ b/src/models/messagecontentmodel.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "messagecontentmodel.h" +#include "contentprovider.h" #include "eventhandler.h" #include "messagecomponenttype.h" #include "neochatconfig.h" @@ -26,6 +27,7 @@ #endif #include "chatbarcache.h" +#include "contentprovider.h" #include "filetype.h" #include "linkpreviewer.h" #include "models/reactionmodel.h" @@ -767,4 +769,9 @@ void MessageContentModel::updateReactionModel() resetContent(); } +ThreadModel *MessageContentModel::modelForThread(const QString &threadRootId) +{ + return ContentProvider::self().modelForThread(m_room, threadRootId); +} + #include "moc_messagecontentmodel.cpp" diff --git a/src/models/messagecontentmodel.h b/src/models/messagecontentmodel.h index 84508edf9..07ceb26e2 100644 --- a/src/models/messagecontentmodel.h +++ b/src/models/messagecontentmodel.h @@ -14,6 +14,8 @@ #include "models/reactionmodel.h" #include "neochatroommember.h" +class ThreadModel; + /** * @class MessageContentModel * @@ -102,6 +104,14 @@ 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); + Q_SIGNALS: void showAuthorChanged(); void eventUpdated(); diff --git a/src/models/messagefiltermodel.cpp b/src/models/messagefiltermodel.cpp index ecc6b86f1..36099b4db 100644 --- a/src/models/messagefiltermodel.cpp +++ b/src/models/messagefiltermodel.cpp @@ -7,7 +7,6 @@ #include #include "enums/delegatetype.h" -#include "messagecontentmodel.h" #include "neochatconfig.h" #include "timelinemessagemodel.h" diff --git a/src/models/messagemodel.cpp b/src/models/messagemodel.cpp index 7e0e76a7d..27a1a8d89 100644 --- a/src/models/messagemodel.cpp +++ b/src/models/messagemodel.cpp @@ -4,6 +4,7 @@ #include "messagemodel.h" #include "neochatconfig.h" +#include "threadmodel.h" #include #include @@ -14,6 +15,7 @@ #include +#include "contentprovider.h" #include "enums/delegatetype.h" #include "enums/messagecomponenttype.h" #include "eventhandler.h" @@ -122,14 +124,14 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const if (role == ContentModelRole) { if (event->get().is() || event->get().is()) { - return QVariant::fromValue(m_room->contentModelForEvent(event->get().id())); + return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, event->get().id())); } auto roomMessageEvent = eventCast(&event.value().get()); if (NeoChatConfig::self()->threads() && roomMessageEvent && roomMessageEvent->isThreaded()) { - return QVariant::fromValue(m_room->contentModelForEvent(roomMessageEvent->threadRootEventId())); + return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, roomMessageEvent->threadRootEventId())); } - return QVariant::fromValue(m_room->contentModelForEvent(&event->get())); + return QVariant::fromValue(ContentProvider::self().contentModelForEvent(m_room, &event->get())); } if (role == GenericDisplayRole) { @@ -483,9 +485,4 @@ bool MessageModel::event(QEvent *event) return QObject::event(event); } -ThreadModel *MessageModel::threadModelForRootId(const QString &threadRootId) const -{ - return m_room->modelForThread(threadRootId); -} - #include "moc_messagemodel.cpp" diff --git a/src/models/messagemodel.h b/src/models/messagemodel.h index 4df174e68..ca2743e55 100644 --- a/src/models/messagemodel.h +++ b/src/models/messagemodel.h @@ -113,11 +113,6 @@ public: */ Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const; - /** - * @brief Get a ThreadModel for the give thread root Matrix ID. - */ - Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const; - Q_SIGNALS: /** * @brief Emitted when the room is changed. diff --git a/src/models/pinnedmessagemodel.cpp b/src/models/pinnedmessagemodel.cpp index 6a8c4de1d..5768202aa 100644 --- a/src/models/pinnedmessagemodel.cpp +++ b/src/models/pinnedmessagemodel.cpp @@ -5,7 +5,6 @@ #include "enums/delegatetype.h" #include "eventhandler.h" -#include "models/messagecontentmodel.h" #include "neochatroom.h" #include diff --git a/src/models/reactionmodel.cpp b/src/models/reactionmodel.cpp index 7fee8393d..2152f8dda 100644 --- a/src/models/reactionmodel.cpp +++ b/src/models/reactionmodel.cpp @@ -9,6 +9,7 @@ #include +#include "models/messagecontentmodel.h" #include "neochatroom.h" using namespace Qt::StringLiterals; diff --git a/src/models/threadmodel.cpp b/src/models/threadmodel.cpp index 84a3d74ec..9650fa61e 100644 --- a/src/models/threadmodel.cpp +++ b/src/models/threadmodel.cpp @@ -9,6 +9,7 @@ #include #include "chatbarcache.h" +#include "contentprovider.h" #include "eventhandler.h" #include "messagecomponenttype.h" #include "messagecontentmodel.h" @@ -136,9 +137,9 @@ void ThreadModel::addModels() } addSourceModel(m_threadFetchModel); for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) { - const auto contentModel = room->contentModelForEvent(*it); + const auto contentModel = ContentProvider::self().contentModelForEvent(room, *it); if (contentModel != nullptr) { - addSourceModel(room->contentModelForEvent(*it)); + addSourceModel(ContentProvider::self().contentModelForEvent(room, *it)); } } addSourceModel(m_threadChatBarModel); @@ -155,7 +156,7 @@ void ThreadModel::clearModels() } removeSourceModel(m_threadFetchModel); for (const auto &model : m_events) { - const auto contentModel = room->contentModelForEvent(model); + const auto contentModel = ContentProvider::self().contentModelForEvent(room, model); if (sourceModels().contains(contentModel)) { removeSourceModel(contentModel); } diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 64ffcf7d0..84d86e488 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -173,8 +173,6 @@ void NeoChatRoom::setVisible(bool visible) if (!visible) { m_memberObjects.clear(); - m_eventContentModels.clear(); - m_threadModels.clear(); } } @@ -1706,77 +1704,6 @@ NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId) return m_memberObjects[memberId].get(); } -MessageContentModel *NeoChatRoom::contentModelForEvent(const QString &eventId) -{ - if (eventId.isEmpty()) { - return nullptr; - } - - if (!m_eventContentModels.contains(eventId)) { - return m_eventContentModels.emplace(eventId, std::make_unique(this, eventId)).first->second.get(); - } - - return m_eventContentModels[eventId].get(); -} - -MessageContentModel *NeoChatRoom::contentModelForEvent(const Quotient::RoomEvent *event) -{ - const auto roomMessageEvent = eventCast(event); - if (roomMessageEvent == nullptr) { - // If for some reason a model is there remove. - if (m_eventContentModels.contains(event->id())) { - m_eventContentModels.erase(event->id()); - } - if (m_eventContentModels.contains(event->transactionId())) { - m_eventContentModels.erase(event->transactionId()); - } - return nullptr; - } - - if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) { - return nullptr; - } - - auto eventId = event->id(); - const auto txnId = event->transactionId(); - if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) { - return m_eventContentModels - .emplace(eventId.isEmpty() ? txnId : eventId, - std::make_unique(this, eventId.isEmpty() ? txnId : eventId, false, eventId.isEmpty())) - .first->second.get(); - } - - if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) { - return m_eventContentModels[eventId].get(); - } - - if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) { - if (eventId.isEmpty()) { - return m_eventContentModels[txnId].get(); - } - - // If we now have an event ID use that as the map key instead of transaction ID. - auto txnModel = std::move(m_eventContentModels[txnId]); - m_eventContentModels.erase(txnId); - return m_eventContentModels.emplace(eventId, std::move(txnModel)).first->second.get(); - } - - return nullptr; -} - -ThreadModel *NeoChatRoom::modelForThread(const QString &threadRootId) -{ - if (threadRootId.isEmpty()) { - return nullptr; - } - - if (!m_threadModels.contains(threadRootId)) { - return m_threadModels.emplace(threadRootId, std::make_unique(threadRootId, this)).first->second.get(); - } - - return m_threadModels[threadRootId].get(); -} - void NeoChatRoom::pinEvent(const QString &eventId) { auto eventIds = pinnedEventIds(); diff --git a/src/neochatroom.h b/src/neochatroom.h index 484b1014b..79dc6751a 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -16,8 +16,6 @@ #include "enums/messagetype.h" #include "enums/pushrule.h" #include "events/pollevent.h" -#include "models/messagecontentmodel.h" -#include "models/threadmodel.h" #include "neochatroommember.h" #include "pollhandler.h" @@ -560,43 +558,6 @@ public: */ NeochatRoomMember *qmlSafeMember(const QString &memberId); - /** - * @brief Returns the content model for the given event ID. - * - * A model is created is one doesn't exist. Will return nullptr if evtOrTxnId - * is empty. - * - * @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent - * event ID. The caller must ensure that the ID is a real event. A model will be - * returned unconditionally. - * - * @warning Do NOT use for pending events as this function has no way to differentiate. - */ - MessageContentModel *contentModelForEvent(const QString &evtOrTxnId); - - /** - * @brief Returns the content model for the given event. - * - * A model is created is one doesn't exist. Will return nullptr if event is: - * - nullptr - * - not a Quotient::RoomMessageEvent (e.g a state event) - * - * @note This method is preferred to the version using just an event ID as it - * can perform some basic checks. If a copy of the event is not available, - * you may have to use the version that takes an event ID. - * - * @note This version must be used for pending events as it can differentiate. - */ - MessageContentModel *contentModelForEvent(const Quotient::RoomEvent *event); - - /** - * @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); - /** * @brief Pin a message in the room. * @param eventId The id of the event to pin. @@ -646,8 +607,6 @@ private: void cleanupExtraEvent(const QString &eventId); std::unordered_map> m_memberObjects; - std::unordered_map> m_eventContentModels; - std::unordered_map> m_threadModels; private Q_SLOTS: void updatePushNotificationState(QString type); diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index af5fa44c6..2fbfd9a56 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -190,6 +190,7 @@ TimelineDelegate { Message.room: root.room Message.timeline: root.ListView.view + Message.contentModel: root.contentModel Message.index: root.index Message.maxContentWidth: contentMaxWidth diff --git a/src/timeline/ThreadBodyComponent.qml b/src/timeline/ThreadBodyComponent.qml index af1a161ca..220af9f75 100644 --- a/src/timeline/ThreadBodyComponent.qml +++ b/src/timeline/ThreadBodyComponent.qml @@ -38,7 +38,7 @@ ColumnLayout { Repeater { id: threadRepeater - model: root.Message.room.modelForThread(root.threadRoot); + model: root.Message.contentModel.modelForThread(root.threadRoot); delegate: BaseMessageComponentChooser { onSelectedTextChanged: selectedText => {