Separate out a base MessageContentModel.

Separate out a base `MessageContentModel` that can be extended to get the component types from different places. This is used currently for `EventMessageContentModel` but will be used later as part of the rich chat bar.

All display text is now in the text component so it never needs special casing. This also cleans up some of the model parameters so more things come from attributes including location and file data (which was already a qvariantmap anyway).

Also cleaned up the itinerary and file enhancement views,
This commit is contained in:
James Graham
2025-08-01 12:15:51 +01:00
parent 501f14fead
commit b4e1740cad
24 changed files with 874 additions and 789 deletions

View File

@@ -5,22 +5,29 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QImageReader>
#include <Quotient/events/roomevent.h>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
#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<int, QByteArray> roleNames() const override;
static QHash<int, QByteArray> 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<NeoChatRoom> 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<MessageComponent>::iterator;
@@ -141,14 +153,61 @@ private:
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
QPointer<MessageContentModel> m_replyModel;
QPointer<ReactionModel> m_reactionModel = nullptr;
QPointer<ItineraryModel> m_itineraryModel = nullptr;
bool m_emptyItinerary = false;
private:
void initializeModel();
std::function<ComponentIt(const ComponentIt &)> m_fileInfoFunction = [this](ComponentIt it) {
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {MessageContentModel::FileTransferInfoRole});
return ++it;
};
std::function<ComponentIt(const ComponentIt &)> m_linkPreviewFunction = [this](ComponentIt it) {
std::function<ComponentIt(const ComponentIt &)> 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<ComponentIt(const ComponentIt &)> 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<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();
ReactionModel *m_reactionModel = nullptr;
ItineraryModel *m_itineraryModel = nullptr;
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link);
std::function<ComponentIt(const ComponentIt &)> 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<QUrl> m_removedLinkPreviews;
void updateItineraryModel();
bool m_emptyItinerary = false;
void updateReactionModel();
static bool m_threadsEnabled;
MessageComponent linkPreviewComponent(const QUrl &link);
};