Move the storage of MessageContentModels to the room

Move the storage of MessageContentModels to the room in the same manner as memeber objects to prevent duplication but mainly to make the system easier to maintain going forward with things like threads for example. This requires the creation of a MessageContentFilterModel as the same model may be used in multiple places, sometimes with the author showning sometimes not.
This commit is contained in:
James Graham
2025-01-11 13:16:14 +00:00
parent bb8f0eae1b
commit 37de1ec583
16 changed files with 177 additions and 89 deletions

View File

@@ -192,6 +192,8 @@ add_library(neochat STATIC
models/roomsortparametermodel.h models/roomsortparametermodel.h
models/messagemodel.cpp models/messagemodel.cpp
models/messagemodel.h models/messagemodel.h
models/messagecontentfiltermodel.cpp
models/messagecontentfiltermodel.h
) )
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES

View File

@@ -39,14 +39,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date(); const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay; return day != previousEventDay;
} }
// Catch and force the author to be shown for all rows
if (role == TimelineMessageModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
if (model != nullptr) {
model->setShowAuthor(true);
}
return QVariant::fromValue<MessageContentModel *>(model);
}
QVariantMap mediaInfo = mapToSource(index).data(TimelineMessageModel::MediaInfoRole).toMap(); QVariantMap mediaInfo = mapToSource(index).data(TimelineMessageModel::MediaInfoRole).toMap();

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentfiltermodel.h"
#include "enums/messagecomponenttype.h"
#include "models/messagecontentmodel.h"
MessageContentFilterModel::MessageContentFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
}
bool MessageContentFilterModel::showAuthor() const
{
return m_showAuthor;
}
void MessageContentFilterModel::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
Q_EMIT showAuthorChanged();
}
bool MessageContentFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (m_showAuthor) {
return true;
}
const auto index = sourceModel()->index(source_row, 0, source_parent);
auto contentType = static_cast<MessageComponentType::Type>(index.data(MessageContentModel::ComponentTypeRole).toInt());
return contentType != MessageComponentType::Author;
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class MessageContentFilterModel
*
* This model filters a message's contents.
*/
class MessageContentFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether the author component should be shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public:
explicit MessageContentFilterModel(QObject *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
Q_SIGNALS:
void showAuthorChanged();
protected:
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
bool m_showAuthor = true;
};

View File

@@ -233,32 +233,6 @@ NeochatRoomMember *MessageContentModel::senderObject() const
return m_room->qmlSafeMember(eventResult.first->senderId()); return m_room->qmlSafeMember(eventResult.first->senderId());
} }
bool MessageContentModel::showAuthor() const
{
return m_showAuthor;
}
void MessageContentModel::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
if (m_room->connection()->isIgnored(senderId())) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
endInsertRows();
} else {
beginRemoveRows({}, 0, 0);
m_components.remove(0, 1);
endRemoveRows();
}
}
Q_EMIT showAuthorChanged();
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer; static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
QVariant MessageContentModel::data(const QModelIndex &index, int role) const QVariant MessageContentModel::data(const QModelIndex &index, int role) const
@@ -434,9 +408,7 @@ void MessageContentModel::resetModel()
return; return;
} }
if (m_showAuthor) { m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
}
m_components += messageContentComponents(); m_components += messageContentComponents();
endResetModel(); endResetModel();

View File

@@ -25,11 +25,6 @@ class MessageContentModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
/**
* @brief Whether the author component is being shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public: public:
enum MessageState { enum MessageState {
Unknown, /**< The message state is unknown. */ Unknown, /**< The message state is unknown. */
@@ -75,9 +70,6 @@ public:
bool isPending = false, bool isPending = false,
MessageContentModel *parent = nullptr); MessageContentModel *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
* *
@@ -117,7 +109,6 @@ private:
NeochatRoomMember *senderObject() const; NeochatRoomMember *senderObject() const;
MessageState m_currentState = Unknown; MessageState m_currentState = Unknown;
bool m_showAuthor = true;
bool m_isReply; bool m_isReply;
void initializeModel(); void initializeModel();

View File

@@ -92,12 +92,8 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
return authorList(mapToSource(index).row()); return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) { } else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row()); return excessAuthors(mapToSource(index).row());
} else if (role == TimelineMessageModel::ContentModelRole) { } else if (role == ShowAuthorRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole)); return showAuthor(index);
if (model != nullptr && !showAuthor(index)) {
model->setShowAuthor(false);
}
return QVariant::fromValue<MessageContentModel *>(model);
} }
return QSortFilterProxyModel::data(index, role); return QSortFilterProxyModel::data(index, role);
} }
@@ -109,6 +105,7 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
roles[StateEventsRole] = "stateEvents"; roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList"; roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors"; roles[ExcessAuthorsRole] = "excessAuthors";
roles[ShowAuthorRole] = "showAuthor";
return roles; return roles;
} }

View File

@@ -34,6 +34,7 @@ public:
StateEventsRole, /**< List of state events in the aggregated state. */ StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */ AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */ ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
ShowAuthorRole, /**< Whether the author of a message should be shown. */
LastRole, // Keep this last LastRole, // Keep this last
}; };

View File

@@ -120,16 +120,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
} }
if (role == ContentModelRole) { if (role == ContentModelRole) {
QString modelId; auto evtOrTxnId = event->get().id();
if (!event->get().id().isEmpty() && m_contentModels.contains(event->get().id())) { if (evtOrTxnId.isEmpty()) {
modelId = event.value().get().id(); evtOrTxnId = event->get().transactionId();
} else if (!event.value().get().transactionId().isEmpty() && m_contentModels.contains(event.value().get().transactionId())) {
modelId = event.value().get().transactionId();
} }
if (!modelId.isEmpty()) { return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(evtOrTxnId));
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
}
return {};
} }
if (role == GenericDisplayRole) { if (role == GenericDisplayRole) {
@@ -430,12 +425,6 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
senderId = m_room->localMember().id(); senderId = m_room->localMember().id();
} }
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
if (!event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_room, eventId, false, isPending));
}
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event); const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) { if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) {
m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room)); m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room));
@@ -515,7 +504,6 @@ void MessageModel::clearModel()
void MessageModel::clearEventObjects() void MessageModel::clearEventObjects()
{ {
m_contentModels.clear();
m_reactionModels.clear(); m_reactionModels.clear();
m_readMarkerModels.clear(); m_readMarkerModels.clear();
} }

View File

@@ -7,7 +7,6 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <functional> #include <functional>
#include "messagecontentmodel.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "pollhandler.h" #include "pollhandler.h"
#include "readmarkermodel.h" #include "readmarkermodel.h"
@@ -153,7 +152,6 @@ private:
bool resetting = false; bool resetting = false;
bool movingEvent = false; bool movingEvent = false;
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels; QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels; QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels; QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;

View File

@@ -97,10 +97,9 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
const auto connection = room->connection(); const auto connection = room->connection();
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5); m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5);
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() { connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto newEvents = m_currentJob->chunk(); auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) { for (auto &event : newEvents) {
m_contentModels.push_back(new MessageContentModel(room, event->id())); m_events.push_back(event->id());
} }
addModels(); addModels();
@@ -122,12 +121,11 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event) void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
{ {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto eventId = event->id(); auto eventId = event->id();
if (eventId.isEmpty()) { if (eventId.isEmpty()) {
eventId = event->transactionId(); eventId = event->transactionId();
} }
m_contentModels.push_front(new MessageContentModel(room, eventId)); m_events.push_front(eventId);
} }
void ThreadModel::addModels() void ThreadModel::addModels()
@@ -137,8 +135,15 @@ void ThreadModel::addModels()
} }
addSourceModel(m_threadRootContentModel.get()); addSourceModel(m_threadRootContentModel.get());
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) { const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
addSourceModel(*it); if (room == nullptr) {
return;
}
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
const auto contentModel = room->contentModelForEvent(*it);
if (contentModel != nullptr) {
addSourceModel(room->contentModelForEvent(*it));
}
} }
addSourceModel(m_threadChatBarModel); addSourceModel(m_threadChatBarModel);
@@ -149,9 +154,15 @@ void ThreadModel::addModels()
void ThreadModel::clearModels() void ThreadModel::clearModels()
{ {
removeSourceModel(m_threadRootContentModel.get()); removeSourceModel(m_threadRootContentModel.get());
for (const auto &model : m_contentModels) {
if (sourceModels().contains(model)) { const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
removeSourceModel(model); if (room == nullptr) {
return;
}
for (const auto &model : m_events) {
const auto contentModel = room->contentModelForEvent(model);
if (sourceModels().contains(contentModel)) {
removeSourceModel(contentModel);
} }
} }
removeSourceModel(m_threadChatBarModel); removeSourceModel(m_threadChatBarModel);

View File

@@ -122,14 +122,9 @@ private:
std::unique_ptr<MessageContentModel> m_threadRootContentModel; std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<MessageContentModel *> m_contentModels; std::deque<QString> m_events;
ThreadChatBarModel *m_threadChatBarModel; ThreadChatBarModel *m_threadChatBarModel;
QList<QString> m_events;
QList<QString> m_pendingEvents;
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels; QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr; QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;

View File

@@ -173,6 +173,7 @@ void NeoChatRoom::setVisible(bool visible)
if (!visible) { if (!visible) {
m_memberObjects.clear(); m_memberObjects.clear();
m_eventContentModels.clear();
} }
} }
@@ -1754,4 +1755,46 @@ NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
return m_memberObjects[memberId].get(); return m_memberObjects[memberId].get();
} }
MessageContentModel *NeoChatRoom::contentModelForEvent(const QString &evtOrTxnId)
{
const auto event = getEvent(evtOrTxnId);
if (event.first == nullptr) {
// If for some reason a model is there remove.
if (m_eventContentModels.contains(evtOrTxnId)) {
m_eventContentModels.erase(evtOrTxnId);
}
return nullptr;
}
if (event.first->isStateEvent() || event.first->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return nullptr;
}
auto eventId = event.first->id();
const auto txnId = event.first->transactionId();
if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) {
if (eventId.isEmpty()) {
eventId = txnId;
}
return m_eventContentModels.emplace(eventId, std::make_unique<MessageContentModel>(this, eventId, false, event.second)).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;
}
#include "moc_neochatroom.cpp" #include "moc_neochatroom.cpp"

View File

@@ -14,6 +14,7 @@
#include "enums/messagetype.h" #include "enums/messagetype.h"
#include "enums/pushrule.h" #include "enums/pushrule.h"
#include "models/messagecontentmodel.h"
#include "neochatroommember.h" #include "neochatroommember.h"
#include "pollhandler.h" #include "pollhandler.h"
@@ -595,6 +596,8 @@ public:
NeochatRoomMember *qmlSafeMember(const QString &memberId); NeochatRoomMember *qmlSafeMember(const QString &memberId);
MessageContentModel *contentModelForEvent(const QString &evtOrTxnId);
private: private:
bool m_visible = false; bool m_visible = false;
@@ -627,6 +630,7 @@ private:
void cleanupExtraEvent(const QString &eventId); void cleanupExtraEvent(const QString &eventId);
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects; std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
std::unordered_map<QString, std::unique_ptr<MessageContentModel>> m_eventContentModels;
private Q_SLOTS: private Q_SLOTS:
void updatePushNotificationState(QString type); void updatePushNotificationState(QString type);

View File

@@ -41,6 +41,11 @@ QQC2.Control {
*/ */
property var author property var author
/**
* @brief Whether the message author should be shown.
*/
required property bool showAuthor
/** /**
* @brief Whether the message should be highlighted. * @brief Whether the message should be highlighted.
*/ */
@@ -105,7 +110,10 @@ QQC2.Control {
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
Repeater { Repeater {
id: contentRepeater id: contentRepeater
model: root.contentModel model: MessageContentFilterModel {
showAuthor: root.showAuthor
sourceModel: root.contentModel
}
delegate: MessageComponentChooser { delegate: MessageComponentChooser {
room: root.room room: root.room
index: root.index index: root.index

View File

@@ -52,6 +52,11 @@ TimelineDelegate {
*/ */
required property NeochatRoomMember author required property NeochatRoomMember author
/**
* @brief Whether the message author should be shown.
*/
required property bool showAuthor
/** /**
* @brief The model to visualise the content of the message. * @brief The model to visualise the content of the message.
*/ */
@@ -222,11 +227,11 @@ TimelineDelegate {
id: mainContainer id: mainContainer
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: root.contentModel?.showAuthor ? Kirigami.Units.largeSpacing : (NeoChatConfig.compactLayout ? 1 : Kirigami.Units.smallSpacing) Layout.topMargin: root.showAuthor ? Kirigami.Units.largeSpacing : (NeoChatConfig.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(root.contentModel?.showAuthor ? avatar.implicitHeight : 0, bubble.height) implicitHeight: Math.max(root.showAuthor ? avatar.implicitHeight : 0, bubble.height)
// show hover actions // show hover actions
onHoveredChanged: { onHoveredChanged: {
@@ -246,7 +251,7 @@ TimelineDelegate {
topMargin: Kirigami.Units.smallSpacing topMargin: Kirigami.Units.smallSpacing
} }
visible: ((root.contentModel?.showAuthor ?? false) || root.isThreaded) && NeoChatConfig.showAvatarInTimeline && (NeoChatConfig.compactLayout || !_private.showUserMessageOnRight) visible: ((root.showAuthor ?? false) || root.isThreaded) && NeoChatConfig.showAvatarInTimeline && (NeoChatConfig.compactLayout || !_private.showUserMessageOnRight)
name: root.author.displayName name: root.author.displayName
source: root.author.avatarUrl source: root.author.avatarUrl
color: root.author.color color: root.author.color
@@ -292,6 +297,7 @@ TimelineDelegate {
room: root.room room: root.room
index: root.index index: root.index
author: root.author author: root.author
showAuthor: root.showAuthor
isThreaded: root.isThreaded isThreaded: root.isThreaded
// HACK: This is stupid but seemingly QConcatenateTablesProxyModel // HACK: This is stupid but seemingly QConcatenateTablesProxyModel