From 73de99f661646dc80e7db10df5a0e4b0d2524c5d Mon Sep 17 00:00:00 2001 From: James Graham Date: Sat, 20 Jul 2024 18:05:15 +0000 Subject: [PATCH] Create a list model for readmarkers Create a list model for read markers. The primary reason is to stop `RoomMembers` being accessed after their state event is deleted. With this the read marker doesn't pass and `RoomMember` objects to qml. --- autotests/eventhandlertest.cpp | 56 ------------- src/CMakeLists.txt | 2 + src/eventhandler.cpp | 98 ---------------------- src/eventhandler.h | 37 --------- src/models/messageeventmodel.cpp | 99 ++++++++++++---------- src/models/messageeventmodel.h | 6 +- src/models/readmarkermodel.cpp | 136 +++++++++++++++++++++++++++++++ src/models/readmarkermodel.h | 79 ++++++++++++++++++ src/timeline/AvatarFlow.qml | 16 ++-- src/timeline/MessageDelegate.qml | 12 --- src/timeline/StateDelegate.qml | 12 --- 11 files changed, 286 insertions(+), 267 deletions(-) create mode 100644 src/models/readmarkermodel.cpp create mode 100644 src/models/readmarkermodel.h diff --git a/autotests/eventhandlertest.cpp b/autotests/eventhandlertest.cpp index 09aa20aa3..0b62c7c01 100644 --- a/autotests/eventhandlertest.cpp +++ b/autotests/eventhandlertest.cpp @@ -75,8 +75,6 @@ private Q_SLOTS: void nullThread(); void location(); void nullLocation(); - void readMarkers(); - void nullReadMarkers(); }; void EventHandlerTest::initTestCase() @@ -521,59 +519,5 @@ void EventHandlerTest::nullLocation() QCOMPARE(emptyHandler.getLocationAssetType(), QString()); } -void EventHandlerTest::readMarkers() -{ - EventHandler eventHandler(room, room->messageEvents().at(0).get()); - QCOMPARE(eventHandler.hasReadMarkers(), true); - - auto readMarkers = eventHandler.getReadMarkers(); - - QCOMPARE(readMarkers.size(), 1); - QCOMPARE(readMarkers[0].id(), QStringLiteral("@alice:example.org")); - - QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString()); - QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: Alice Margatroid")); - - EventHandler eventHandler2(room, room->messageEvents().at(2).get()); - QCOMPARE(eventHandler2.hasReadMarkers(), true); - - readMarkers = eventHandler2.getReadMarkers(); - - QCOMPARE(readMarkers.size(), 5); - - QCOMPARE(eventHandler2.getNumberExcessReadMarkers(), QStringLiteral("+ 1")); - // There are no guarantees on the order of the users it will be different every time so don't match the whole string. - QCOMPARE(eventHandler2.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true); -} - -void EventHandlerTest::nullReadMarkers() -{ - QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr."); - QCOMPARE(emptyHandler.hasReadMarkers(), false); - - QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr."); - QCOMPARE(emptyHandler.getReadMarkers(), QList()); - - QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr."); - QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString()); - - QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr."); - QCOMPARE(emptyHandler.getReadMarkersString(), QString()); - - EventHandler noEventHandler(room, nullptr); - - QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr."); - QCOMPARE(noEventHandler.hasReadMarkers(), false); - - QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr."); - QCOMPARE(noEventHandler.getReadMarkers(), QList()); - - QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr."); - QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString()); - - QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr."); - QCOMPARE(noEventHandler.getReadMarkersString(), QString()); -} - QTEST_MAIN(EventHandlerTest) #include "eventhandlertest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 77ba048c6..aa89f0ca1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -186,6 +186,8 @@ add_library(neochat STATIC models/permissionsmodel.h threepidbindhelper.cpp threepidbindhelper.h + models/readmarkermodel.cpp + models/readmarkermodel.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES diff --git a/src/eventhandler.cpp b/src/eventhandler.cpp index ae18ecb70..237c57382 100644 --- a/src/eventhandler.cpp +++ b/src/eventhandler.cpp @@ -961,102 +961,4 @@ QString EventHandler::getLocationAssetType() const return assetType; } -bool EventHandler::hasReadMarkers() const -{ - if (m_room == nullptr) { - qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr."; - return false; - } - if (m_event == nullptr) { - qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr."; - return false; - } - - auto userIds = m_room->userIdsAtEvent(m_event->id()); - userIds.remove(m_room->localMember().id()); - return userIds.size() > 0; -} - -QList EventHandler::getReadMarkers(int maxMarkers) const -{ - if (m_room == nullptr) { - qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr."; - return {}; - } - if (m_event == nullptr) { - qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr."; - return {}; - } - - auto userIds_temp = m_room->userIdsAtEvent(m_event->id()); - userIds_temp.remove(m_room->localMember().id()); - - auto userIds = userIds_temp.values(); - if (userIds.count() > maxMarkers) { - userIds = userIds.mid(0, maxMarkers); - } - - QList users; - users.reserve(userIds.size()); - for (const auto &userId : userIds) { - users += m_room->member(userId); - } - - return users; -} - -QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const -{ - if (m_room == nullptr) { - qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr."; - return {}; - } - if (m_event == nullptr) { - qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr."; - return {}; - } - - auto userIds = m_room->userIdsAtEvent(m_event->id()); - userIds.remove(m_room->localMember().id()); - - if (userIds.count() > maxMarkers) { - return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers); - } else { - return QString(); - } -} - -QString EventHandler::getReadMarkersString() const -{ - if (m_room == nullptr) { - qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr."; - return {}; - } - if (m_event == nullptr) { - qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr."; - return {}; - } - - auto userIds = m_room->userIdsAtEvent(m_event->id()); - userIds.remove(m_room->localMember().id()); - - /** - * The string ends up in the form - * "x users: user1DisplayName, user2DisplayName, etc." - */ - QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size()); - for (const auto &userId : userIds) { - auto member = m_room->member(userId); - QString displayName; - if (!m_room->memberIds().contains(userId)) { - displayName = i18nc("A member who is not in the room has been requested.", "unknown member"); - } else { - displayName = member.displayName(); - } - readMarkersString += displayName + i18nc("list separator", ", "); - } - readMarkersString.chop(2); - return readMarkersString; -} - #include "moc_eventhandler.cpp" diff --git a/src/eventhandler.h b/src/eventhandler.h index e0d7a160b..ca4c2895d 100644 --- a/src/eventhandler.h +++ b/src/eventhandler.h @@ -342,43 +342,6 @@ public: */ QString getLocationAssetType() const; - /** - * @brief Whether the event has any read marker for other users. - */ - bool hasReadMarkers() const; - - /** - * @brief Returns a list of user read marker for the event. - * - * @param maxMarkers the maximum number of users to return. Usually the number - * of user read makers shown is limited to not clutter the UI. - * This needs to be the same as used in getNumberExcessReadMarkers - * so that the markers line up with the number displayed, i.e. - * the number of users shown plus the excess number will be - * the total number of other user read markers at an event. - */ - QList getReadMarkers(int maxMarkers = 5) const; - - /** - * @brief Returns the number of excess user read markers for the event. - * - * This returns a string in the form "+ x" ready for use in the UI. - * - * @param maxMarkers the maximum number of markers shown in the UI. This needs to - * be the same as used in getReadMarkers so that the value lines - * up with the number displayed, i.e. the number of users shown - * plus the excess number will be the total number of other user - * read markers at an event. - */ - QString getNumberExcessReadMarkers(int maxMarkers = 5) const; - - /** - * @brief Returns a string with the names of the read markers at the event. - * - * This is in the form "x users: name 1, name 2, ...". - */ - QString getReadMarkersString() const; - private: const NeoChatRoom *m_room = nullptr; const Quotient::RoomEvent *m_event = nullptr; diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index ae7ec83bd..26eaf1b5a 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -26,6 +26,7 @@ #include "messagecontentmodel.h" #include "models/messagefiltermodel.h" #include "models/reactionmodel.h" +#include "readmarkermodel.h" #include "texthandler.h" using namespace Quotient; @@ -45,8 +46,6 @@ QHash MessageEventModel::roleNames() const roles[ThreadRootRole] = "threadRoot"; roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; - roles[ExcessReadMarkersRole] = "excessReadMarkers"; - roles[ReadMarkersStringRole] = "readMarkersString"; roles[ShowReadMarkersRole] = "showReadMarkers"; roles[ReactionRole] = "reaction"; roles[ShowReactionsRole] = "showReactions"; @@ -87,6 +86,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) // HACK: Reset the model to a null room first to make sure QML dismantles // last room's objects before the room is actually changed beginResetModel(); + m_readMarkerModels.clear(); m_currentRoom->disconnect(this); m_currentRoom = nullptr; endResetModel(); @@ -100,9 +100,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) room->setDisplayed(); for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) { - if (const auto &roomMessageEvent = &*event->viewAs()) { - createEventObjects(roomMessageEvent); - } + createEventObjects(&*event->viewAs()); if (event->event()->is()) { m_currentRoom->createPollHandler(eventCast(event->event())); } @@ -115,11 +113,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { for (auto &&event : events) { - const RoomMessageEvent *message = dynamic_cast(event.get()); - - if (message != nullptr) { - createEventObjects(message); - } + createEventObjects(event.get()); if (event->is()) { m_currentRoom->createPollHandler(eventCast(event.get())); } @@ -129,9 +123,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) }); connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { for (auto &event : events) { - if (const auto &roomMessageEvent = dynamic_cast(event.get())) { - createEventObjects(roomMessageEvent); - } + createEventObjects(event.get()); if (event->is()) { m_currentRoom->createPollHandler(eventCast(event.get())); } @@ -195,10 +187,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) moveReadMarker(toEventId); }); connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { - const RoomMessageEvent *message = eventCast(newEvent); - if (message != nullptr) { - createEventObjects(message); - } + createEventObjects(newEvent); }); connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) { if (eventId.isEmpty()) { // How did we get here? @@ -206,9 +195,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) } const auto eventIt = m_currentRoom->findInTimeline(eventId); if (eventIt != m_currentRoom->historyEdge()) { - if (const auto &event = dynamic_cast(&**eventIt)) { - createEventObjects(event); - } + createEventObjects(eventIt->event()); if (eventIt->event()->is()) { m_currentRoom->createPollHandler(eventCast(eventIt->event())); } @@ -216,11 +203,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room) refreshEventRoles(eventId, {Qt::DisplayRole}); }); connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) { - if (changes & (Room::Change::PartiallyReadStats | Room::Change::UnreadStats | Room::Change::Other | Room::Change::Members)) { + if (changes.testFlag(Quotient::Room::Change::Other)) { // this is slow for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) { - auto event = it->event(); - refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole}); + createEventObjects(it->event()); } } }); @@ -557,19 +543,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } if (role == ReadMarkersRole) { - return QVariant::fromValue(eventHandler.getReadMarkers()); - } - - if (role == ExcessReadMarkersRole) { - return eventHandler.getNumberExcessReadMarkers(); - } - - if (role == ReadMarkersStringRole) { - return eventHandler.getReadMarkersString(); + if (m_readMarkerModels.contains(evt.id())) { + return QVariant::fromValue(m_readMarkerModels[evt.id()].get()); + } else { + return QVariantList(); + } } if (role == ShowReadMarkersRole) { - return eventHandler.hasReadMarkers(); + return m_readMarkerModels.contains(evt.id()); } if (role == ReactionRole) { @@ -630,30 +612,61 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); } -void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event) +void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event) { + if (event == nullptr) { + return; + } + auto eventId = event->id(); - // ReactionModel handles updates to add and remove reactions, we only need to + // ReadMarkerModel handles updates to add and remove markers, we only need to // handle adding and removing whole models here. - if (m_reactionModels.contains(eventId)) { + if (m_readMarkerModels.contains(eventId)) { // If a model already exists but now has no reactions remove it - if (m_reactionModels[eventId]->rowCount() <= 0) { - m_reactionModels.remove(eventId); + if (m_readMarkerModels[eventId]->rowCount() <= 0) { + m_readMarkerModels.remove(eventId); if (!resetting) { - refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); + refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); } } } else { - if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) { + auto memberIds = m_currentRoom->userIdsAtEvent(eventId); + memberIds.remove(m_currentRoom->localMember().id()); + if (memberIds.size() > 0) { // If a model doesn't exist and there are reactions add it. - auto reactionModel = QSharedPointer(new ReactionModel(event, m_currentRoom)); - if (reactionModel->rowCount() > 0) { - m_reactionModels[eventId] = reactionModel; + auto newModel = QSharedPointer(new ReadMarkerModel(eventId, m_currentRoom)); + if (newModel->rowCount() > 0) { + m_readMarkerModels[eventId] = newModel; + if (!resetting) { + refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); + } + } + } + } + + if (const auto roomEvent = eventCast(event)) { + // ReactionModel handles updates to add and remove reactions, we only need to + // handle adding and removing whole models here. + if (m_reactionModels.contains(eventId)) { + // If a model already exists but now has no reactions remove it + if (m_reactionModels[eventId]->rowCount() <= 0) { + m_reactionModels.remove(eventId); if (!resetting) { refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); } } + } else { + if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) { + // If a model doesn't exist and there are reactions add it. + auto reactionModel = QSharedPointer(new ReactionModel(roomEvent, m_currentRoom)); + if (reactionModel->rowCount() > 0) { + m_reactionModels[eventId] = reactionModel; + if (!resetting) { + refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); + } + } + } } } } diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index deab8fd65..cdf7cb685 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -10,6 +10,7 @@ #include "linkpreviewer.h" #include "neochatroom.h" #include "pollhandler.h" +#include "readmarkermodel.h" class ReactionModel; @@ -58,8 +59,6 @@ public: ShowSectionRole, /**< Whether the section header should be shown. */ ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */ - ExcessReadMarkersRole, /**< The number of other users at the event after the first 5. */ - 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 model for this event. */ ShowReactionsRole, /**< Whether there are any reactions to be shown. */ @@ -116,6 +115,7 @@ private: bool movingEvent = false; KFormat m_format; + QMap> m_readMarkerModels; QMap> m_reactionModels; [[nodiscard]] int timelineBaseIndex() const; @@ -130,7 +130,7 @@ private: int refreshEventRoles(const QString &eventId, const QList &roles = {}); void moveReadMarker(const QString &toEventId); - void createEventObjects(const Quotient::RoomMessageEvent *event); + void createEventObjects(const Quotient::RoomEvent *event); // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows bool m_initialized = false; diff --git a/src/models/readmarkermodel.cpp b/src/models/readmarkermodel.cpp new file mode 100644 index 000000000..bdfaed912 --- /dev/null +++ b/src/models/readmarkermodel.cpp @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "readmarkermodel.h" + +#include + +#include + +#define MAXMARKERS 5 + +ReadMarkerModel::ReadMarkerModel(const QString &eventId, NeoChatRoom *room) + : QAbstractListModel(nullptr) + , m_room(room) + , m_eventId(eventId) +{ + Q_ASSERT(!m_eventId.isEmpty()); + Q_ASSERT(m_room != nullptr); + + connect(m_room, &NeoChatRoom::changed, this, [this](Quotient::Room::Changes changes) { + if (m_room != nullptr && changes.testFlag(Quotient::Room::Change::Other)) { + auto memberIds = m_room->userIdsAtEvent(m_eventId).values(); + if (memberIds == m_markerIds) { + return; + } + + beginResetModel(); + m_markerIds.clear(); + endResetModel(); + + beginResetModel(); + memberIds.removeAll(m_room->localMember().id()); + m_markerIds = memberIds; + endResetModel(); + + Q_EMIT reactionUpdated(); + } + }); + connect(m_room, &NeoChatRoom::memberNameUpdated, this, [this](Quotient::RoomMember member) { + if (m_markerIds.contains(member.id())) { + const auto memberIndex = index(m_markerIds.indexOf(member.id())); + Q_EMIT dataChanged(memberIndex, memberIndex); + } + }); + connect(m_room, &NeoChatRoom::memberAvatarUpdated, this, [this](Quotient::RoomMember member) { + if (m_markerIds.contains(member.id())) { + const auto memberIndex = index(m_markerIds.indexOf(member.id())); + Q_EMIT dataChanged(memberIndex, memberIndex); + } + }); + + beginResetModel(); + auto userIds = m_room->userIdsAtEvent(m_eventId); + userIds.remove(m_room->localMember().id()); + m_markerIds = userIds.values(); + endResetModel(); + + Q_EMIT reactionUpdated(); +} + +QVariant ReadMarkerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= rowCount()) { + qDebug() << "ReactionModel, something's wrong: index.row() >= rowCount()"; + return {}; + } + + const auto member = m_room->member(m_markerIds.value(index.row())); + + if (role == DisplayNameRole) { + return member.htmlSafeDisplayName(); + } + + if (role == AvatarUrlRole) { + return member.avatarUrl(); + } + + if (role == ColorRole) { + return member.color(); + } + + return {}; +} + +int ReadMarkerModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return std::min(int(m_markerIds.size()), MAXMARKERS); +} + +QHash ReadMarkerModel::roleNames() const +{ + return { + {DisplayNameRole, "displayName"}, + {AvatarUrlRole, "avatarUrl"}, + {ColorRole, "memberColor"}, + }; +} + +QString ReadMarkerModel::readMarkersString() +{ + /** + * The string ends up in the form + * "x users: user1DisplayName, user2DisplayName, etc." + */ + QString readMarkersString = i18np("1 user: ", "%1 users: ", m_markerIds.size()); + for (const auto &memberId : m_markerIds) { + auto member = m_room->member(memberId); + QString displayName = member.htmlSafeDisambiguatedName(); + if (displayName.isEmpty()) { + displayName = i18nc("A member who is not in the room has been requested.", "unknown member"); + } + readMarkersString += displayName + i18nc("list separator", ", "); + } + readMarkersString.chop(2); + return readMarkersString; +} + +QString ReadMarkerModel::excessReadMarkersString() +{ + if (m_room == nullptr) { + return {}; + } + + if (m_markerIds.size() > MAXMARKERS) { + return QStringLiteral("+ ") + QString::number(m_markerIds.size() - MAXMARKERS); + } else { + return QString(); + } +} + +#include "moc_readmarkermodel.cpp" diff --git a/src/models/readmarkermodel.h b/src/models/readmarkermodel.h new file mode 100644 index 000000000..05b14590c --- /dev/null +++ b/src/models/readmarkermodel.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#include "neochatroom.h" + +/** + * @class ReadMarkerModel + * + * This class defines the model for visualising a list of reactions to an event. + */ +class ReadMarkerModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + /** + * @brief Returns a string with the names of the read markers at the event. + * + * This is in the form "x users: name 1, name 2, ...". + */ + Q_PROPERTY(QString readMarkersString READ readMarkersString NOTIFY reactionUpdated) + + /** + * @brief Returns the number of excess user read markers for the event. + * + * This returns a string in the form "+ x" ready for use in the UI. + */ + Q_PROPERTY(QString excessReadMarkersString READ excessReadMarkersString NOTIFY reactionUpdated) + +public: + /** + * @brief Defines the model roles. + */ + enum Roles { + DisplayNameRole = Qt::DisplayRole, /**< The display name of the member in the room. */ + AvatarUrlRole, /**< The avatar for the member in the room. */ + ColorRole, /**< The color for the member. */ + }; + + explicit ReadMarkerModel(const QString &eventId, NeoChatRoom *room); + + QString readMarkersString(); + QString excessReadMarkersString(); + + /** + * @brief Get the given role value at the given index. + * + * @sa QAbstractItemModel::data + */ + [[nodiscard]] QVariant data(const QModelIndex &index, 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 Roles, QAbstractItemModel::roleNames() + */ + [[nodiscard]] QHash roleNames() const override; + +Q_SIGNALS: + void reactionUpdated(); + +private: + QPointer m_room; + QString m_eventId; + QList m_markerIds; +}; diff --git a/src/timeline/AvatarFlow.qml b/src/timeline/AvatarFlow.qml index 2285f6cb4..212c2f58b 100644 --- a/src/timeline/AvatarFlow.qml +++ b/src/timeline/AvatarFlow.qml @@ -13,20 +13,21 @@ Flow { property var avatarSize: Kirigami.Units.iconSizes.small property alias model: avatarFlowRepeater.model property string toolTipText - property alias excessAvatars: excessAvatarsLabel.text spacing: -avatarSize / 2 Repeater { id: avatarFlowRepeater delegate: KirigamiComponents.Avatar { - required property var modelData + required property string displayName + required property url avatarUrl + required property color memberColor implicitWidth: root.avatarSize implicitHeight: root.avatarSize - name: modelData.displayName - source: modelData.avatarUrl - color: modelData.color + name: displayName + source: avatarUrl + color: memberColor } } QQC2.Label { @@ -34,6 +35,9 @@ Flow { visible: text !== "" color: Kirigami.Theme.textColor horizontalAlignment: Text.AlignHCenter + + text: root.model?.excessReadMarkersString ?? "" + background: Kirigami.ShadowedRectangle { color: Kirigami.Theme.backgroundColor Kirigami.Theme.inherit: false @@ -54,7 +58,7 @@ Flow { } } - QQC2.ToolTip.text: toolTipText + QQC2.ToolTip.text: root.model?.readMarkersString ?? "" QQC2.ToolTip.visible: hoverHandler.hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 7d014da60..a836b73ba 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -82,16 +82,6 @@ TimelineDelegate { */ required property var readMarkers - /** - * @brief String with the display name and matrix ID of the other user read markers. - */ - required property string readMarkersString - - /** - * @brief The number of other users at the event after the first 5. - */ - required property var excessReadMarkers - /** * @brief Whether the other user read marker component should be shown. */ @@ -342,8 +332,6 @@ TimelineDelegate { Layout.rightMargin: Kirigami.Units.largeSpacing visible: root.showReadMarkers model: root.readMarkers - toolTipText: root.readMarkersString - excessAvatars: root.excessReadMarkers } DelegateSizeHelper { diff --git a/src/timeline/StateDelegate.qml b/src/timeline/StateDelegate.qml index e2131e2b4..a984763e2 100644 --- a/src/timeline/StateDelegate.qml +++ b/src/timeline/StateDelegate.qml @@ -53,16 +53,6 @@ TimelineDelegate { */ required property var readMarkers - /** - * @brief String with the display name and matrix ID of the other user read markers. - */ - required property string readMarkersString - - /** - * @brief The number of other users at the event after the first 5. - */ - required property var excessReadMarkers - /** * @brief Whether the other user read marker component should be shown. */ @@ -197,8 +187,6 @@ TimelineDelegate { Layout.alignment: Qt.AlignRight visible: root.showReadMarkers model: root.readMarkers - toolTipText: root.readMarkersString - excessAvatars: root.excessReadMarkers } }