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.
This commit is contained in:
@@ -75,8 +75,6 @@ private Q_SLOTS:
|
|||||||
void nullThread();
|
void nullThread();
|
||||||
void location();
|
void location();
|
||||||
void nullLocation();
|
void nullLocation();
|
||||||
void readMarkers();
|
|
||||||
void nullReadMarkers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void EventHandlerTest::initTestCase()
|
void EventHandlerTest::initTestCase()
|
||||||
@@ -521,59 +519,5 @@ void EventHandlerTest::nullLocation()
|
|||||||
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
|
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<Quotient::RoomMember>());
|
|
||||||
|
|
||||||
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<Quotient::RoomMember>());
|
|
||||||
|
|
||||||
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)
|
QTEST_MAIN(EventHandlerTest)
|
||||||
#include "eventhandlertest.moc"
|
#include "eventhandlertest.moc"
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ add_library(neochat STATIC
|
|||||||
models/permissionsmodel.h
|
models/permissionsmodel.h
|
||||||
threepidbindhelper.cpp
|
threepidbindhelper.cpp
|
||||||
threepidbindhelper.h
|
threepidbindhelper.h
|
||||||
|
models/readmarkermodel.cpp
|
||||||
|
models/readmarkermodel.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||||
|
|||||||
@@ -961,102 +961,4 @@ QString EventHandler::getLocationAssetType() const
|
|||||||
return assetType;
|
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<Quotient::RoomMember> 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<Quotient::RoomMember> 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"
|
#include "moc_eventhandler.cpp"
|
||||||
|
|||||||
@@ -342,43 +342,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
QString getLocationAssetType() const;
|
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<Quotient::RoomMember> 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:
|
private:
|
||||||
const NeoChatRoom *m_room = nullptr;
|
const NeoChatRoom *m_room = nullptr;
|
||||||
const Quotient::RoomEvent *m_event = nullptr;
|
const Quotient::RoomEvent *m_event = nullptr;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "messagecontentmodel.h"
|
#include "messagecontentmodel.h"
|
||||||
#include "models/messagefiltermodel.h"
|
#include "models/messagefiltermodel.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
|
#include "readmarkermodel.h"
|
||||||
#include "texthandler.h"
|
#include "texthandler.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
@@ -45,8 +46,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[ThreadRootRole] = "threadRoot";
|
roles[ThreadRootRole] = "threadRoot";
|
||||||
roles[ShowSectionRole] = "showSection";
|
roles[ShowSectionRole] = "showSection";
|
||||||
roles[ReadMarkersRole] = "readMarkers";
|
roles[ReadMarkersRole] = "readMarkers";
|
||||||
roles[ExcessReadMarkersRole] = "excessReadMarkers";
|
|
||||||
roles[ReadMarkersStringRole] = "readMarkersString";
|
|
||||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||||
roles[ReactionRole] = "reaction";
|
roles[ReactionRole] = "reaction";
|
||||||
roles[ShowReactionsRole] = "showReactions";
|
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
|
// HACK: Reset the model to a null room first to make sure QML dismantles
|
||||||
// last room's objects before the room is actually changed
|
// last room's objects before the room is actually changed
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
m_readMarkerModels.clear();
|
||||||
m_currentRoom->disconnect(this);
|
m_currentRoom->disconnect(this);
|
||||||
m_currentRoom = nullptr;
|
m_currentRoom = nullptr;
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -100,9 +100,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
room->setDisplayed();
|
room->setDisplayed();
|
||||||
|
|
||||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||||
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
|
createEventObjects(&*event->viewAs<RoomEvent>());
|
||||||
createEventObjects(roomMessageEvent);
|
|
||||||
}
|
|
||||||
if (event->event()->is<PollStartEvent>()) {
|
if (event->event()->is<PollStartEvent>()) {
|
||||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
|
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
|
||||||
}
|
}
|
||||||
@@ -115,11 +113,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
|
|
||||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||||
for (auto &&event : events) {
|
for (auto &&event : events) {
|
||||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
createEventObjects(event.get());
|
||||||
|
|
||||||
if (message != nullptr) {
|
|
||||||
createEventObjects(message);
|
|
||||||
}
|
|
||||||
if (event->is<PollStartEvent>()) {
|
if (event->is<PollStartEvent>()) {
|
||||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
||||||
}
|
}
|
||||||
@@ -129,9 +123,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||||
for (auto &event : events) {
|
for (auto &event : events) {
|
||||||
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
|
createEventObjects(event.get());
|
||||||
createEventObjects(roomMessageEvent);
|
|
||||||
}
|
|
||||||
if (event->is<PollStartEvent>()) {
|
if (event->is<PollStartEvent>()) {
|
||||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
|
||||||
}
|
}
|
||||||
@@ -195,10 +187,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
moveReadMarker(toEventId);
|
moveReadMarker(toEventId);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||||
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
|
createEventObjects(newEvent);
|
||||||
if (message != nullptr) {
|
|
||||||
createEventObjects(message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
|
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
|
||||||
if (eventId.isEmpty()) { // How did we get here?
|
if (eventId.isEmpty()) { // How did we get here?
|
||||||
@@ -206,9 +195,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
||||||
if (eventIt != m_currentRoom->historyEdge()) {
|
if (eventIt != m_currentRoom->historyEdge()) {
|
||||||
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
|
createEventObjects(eventIt->event());
|
||||||
createEventObjects(event);
|
|
||||||
}
|
|
||||||
if (eventIt->event()->is<PollStartEvent>()) {
|
if (eventIt->event()->is<PollStartEvent>()) {
|
||||||
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
|
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
|
||||||
}
|
}
|
||||||
@@ -216,11 +203,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
refreshEventRoles(eventId, {Qt::DisplayRole});
|
refreshEventRoles(eventId, {Qt::DisplayRole});
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) {
|
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
|
// this is slow
|
||||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||||
auto event = it->event();
|
createEventObjects(it->event());
|
||||||
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -557,19 +543,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReadMarkersRole) {
|
if (role == ReadMarkersRole) {
|
||||||
return QVariant::fromValue(eventHandler.getReadMarkers());
|
if (m_readMarkerModels.contains(evt.id())) {
|
||||||
}
|
return QVariant::fromValue<ReadMarkerModel *>(m_readMarkerModels[evt.id()].get());
|
||||||
|
} else {
|
||||||
if (role == ExcessReadMarkersRole) {
|
return QVariantList();
|
||||||
return eventHandler.getNumberExcessReadMarkers();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReadMarkersStringRole) {
|
|
||||||
return eventHandler.getReadMarkersString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ShowReadMarkersRole) {
|
if (role == ShowReadMarkersRole) {
|
||||||
return eventHandler.hasReadMarkers();
|
return m_readMarkerModels.contains(evt.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReactionRole) {
|
if (role == ReactionRole) {
|
||||||
@@ -630,30 +612,61 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
|
|||||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
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();
|
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.
|
// 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 a model already exists but now has no reactions remove it
|
||||||
if (m_reactionModels[eventId]->rowCount() <= 0) {
|
if (m_readMarkerModels[eventId]->rowCount() <= 0) {
|
||||||
m_reactionModels.remove(eventId);
|
m_readMarkerModels.remove(eventId);
|
||||||
if (!resetting) {
|
if (!resetting) {
|
||||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
|
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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.
|
// If a model doesn't exist and there are reactions add it.
|
||||||
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(event, m_currentRoom));
|
auto newModel = QSharedPointer<ReadMarkerModel>(new ReadMarkerModel(eventId, m_currentRoom));
|
||||||
if (reactionModel->rowCount() > 0) {
|
if (newModel->rowCount() > 0) {
|
||||||
m_reactionModels[eventId] = reactionModel;
|
m_readMarkerModels[eventId] = newModel;
|
||||||
|
if (!resetting) {
|
||||||
|
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto roomEvent = eventCast<const RoomMessageEvent>(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) {
|
if (!resetting) {
|
||||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
|
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<ReactionModel>(new ReactionModel(roomEvent, m_currentRoom));
|
||||||
|
if (reactionModel->rowCount() > 0) {
|
||||||
|
m_reactionModels[eventId] = reactionModel;
|
||||||
|
if (!resetting) {
|
||||||
|
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "pollhandler.h"
|
#include "pollhandler.h"
|
||||||
|
#include "readmarkermodel.h"
|
||||||
|
|
||||||
class ReactionModel;
|
class ReactionModel;
|
||||||
|
|
||||||
@@ -58,8 +59,6 @@ public:
|
|||||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||||
|
|
||||||
ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */
|
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. */
|
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
|
||||||
ReactionRole, /**< List model for this event. */
|
ReactionRole, /**< List model for this event. */
|
||||||
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
|
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
|
||||||
@@ -116,6 +115,7 @@ private:
|
|||||||
bool movingEvent = false;
|
bool movingEvent = false;
|
||||||
KFormat m_format;
|
KFormat m_format;
|
||||||
|
|
||||||
|
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
|
||||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||||
|
|
||||||
[[nodiscard]] int timelineBaseIndex() const;
|
[[nodiscard]] int timelineBaseIndex() const;
|
||||||
@@ -130,7 +130,7 @@ private:
|
|||||||
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
|
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
|
||||||
void moveReadMarker(const QString &toEventId);
|
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
|
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
|
|
||||||
|
|||||||
136
src/models/readmarkermodel.cpp
Normal file
136
src/models/readmarkermodel.cpp
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 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 "readmarkermodel.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include <Quotient/roommember.h>
|
||||||
|
|
||||||
|
#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<int, QByteArray> 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"
|
||||||
79
src/models/readmarkermodel.h
Normal file
79
src/models/readmarkermodel.h
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 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 <QAbstractListModel>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#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<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void reactionUpdated();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
QString m_eventId;
|
||||||
|
QList<QString> m_markerIds;
|
||||||
|
};
|
||||||
@@ -13,20 +13,21 @@ Flow {
|
|||||||
property var avatarSize: Kirigami.Units.iconSizes.small
|
property var avatarSize: Kirigami.Units.iconSizes.small
|
||||||
property alias model: avatarFlowRepeater.model
|
property alias model: avatarFlowRepeater.model
|
||||||
property string toolTipText
|
property string toolTipText
|
||||||
property alias excessAvatars: excessAvatarsLabel.text
|
|
||||||
|
|
||||||
spacing: -avatarSize / 2
|
spacing: -avatarSize / 2
|
||||||
Repeater {
|
Repeater {
|
||||||
id: avatarFlowRepeater
|
id: avatarFlowRepeater
|
||||||
delegate: KirigamiComponents.Avatar {
|
delegate: KirigamiComponents.Avatar {
|
||||||
required property var modelData
|
required property string displayName
|
||||||
|
required property url avatarUrl
|
||||||
|
required property color memberColor
|
||||||
|
|
||||||
implicitWidth: root.avatarSize
|
implicitWidth: root.avatarSize
|
||||||
implicitHeight: root.avatarSize
|
implicitHeight: root.avatarSize
|
||||||
|
|
||||||
name: modelData.displayName
|
name: displayName
|
||||||
source: modelData.avatarUrl
|
source: avatarUrl
|
||||||
color: modelData.color
|
color: memberColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
@@ -34,6 +35,9 @@ Flow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
|
text: root.model?.excessReadMarkersString ?? ""
|
||||||
|
|
||||||
background: Kirigami.ShadowedRectangle {
|
background: Kirigami.ShadowedRectangle {
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
Kirigami.Theme.inherit: false
|
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.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
|||||||
@@ -82,16 +82,6 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
required property var readMarkers
|
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.
|
* @brief Whether the other user read marker component should be shown.
|
||||||
*/
|
*/
|
||||||
@@ -342,8 +332,6 @@ TimelineDelegate {
|
|||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
visible: root.showReadMarkers
|
visible: root.showReadMarkers
|
||||||
model: root.readMarkers
|
model: root.readMarkers
|
||||||
toolTipText: root.readMarkersString
|
|
||||||
excessAvatars: root.excessReadMarkers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateSizeHelper {
|
DelegateSizeHelper {
|
||||||
|
|||||||
@@ -53,16 +53,6 @@ TimelineDelegate {
|
|||||||
*/
|
*/
|
||||||
required property var readMarkers
|
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.
|
* @brief Whether the other user read marker component should be shown.
|
||||||
*/
|
*/
|
||||||
@@ -197,8 +187,6 @@ TimelineDelegate {
|
|||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
visible: root.showReadMarkers
|
visible: root.showReadMarkers
|
||||||
model: root.readMarkers
|
model: root.readMarkers
|
||||||
toolTipText: root.readMarkersString
|
|
||||||
excessAvatars: root.excessReadMarkers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user