From 7bb7dd7bbb401500305e94894bbdff9e25246c37 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 9 Apr 2023 14:02:30 +0000 Subject: [PATCH] Document messageeventmodel Document the API and cleanup some unused roles. --- src/models/collapsestateproxymodel.cpp | 10 +- src/models/messageeventmodel.cpp | 50 ++--- src/models/messageeventmodel.h | 193 ++++++++++++------ src/models/messagefiltermodel.cpp | 2 +- src/qml/Component/ChatBox/ChatBar.qml | 2 +- src/qml/Component/Timeline/EventDelegate.qml | 2 +- .../Component/Timeline/TimelineContainer.qml | 2 +- src/qml/Page/RoomPage.qml | 6 +- 8 files changed, 160 insertions(+), 107 deletions(-) diff --git a/src/models/collapsestateproxymodel.cpp b/src/models/collapsestateproxymodel.cpp index 73ea935c8..2d674e4a9 100644 --- a/src/models/collapsestateproxymodel.cpp +++ b/src/models/collapsestateproxymodel.cpp @@ -9,9 +9,9 @@ bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent); - return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole) + return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole) != MessageEventModel::DelegateType::State // If this is not a state, show it - || sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole) + || sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole) != MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events? || sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it } @@ -47,7 +47,7 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const if (!uniqueAuthors.contains(nextAuthor)) { uniqueAuthors.append(nextAuthor); } - if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole) + if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != MessageEventModel::DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible ) { @@ -105,7 +105,7 @@ QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const {"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()}, }; stateEvents.append(nextState); - if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole) + if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != MessageEventModel::DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible ) { @@ -123,7 +123,7 @@ QVariantList CollapseStateProxyModel::authorList(int sourceRow) const if (!uniqueAuthors.contains(nextAvatar)) { uniqueAuthors.append(nextAvatar); } - if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole) + if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != MessageEventModel::DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible ) { diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index 11938aea5..d23b0af0c 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -33,7 +33,7 @@ using namespace Quotient; QHash MessageEventModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); - roles[EventTypeRole] = "eventType"; + roles[DelegateTypeRole] = "delegateType"; roles[MessageRole] = "message"; roles[EventIdRole] = "eventId"; roles[TimeRole] = "time"; @@ -45,12 +45,10 @@ QHash MessageEventModel::roleNames() const roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; roles[FileMimetypeIcon] = "fileMimetypeIcon"; - roles[AnnotationRole] = "annotation"; roles[EventResolvedTypeRole] = "eventResolvedType"; roles[IsReplyRole] = "isReply"; roles[ReplyRole] = "reply"; roles[ReplyIdRole] = "replyId"; - roles[UserMarkerRole] = "userMarker"; roles[ShowAuthorRole] = "showAuthor"; roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; @@ -76,7 +74,6 @@ QHash MessageEventModel::roleNames() const MessageEventModel::MessageEventModel(QObject *parent) : QAbstractListModel(parent) - , m_currentRoom(nullptr) { using namespace Quotient; qmlRegisterAnonymousType("org.kde.neochat", 1); @@ -89,6 +86,11 @@ MessageEventModel::MessageEventModel(QObject *parent) MessageEventModel::~MessageEventModel() = default; +NeoChatRoom *MessageEventModel::room() const +{ + return m_currentRoom; +} + void MessageEventModel::setRoom(NeoChatRoom *room) { if (room == m_currentRoom) { @@ -316,7 +318,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector & return -1; } row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); - if (data(index(row, 0), EventTypeRole).toInt() == ReadMarker || data(index(row, 0), EventTypeRole).toInt() == Other) { + if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) { row++; } } @@ -447,7 +449,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const if (m_lastReadEventIndex.row() == row) { switch (role) { - case EventTypeRole: + case DelegateTypeRole: return DelegateType::ReadMarker; case TimeRole: { const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime(); @@ -499,7 +501,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return evt.originalJson(); } - if (role == EventTypeRole) { + if (role == DelegateTypeRole) { if (auto e = eventCast(&evt)) { switch (e->msgtype()) { case MessageEventType::Emote: @@ -676,29 +678,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } } - if (role == AnnotationRole) { - if (isPending) { - return pendingIt->annotation(); - } - } - if (role == TimeRole || role == SectionRole) { auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt); return role == TimeRole ? QVariant(ts) : renderDate(ts); } - if (role == UserMarkerRole) { - QVariantList variantList; - const auto users = m_currentRoom->usersAtEventId(evt.id()); - for (User *user : users) { - if (user == m_currentRoom->localUser()) { - continue; - } - variantList.append(QVariant::fromValue(user)); - } - return variantList; - } - if (role == IsReplyRole) { return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty(); } @@ -784,7 +768,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { - return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) == MessageEventModel::State + return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State || data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000 || data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day(); } @@ -1005,7 +989,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return {}; } -int MessageEventModel::eventIDToIndex(const QString &eventID) const +int MessageEventModel::eventIdToRow(const QString &eventID) const { const auto it = m_currentRoom->findInTimeline(eventID); if (it == m_currentRoom->historyEdge()) { @@ -1047,7 +1031,7 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId() targetMessage.insert("event_id", eventId); targetMessage.insert("formattedBody", content["formatted_body"].toString()); // Need to get the message from the original eventId or body will have * on the front - QModelIndex idx = index(eventIDToIndex(eventId), 0); + QModelIndex idx = index(eventIdToRow(eventId), 0); targetMessage.insert("message", idx.data(Qt::UserRole + 2)); return targetMessage; @@ -1057,14 +1041,14 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId() return targetMessage; } -QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline) +QVariant MessageEventModel::getLatestMessageFromRow(const int startRow) { QVariantMap replyResponse; - const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + baseline; + const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow; - // set a cap limit of baseline + 35 messages, to prevent loading a lot of messages + // set a cap limit of startRow + 35 messages, to prevent loading a lot of messages // in rooms where the user has not sent many messages - const auto limit = timelineBottom + std::min(baseline + 35, m_currentRoom->timelineSize()); + const auto limit = timelineBottom + std::min(startRow + 35, m_currentRoom->timelineSize()); for (auto it = timelineBottom; it != limit; ++it) { auto evt = it->event(); @@ -1086,7 +1070,7 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline) } replyResponse.insert("event_id", eventId); // Need to get the message from the original eventId or body will have * on the front - QModelIndex idx = index(eventIDToIndex(eventId), 0); + QModelIndex idx = index(eventIdToRow(eventId), 0); replyResponse.insert("message", idx.data(Qt::UserRole + 2)); replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId()))); replyResponse.insert("at", -it->index()); diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index 5122bc380..e6cd971d2 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -7,77 +7,99 @@ #include "neochatroom.h" +/** + * @class MessageEventModel + * + * This class defines the model for visualising the room timeline. + * + * This model covers all event types in the timeline with many of the roles being + * specific to a subset of events. This means the user needs to understand which + * roles will return useful information for a given event type. + * + * @sa NeoChatRoom + */ class MessageEventModel : public QAbstractListModel { Q_OBJECT + + /** + * @brief The current room that the model is getting its messages from. + */ Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) public: + /** + * @brief The type of delegate that is needed for the event. + * + * @note While similar this is not the matrix event or message type. This is + * to tell a QML ListView what delegate to show for each event. So while + * similar to the spec it is not the same. + */ enum DelegateType { - Emote, - Notice, - Image, - Audio, - Video, - File, - Message, - Sticker, - State, - Encrypted, - ReadMarker, - Poll, - Location, - Other, + Emote, /**< A message that begins with /me. */ + Notice, /**< A notice event. */ + Image, /**< A message that is an image. */ + Audio, /**< A message that is an audio recording. */ + Video, /**< A message that is a video. */ + File, /**< A message that is a file. */ + Message, /**< A text message. */ + Sticker, /**< A message that is a sticker. */ + State, /**< A state event in the room. */ + Encrypted, /**< An encrypted message that cannot be decrypted. */ + ReadMarker, /**< The local user read marker. */ + Poll, /**< The initial event for a poll. */ + Location, /**< A location event. */ + Other, /**< Anything that cannot be classified as another type. */ }; Q_ENUM(DelegateType); + /** + * @brief Defines the model roles. + */ enum EventRoles { - EventTypeRole = Qt::UserRole + 1, - MessageRole, - EventIdRole, - TimeRole, - SectionRole, - AuthorRole, - ContentRole, - ContentTypeRole, - HighlightRole, - SpecialMarksRole, - LongOperationRole, - AnnotationRole, - UserMarkerRole, - FormattedBodyRole, - GenericDisplayRole, + DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */ + MessageRole, /**< Plain text representation of the message. */ + EventIdRole, /**< The matrix event ID of the event. */ + TimeRole, /**< The timestamp for when the event was sent. */ + SectionRole, /**< The date of the event as a string. */ + AuthorRole, /**< The author of the event. */ + ContentRole, /**< The full message content. */ + ContentTypeRole, /**< The content mime type. */ + HighlightRole, /**< Whether the event should be highlighted. */ + SpecialMarksRole, /**< Whether the event is hidden or not. */ + LongOperationRole, /**< Progress info when downloading files. */ + FormattedBodyRole, /**< The formatted body of a rich message. */ + GenericDisplayRole, /**< A generic string based upon the message type. */ - MimeTypeRole, - FileMimetypeIcon, + MimeTypeRole, /**< The mime type of the message's file or media. */ + FileMimetypeIcon, /**< The icon name for the mime type of a file. */ - IsReplyRole, - ReplyRole, - ReplyIdRole, + IsReplyRole, /**< Is the message a reply to another event. */ + ReplyRole, /**< The content data of the message that was replied to. */ + ReplyIdRole, /**< The matrix ID of the message that was replied to. */ - ShowAuthorRole, - ShowSectionRole, + ShowAuthorRole, /**< Whether the author's name should be shown. */ + ShowSectionRole, /**< Whether the section header should be shown. */ - ReadMarkersRole, /**< QVariantList of users at the event for read marker tracking. */ - ReadMarkersStringRole, /**< QString with the display name and mxID of the users at the event. */ - ShowReadMarkersRole, /**< bool with whether there are any other user read markers to be shown. */ - ReactionRole, + ReadMarkersRole, /**< Other users at the event for read marker tracking. */ + ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */ + ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ + ReactionRole, /**< List of reactions to this event. */ + SourceRole, /**< The full message source JSON. */ + MediaUrlRole, /**< The source URL for any media in the message. */ - SourceRole, - MediaUrlRole, // For debugging - EventResolvedTypeRole, - AuthorIdRole, - VerifiedRole, - // Sender's displayname, always without the matrix id - DisplayNameForInitialsRole, - // The displayname for the event's sender; for name change events, the old displayname - AuthorDisplayNameRole, - IsRedactedRole, - IsPendingRole, - LatitudeRole, - LongitudeRole, - AssetRole, + EventResolvedTypeRole, /**< The event type the message. */ + AuthorIdRole, /**< Matrix ID of the message author. */ + + VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */ + DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */ + AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */ + IsRedactedRole, /**< Whether an event has been deleted. */ + IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */ + 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. */ LastRole, // Keep this last }; Q_ENUM(EventRoles) @@ -85,20 +107,67 @@ public: explicit MessageEventModel(QObject *parent = nullptr); ~MessageEventModel() override; - [[nodiscard]] NeoChatRoom *room() const - { - return m_currentRoom; - } + [[nodiscard]] NeoChatRoom *room() const; void setRoom(NeoChatRoom *room); - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ [[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * @brief Returns a mapping from Role enum values to role names. + * + * @sa EventRoles, QAbstractItemModel::roleNames() + */ [[nodiscard]] QHash roleNames() const override; - Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const; + /** + * @brief Get the row number of the given event ID in the model. + */ + Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const; + + /** + * @brief Get the last message sent by the local user. + * + * @note This checks a maximum of the previous 35 message for performance reasons. + * + * @return a QVariantMap for the event with the following parameters: + * - eventId - The event ID. + * - formattedBody - The message text formatted as Qt::RichText. + * - message - The message text formatted as Qt::PlainText. + */ Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId(); - Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline); - Q_INVOKABLE void loadReply(const QModelIndex &row); + + /** + * @brief Get the last message sent earlier than the given row. + * + * @note This checks a maximum of the previous 35 message for performance reasons. + * + * @return a QVariantMap for the event with the following parameters: + * - eventId - The event ID. + * - message - The message text formatted as Qt::PlainText. + * - sender_id - The matrix ID of the sender. + * - at - The QModelIndex of the message. + */ + Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromRow(const int startRow); + + /** + * @brief Load the event that the item at the given index replied to. + * + * This is used to ensure that the reply data is available when the message that + * was replied to is outside the currently loaded timeline. + */ + Q_INVOKABLE void loadReply(const QModelIndex &index); private Q_SLOTS: int refreshEvent(const QString &eventId); diff --git a/src/models/messagefiltermodel.cpp b/src/models/messagefiltermodel.cpp index 3549c7f58..6a1a5d077 100644 --- a/src/models/messagefiltermodel.cpp +++ b/src/models/messagefiltermodel.cpp @@ -41,7 +41,7 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour return false; } - const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt(); + const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt(); if (eventType == MessageEventModel::Other) { return false; diff --git a/src/qml/Component/ChatBox/ChatBar.qml b/src/qml/Component/ChatBox/ChatBar.qml index 303ff3c87..8ecffe7a8 100644 --- a/src/qml/Component/ChatBox/ChatBar.qml +++ b/src/qml/Component/ChatBox/ChatBar.qml @@ -206,7 +206,7 @@ QQC2.Control { if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { chatBar.pasteImage(); } else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) { - let replyEvent = messageEventModel.getLatestMessageFromIndex(0) + let replyEvent = messageEventModel.getLatestMessageFromRow(0) if (replyEvent && replyEvent["event_id"]) { currentRoom.chatBoxReplyId = replyEvent["event_id"] } diff --git a/src/qml/Component/Timeline/EventDelegate.qml b/src/qml/Component/Timeline/EventDelegate.qml index eb4c9d41f..0a15e22d9 100644 --- a/src/qml/Component/Timeline/EventDelegate.qml +++ b/src/qml/Component/Timeline/EventDelegate.qml @@ -11,7 +11,7 @@ import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 DelegateChooser { - role: "eventType" + role: "delegateType" DelegateChoice { roleValue: MessageEventModel.State diff --git a/src/qml/Component/Timeline/TimelineContainer.qml b/src/qml/Component/Timeline/TimelineContainer.qml index 8b8d53709..7c538a81d 100644 --- a/src/qml/Component/Timeline/TimelineContainer.qml +++ b/src/qml/Component/Timeline/TimelineContainer.qml @@ -336,7 +336,7 @@ ColumnLayout { Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0 - visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0 + visible: delegateType !== MessageEventModel.State && delegateType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0 } AvatarFlow { Layout.alignment: Qt.AlignRight diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index 7d80b1017..75b6e3ab4 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -476,7 +476,7 @@ Kirigami.ScrollablePage { id: hoverActions property var event: null property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId - property bool showEdit: event && (userMsg && (event.eventType === MessageEventModel.Emote || event.eventType === MessageEventModel.Message)) + property bool showEdit: event && (userMsg && (event.delegateType === MessageEventModel.Emote || event.delegateType === MessageEventModel.Message)) property var delegate: null property var bubble: null property var hovered: bubble && bubble.hovered @@ -642,7 +642,7 @@ Kirigami.ScrollablePage { } function eventToIndex(eventID) { - const index = messageEventModel.eventIDToIndex(eventID) + const index = messageEventModel.eventIdToRow(eventID) if (index === -1) return -1 return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row @@ -702,7 +702,7 @@ Kirigami.ScrollablePage { eventId: event.eventId, formattedBody: event.formattedBody, source: event.source, - eventType: event.eventType, + eventType: event.delegateType, plainMessage: plainMessage, }); contextMenu.open();