diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index c21b6528d..2efbc58e8 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -6,12 +6,12 @@ #include "neochatconfig.h" #include -#include #include #include #include #include #include +#include #include #ifdef QUOTIENT_07 @@ -123,6 +123,14 @@ void MessageEventModel::setRoom(NeoChatRoom *room) #else lastReadEventId = room->readMarkerEventId(); #endif + connect(m_currentRoom, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) { + Q_UNUSED(replyId); + auto row = eventIdToRow(eventId); + if (row == -1) { + return; + } + Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor}); + }); connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { for (auto &&event : events) { @@ -675,7 +683,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } if (role == ReplyAuthor) { - auto replyPtr = getReplyForEvent(evt); + auto replyPtr = m_currentRoom->getReplyForEvent(evt); if (replyPtr) { auto replyUser = static_cast(m_currentRoom->user(replyPtr->senderId())); @@ -686,7 +694,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } if (role == ReplyMediaInfoRole) { - auto replyPtr = getReplyForEvent(evt); + auto replyPtr = m_currentRoom->getReplyForEvent(evt); if (!replyPtr) { return {}; } @@ -694,7 +702,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } if (role == ReplyRole) { - auto replyPtr = getReplyForEvent(evt); + auto replyPtr = m_currentRoom->getReplyForEvent(evt); if (!replyPtr) { return {}; } @@ -943,36 +951,6 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); } -void MessageEventModel::loadReply(const QModelIndex &index) -{ - auto job = m_currentRoom->connection()->callApi(m_currentRoom->id(), data(index, ReplyIdRole).toString()); - QPersistentModelIndex persistentIndex(index); - connect(job, &BaseJob::success, this, [this, job, persistentIndex] { - m_extraEvents.push_back(fromJson>(job->jsonData())); - Q_EMIT dataChanged(persistentIndex, persistentIndex, {ReplyRole, ReplyMediaInfoRole, ReplyAuthor}); - }); -} - -const RoomEvent *MessageEventModel::getReplyForEvent(const RoomEvent &event) const -{ - const QString &replyEventId = event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString(); - if (replyEventId.isEmpty()) { - return {}; - }; - - const auto replyIt = m_currentRoom->findInTimeline(replyEventId); - const RoomEvent *replyPtr = replyIt != m_currentRoom->historyEdge() ? &**replyIt : nullptr; - if (!replyPtr) { - for (const auto &e : m_extraEvents) { - if (e->id() == replyEventId) { - replyPtr = e.get(); - break; - } - } - } - return replyPtr; -} - QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const { QVariantMap mediaInfo; diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index 1064726a2..801d898b2 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -141,14 +141,6 @@ public: */ Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const; - /** - * @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); void refreshRow(int row); @@ -175,13 +167,10 @@ private: int refreshEventRoles(const QString &eventId, const QVector &roles = {}); void moveReadMarker(const QString &toEventId); - const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const; QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const; QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const; void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event); void createReactionModelForEvent(const Quotient::RoomMessageEvent *event); - - std::vector> m_extraEvents; // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows bool m_initialized = false; diff --git a/src/models/searchmodel.cpp b/src/models/searchmodel.cpp index bc550fb87..6fa72e00d 100644 --- a/src/models/searchmodel.cpp +++ b/src/models/searchmodel.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.0-or-later #include "searchmodel.h" +#include "events/stickerevent.h" #include "messageeventmodel.h" #include "neochatroom.h" #include "neochatuser.h" @@ -96,16 +97,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const case ShowAuthorRole: return true; case AuthorRole: - return QVariantMap{ - {"isLocalUser", event.senderId() == m_room->localUser()->id()}, - {"id", event.senderId()}, - {"avatarMediaId", m_connection->user(event.senderId())->avatarMediaId(m_room)}, - {"avatarUrl", m_connection->user(event.senderId())->avatarUrl(m_room)}, - {"displayName", m_connection->user(event.senderId())->displayname(m_room)}, - {"display", m_connection->user(event.senderId())->name()}, - {"color", dynamic_cast(m_connection->user(event.senderId()))->color()}, - {"object", QVariant::fromValue(m_connection->user(event.senderId()))}, - }; + return m_room->getUser(event.senderId()); case ShowSectionRole: if (row == 0) { return true; @@ -115,6 +107,72 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const return renderDate(event.originTimestamp()); case TimeRole: return event.originTimestamp(); + case ShowReactionsRole: + return false; + case ShowReadMarkersRole: + return false; + case ReplyAuthorRole: + if (const auto &replyPtr = m_room->getReplyForEvent(event)) { + return m_room->getUser(static_cast(m_room->user(replyPtr->senderId()))); + } else { + return m_room->getUser(nullptr); + } + case ReplyRole: + if (role == ReplyRole) { + auto replyPtr = m_room->getReplyForEvent(event); + if (!replyPtr) { + return {}; + } + + MessageEventModel::DelegateType type; + if (auto e = eventCast(replyPtr)) { + switch (e->msgtype()) { + case MessageEventType::Emote: + type = MessageEventModel::DelegateType::Emote; + break; + case MessageEventType::Notice: + type = MessageEventModel::DelegateType::Notice; + break; + case MessageEventType::Image: + type = MessageEventModel::DelegateType::Image; + break; + case MessageEventType::Audio: + type = MessageEventModel::DelegateType::Audio; + break; + case MessageEventType::Video: + type = MessageEventModel::DelegateType::Video; + break; + default: + if (e->hasFileContent()) { + type = MessageEventModel::DelegateType::File; + break; + } + type = MessageEventModel::DelegateType::Message; + } + + } else if (is(*replyPtr)) { + type = MessageEventModel::DelegateType::Sticker; + } else { + type = MessageEventModel::DelegateType::Other; + } + + return QVariantMap{ + {"display", m_room->eventToString(*replyPtr, Qt::RichText)}, + {"type", type}, + }; + } + case IsPendingRole: + return false; + case ShowLinkPreviewRole: + return false; + case IsReplyRole: + return !event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty(); + case HighlightRole: + return !m_room->isDirectChat() && m_room->isEventHighlighted(&event); + case EventIdRole: + return event.id(); + case ReplyIdRole: + return event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString(); } return MessageEventModel::DelegateType::Message; #endif @@ -142,6 +200,27 @@ QHash SearchModel::roleNames() const {SectionRole, "section"}, {TimeRole, "time"}, {ShowAuthorRole, "showAuthor"}, + {EventIdRole, "eventId"}, + {ExcessReadMarkersRole, "excessReadMarkers"}, + {HighlightRole, "isHighlighted"}, + {ReadMarkersString, "readMarkersString"}, + {PlainTextRole, "plainText"}, + {VerifiedRole, "verified"}, + {ReplyAuthorRole, "replyAuthor"}, + {ProgressInfoRole, "progressInfo"}, + {IsReplyRole, "isReply"}, + {ShowReactionsRole, "showReactions"}, + {ReplyRole, "reply"}, + {ReactionRole, "reaction"}, + {ReplyMediaInfoRole, "replyMediaInfo"}, + {ReadMarkersRole, "readMarkers"}, + {IsPendingRole, "isPending"}, + {ShowReadMarkersRole, "showReadMarkers"}, + {ReplyIdRole, "replyId"}, + {MimeTypeRole, "mimeType"}, + {ShowLinkPreviewRole, "showLinkPreview"}, + {LinkPreviewRole, "linkPreview"}, + {SourceRole, "source"}, }; } @@ -152,8 +231,26 @@ NeoChatRoom *SearchModel::room() const void SearchModel::setRoom(NeoChatRoom *room) { + if (m_room) { + disconnect(m_room, nullptr, this, nullptr); + } m_room = room; Q_EMIT roomChanged(); + +#ifdef QUOTIENT_07 + connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) { + Q_UNUSED(replyId); + const auto &results = m_result->results; + auto it = std::find_if(results.begin(), results.end(), [eventId](const auto &event) { + return event.result->id() == eventId; + }); + if (it == results.end()) { + return; + } + auto row = it - results.begin(); + Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole}); + }); +#endif } // TODO deduplicate with messageeventmodel diff --git a/src/models/searchmodel.h b/src/models/searchmodel.h index 45959f97f..629621fa5 100644 --- a/src/models/searchmodel.h +++ b/src/models/searchmodel.h @@ -50,17 +50,40 @@ public: /** * @brief Defines the model roles. * + * For documentation of the roles, see MessageEventModel. + * * Some of the roles exist only for compatibility with the MessageEventModel, * since the same delegates are used. */ enum Roles { - DisplayRole = Qt::DisplayRole, /**< The message string. */ - DelegateTypeRole, /**< The type of the event. */ - ShowAuthorRole, /**< Whether the author should be shown (always true). */ - AuthorRole, /**< The author of the event. */ - ShowSectionRole, /**< Whether the section header should be shown. */ - SectionRole, /**< The date of the event as a string. */ - TimeRole, /**< The timestamp for when the event was sent. */ + DisplayRole = Qt::DisplayRole, + DelegateTypeRole, + ShowAuthorRole, + AuthorRole, + ShowSectionRole, + SectionRole, + TimeRole, + EventIdRole, + ExcessReadMarkersRole, + HighlightRole, + ReadMarkersString, + PlainTextRole, + VerifiedRole, + ReplyAuthorRole, + ProgressInfoRole, + IsReplyRole, + ShowReactionsRole, + ReplyRole, + ReactionRole, + ReplyMediaInfoRole, + ReadMarkersRole, + IsPendingRole, + ShowReadMarkersRole, + ReplyIdRole, + MimeTypeRole, + ShowLinkPreviewRole, + LinkPreviewRole, + SourceRole, }; Q_ENUM(Roles); SearchModel(QObject *parent = nullptr); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 101c0fc50..df12edb18 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -18,10 +18,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -32,6 +34,7 @@ #include #include #include + #ifndef QUOTIENT_07 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include @@ -2069,3 +2072,32 @@ QUrl NeoChatRoom::avatarForMember(NeoChatUser *user) const return url; #endif } + +const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const +{ + const QString &replyEventId = event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString(); + if (replyEventId.isEmpty()) { + return {}; + }; + + const auto replyIt = findInTimeline(replyEventId); + const RoomEvent *replyPtr = replyIt != historyEdge() ? &**replyIt : nullptr; + if (!replyPtr) { + for (const auto &e : m_extraEvents) { + if (e->id() == replyEventId) { + replyPtr = e.get(); + break; + } + } + } + return replyPtr; +} + +void NeoChatRoom::loadReply(const QString &eventId, const QString &replyId) +{ + auto job = connection()->callApi(id(), replyId); + connect(job, &BaseJob::success, this, [this, job, eventId, replyId] { + m_extraEvents.push_back(fromJson>(job->jsonData())); + Q_EMIT replyLoaded(eventId, replyId); + }); +} diff --git a/src/neochatroom.h b/src/neochatroom.h index 6125cfa96..957852bb1 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -833,6 +833,18 @@ public: Q_INVOKABLE [[nodiscard]] QUrl avatarForMember(NeoChatUser *user) const; + /** + * @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply. + */ + const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const; + + /** + * Loads the event replyId with the given id from the server and saves it locally. + * For models to update correctly, eventId must be the event that is replying to replyId. + * Intended to load the replied-to event when it isn't available locally. + */ + Q_INVOKABLE void loadReply(const QString &eventId, const QString &replyId); + private: QSet highlights; @@ -864,6 +876,7 @@ private: #ifdef QUOTIENT_07 QCache m_polls; #endif + std::vector> m_extraEvents; private Q_SLOTS: void updatePushNotificationState(QString type); @@ -912,6 +925,7 @@ Q_SIGNALS: void serverAclPowerLevelChanged(); void spaceChildPowerLevelChanged(); void spaceParentPowerLevelChanged(); + void replyLoaded(const QString &eventId, const QString &replyId); public Q_SLOTS: /** diff --git a/src/qml/Component/Timeline/TimelineContainer.qml b/src/qml/Component/Timeline/TimelineContainer.qml index c85bffbb8..969c78587 100644 --- a/src/qml/Component/Timeline/TimelineContainer.qml +++ b/src/qml/Component/Timeline/TimelineContainer.qml @@ -342,7 +342,7 @@ ColumnLayout { Component.onCompleted: { if (root.isReply && root.reply === undefined) { - messageEventModel.loadReply(sortedMessageEventModel.mapToSource(collapseStateProxyModel.mapToSource(collapseStateProxyModel.index(root.index, 0)))) + currentRoom.loadReply(root.eventId, root.replyId) } } @@ -613,7 +613,9 @@ ColumnLayout { } function setHoverActionsToDelegate() { - ListView.view.setHoverActionsToDelegate(root) + if (ListView.view.setHoverActionsToDelegate) { + ListView.view.setHoverActionsToDelegate(root) + } } DelegateSizeHelper {