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:
@@ -192,6 +192,8 @@ add_library(neochat STATIC
|
||||
models/roomsortparametermodel.h
|
||||
models/messagemodel.cpp
|
||||
models/messagemodel.h
|
||||
models/messagecontentfiltermodel.cpp
|
||||
models/messagecontentfiltermodel.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
|
||||
@@ -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();
|
||||
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();
|
||||
|
||||
|
||||
37
src/models/messagecontentfiltermodel.cpp
Normal file
37
src/models/messagecontentfiltermodel.cpp
Normal 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;
|
||||
}
|
||||
43
src/models/messagecontentfiltermodel.h
Normal file
43
src/models/messagecontentfiltermodel.h
Normal 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;
|
||||
};
|
||||
@@ -233,32 +233,6 @@ NeochatRoomMember *MessageContentModel::senderObject() const
|
||||
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;
|
||||
|
||||
QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
@@ -434,9 +408,7 @@ void MessageContentModel::resetModel()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_showAuthor) {
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
}
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
|
||||
m_components += messageContentComponents();
|
||||
endResetModel();
|
||||
|
||||
@@ -25,11 +25,6 @@ class MessageContentModel : public QAbstractListModel
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief Whether the author component is being shown.
|
||||
*/
|
||||
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
||||
|
||||
public:
|
||||
enum MessageState {
|
||||
Unknown, /**< The message state is unknown. */
|
||||
@@ -75,9 +70,6 @@ public:
|
||||
bool isPending = false,
|
||||
MessageContentModel *parent = nullptr);
|
||||
|
||||
bool showAuthor() const;
|
||||
void setShowAuthor(bool showAuthor);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
@@ -117,7 +109,6 @@ private:
|
||||
NeochatRoomMember *senderObject() const;
|
||||
|
||||
MessageState m_currentState = Unknown;
|
||||
bool m_showAuthor = true;
|
||||
bool m_isReply;
|
||||
|
||||
void initializeModel();
|
||||
|
||||
@@ -92,12 +92,8 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
return authorList(mapToSource(index).row());
|
||||
} else if (role == ExcessAuthorsRole) {
|
||||
return excessAuthors(mapToSource(index).row());
|
||||
} else if (role == TimelineMessageModel::ContentModelRole) {
|
||||
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
|
||||
if (model != nullptr && !showAuthor(index)) {
|
||||
model->setShowAuthor(false);
|
||||
}
|
||||
return QVariant::fromValue<MessageContentModel *>(model);
|
||||
} else if (role == ShowAuthorRole) {
|
||||
return showAuthor(index);
|
||||
}
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
@@ -109,6 +105,7 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
roles[StateEventsRole] = "stateEvents";
|
||||
roles[AuthorListRole] = "authorList";
|
||||
roles[ExcessAuthorsRole] = "excessAuthors";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
StateEventsRole, /**< List of state events in the aggregated state. */
|
||||
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
|
||||
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
|
||||
ShowAuthorRole, /**< Whether the author of a message should be shown. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
|
||||
@@ -120,16 +120,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ContentModelRole) {
|
||||
QString modelId;
|
||||
if (!event->get().id().isEmpty() && m_contentModels.contains(event->get().id())) {
|
||||
modelId = event.value().get().id();
|
||||
} else if (!event.value().get().transactionId().isEmpty() && m_contentModels.contains(event.value().get().transactionId())) {
|
||||
modelId = event.value().get().transactionId();
|
||||
auto evtOrTxnId = event->get().id();
|
||||
if (evtOrTxnId.isEmpty()) {
|
||||
evtOrTxnId = event->get().transactionId();
|
||||
}
|
||||
if (!modelId.isEmpty()) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
|
||||
}
|
||||
return {};
|
||||
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(evtOrTxnId));
|
||||
}
|
||||
|
||||
if (role == GenericDisplayRole) {
|
||||
@@ -430,12 +425,6 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
|
||||
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);
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) {
|
||||
m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room));
|
||||
@@ -515,7 +504,6 @@ void MessageModel::clearModel()
|
||||
|
||||
void MessageModel::clearEventObjects()
|
||||
{
|
||||
m_contentModels.clear();
|
||||
m_reactionModels.clear();
|
||||
m_readMarkerModels.clear();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <QQmlEngine>
|
||||
#include <functional>
|
||||
|
||||
#include "messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
#include "readmarkermodel.h"
|
||||
@@ -153,7 +152,6 @@ private:
|
||||
bool resetting = false;
|
||||
bool movingEvent = false;
|
||||
|
||||
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
|
||||
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
|
||||
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
@@ -97,10 +97,9 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
|
||||
const auto connection = room->connection();
|
||||
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]() {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
auto newEvents = m_currentJob->chunk();
|
||||
for (auto &event : newEvents) {
|
||||
m_contentModels.push_back(new MessageContentModel(room, event->id()));
|
||||
m_events.push_back(event->id());
|
||||
}
|
||||
|
||||
addModels();
|
||||
@@ -122,12 +121,11 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
|
||||
|
||||
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
auto eventId = event->id();
|
||||
if (eventId.isEmpty()) {
|
||||
eventId = event->transactionId();
|
||||
}
|
||||
m_contentModels.push_front(new MessageContentModel(room, eventId));
|
||||
m_events.push_front(eventId);
|
||||
}
|
||||
|
||||
void ThreadModel::addModels()
|
||||
@@ -137,8 +135,15 @@ void ThreadModel::addModels()
|
||||
}
|
||||
|
||||
addSourceModel(m_threadRootContentModel.get());
|
||||
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
|
||||
addSourceModel(*it);
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
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);
|
||||
|
||||
@@ -149,9 +154,15 @@ void ThreadModel::addModels()
|
||||
void ThreadModel::clearModels()
|
||||
{
|
||||
removeSourceModel(m_threadRootContentModel.get());
|
||||
for (const auto &model : m_contentModels) {
|
||||
if (sourceModels().contains(model)) {
|
||||
removeSourceModel(model);
|
||||
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
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);
|
||||
|
||||
@@ -122,14 +122,9 @@ private:
|
||||
|
||||
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
|
||||
|
||||
std::deque<MessageContentModel *> m_contentModels;
|
||||
std::deque<QString> m_events;
|
||||
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;
|
||||
|
||||
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;
|
||||
|
||||
@@ -173,6 +173,7 @@ void NeoChatRoom::setVisible(bool visible)
|
||||
|
||||
if (!visible) {
|
||||
m_memberObjects.clear();
|
||||
m_eventContentModels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1754,4 +1755,46 @@ NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
|
||||
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"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "enums/messagetype.h"
|
||||
#include "enums/pushrule.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "neochatroommember.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
@@ -595,6 +596,8 @@ public:
|
||||
|
||||
NeochatRoomMember *qmlSafeMember(const QString &memberId);
|
||||
|
||||
MessageContentModel *contentModelForEvent(const QString &evtOrTxnId);
|
||||
|
||||
private:
|
||||
bool m_visible = false;
|
||||
|
||||
@@ -627,6 +630,7 @@ private:
|
||||
void cleanupExtraEvent(const QString &eventId);
|
||||
|
||||
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
||||
std::unordered_map<QString, std::unique_ptr<MessageContentModel>> m_eventContentModels;
|
||||
|
||||
private Q_SLOTS:
|
||||
void updatePushNotificationState(QString type);
|
||||
|
||||
@@ -41,6 +41,11 @@ QQC2.Control {
|
||||
*/
|
||||
property var author
|
||||
|
||||
/**
|
||||
* @brief Whether the message author should be shown.
|
||||
*/
|
||||
required property bool showAuthor
|
||||
|
||||
/**
|
||||
* @brief Whether the message should be highlighted.
|
||||
*/
|
||||
@@ -105,7 +110,10 @@ QQC2.Control {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: root.contentModel
|
||||
model: MessageContentFilterModel {
|
||||
showAuthor: root.showAuthor
|
||||
sourceModel: root.contentModel
|
||||
}
|
||||
delegate: MessageComponentChooser {
|
||||
room: root.room
|
||||
index: root.index
|
||||
|
||||
@@ -52,6 +52,11 @@ TimelineDelegate {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -222,11 +227,11 @@ TimelineDelegate {
|
||||
id: mainContainer
|
||||
|
||||
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.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
|
||||
onHoveredChanged: {
|
||||
@@ -246,7 +251,7 @@ TimelineDelegate {
|
||||
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
|
||||
source: root.author.avatarUrl
|
||||
color: root.author.color
|
||||
@@ -292,6 +297,7 @@ TimelineDelegate {
|
||||
room: root.room
|
||||
index: root.index
|
||||
author: root.author
|
||||
showAuthor: root.showAuthor
|
||||
isThreaded: root.isThreaded
|
||||
|
||||
// HACK: This is stupid but seemingly QConcatenateTablesProxyModel
|
||||
|
||||
Reference in New Issue
Block a user