Reaction Model
Create a reaction model that provides all the required data for `ReactionDelegate` so that none need to be calculated in QML. This also cleans up the API for `ReactionDelegate`
This commit is contained in:
@@ -54,6 +54,7 @@ add_library(neochat STATIC
|
||||
events/imagepackevent.cpp
|
||||
events/joinrulesevent.cpp
|
||||
events/stickerevent.cpp
|
||||
models/reactionmodel.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include "models/messageeventmodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/publicroomlistmodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "models/roomlistmodel.h"
|
||||
#include "models/searchmodel.h"
|
||||
#include "models/serverlistmodel.h"
|
||||
@@ -230,6 +231,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
|
||||
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatuser.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
@@ -61,6 +62,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ReadMarkersStringRole] = "readMarkersString";
|
||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[ShowReactionsRole] = "showReactions";
|
||||
roles[SourceRole] = "source";
|
||||
roles[MimeTypeRole] = "mimeType";
|
||||
roles[FormattedBodyRole] = "formattedBody";
|
||||
@@ -106,6 +108,8 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
m_linkPreviewers.clear();
|
||||
qDeleteAll(m_reactionModels);
|
||||
m_reactionModels.clear();
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
@@ -116,6 +120,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
|
||||
createLinkPreviewerForEvent(e);
|
||||
createReactionModelForEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +139,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message != nullptr) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
|
||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||
QString planBody = message->plainBody();
|
||||
@@ -173,6 +179,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
}
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
@@ -244,7 +251,11 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (eventId.isEmpty()) { // How did we get here?
|
||||
return;
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
||||
if (eventIt != m_currentRoom->historyEdge()) {
|
||||
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
@@ -932,38 +943,17 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||
if (annotations.isEmpty()) {
|
||||
return {};
|
||||
};
|
||||
QMap<QString, QList<NeoChatUser *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (auto e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
}
|
||||
if (m_reactionModels.contains(evt.id())) {
|
||||
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
|
||||
} else {
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariantList res = {};
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (auto author : i.value()) {
|
||||
authors.append(userInContext(author, m_currentRoom));
|
||||
}
|
||||
bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser()));
|
||||
res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
|
||||
++i;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
if (role == ShowReactionsRole) {
|
||||
return m_reactionModels.contains(evt.id());
|
||||
}
|
||||
|
||||
if (role == AuthorIdRole) {
|
||||
return evt.senderId();
|
||||
}
|
||||
@@ -1278,3 +1268,60 @@ void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto eventId = event->id();
|
||||
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::Annotation());
|
||||
if (annotations.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
QMap<QString, QList<NeoChatUser *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QList<ReactionModel::Reaction> res;
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(userInContext(author, m_currentRoom));
|
||||
}
|
||||
|
||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
m_reactionModels[eventId]->setReactions(res);
|
||||
} else if (res.size() > 0) {
|
||||
m_reactionModels[eventId] = new ReactionModel(this, res, static_cast<NeoChatUser *>(m_currentRoom->localUser()));
|
||||
} else {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class MessageEventModel
|
||||
*
|
||||
@@ -90,7 +92,8 @@ public:
|
||||
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 of reactions to this event. */
|
||||
ReactionRole, /**< List model for this event. */
|
||||
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
|
||||
SourceRole, /**< The full message source JSON. */
|
||||
|
||||
// For debugging
|
||||
@@ -186,6 +189,7 @@ private:
|
||||
bool movingEvent = false;
|
||||
|
||||
QMap<QString, LinkPreviewer *> m_linkPreviewers;
|
||||
QMap<QString, ReactionModel *> m_reactionModels;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
||||
@@ -203,6 +207,7 @@ private:
|
||||
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<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||
|
||||
110
src/models/reactionmodel.cpp
Normal file
110
src/models/reactionmodel.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2023 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 "reactionmodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "neochatuser.h"
|
||||
|
||||
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, NeoChatUser *localUser)
|
||||
: QAbstractListModel(parent)
|
||||
, m_localUser(localUser)
|
||||
{
|
||||
setReactions(reactions);
|
||||
}
|
||||
|
||||
QVariant ReactionModel::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 &reaction = m_reactions.at(index.row());
|
||||
|
||||
if (role == TextRole) {
|
||||
if (reaction.authors.count() > 1) {
|
||||
return reaction.reaction + QStringLiteral(" %1").arg(reaction.authors.count());
|
||||
} else {
|
||||
return reaction.reaction;
|
||||
}
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
return reaction.reaction;
|
||||
}
|
||||
|
||||
if (role == ToolTipRole) {
|
||||
QString text;
|
||||
|
||||
for (int i = 0; i < reaction.authors.count() && i < 3; i++) {
|
||||
if (i != 0) {
|
||||
if (i < reaction.authors.count() - 1) {
|
||||
text += QStringLiteral(", ");
|
||||
} else {
|
||||
text += i18nc("Separate the usernames of users", " and ");
|
||||
}
|
||||
}
|
||||
text += reaction.authors.at(i).toMap()["displayName"].toString();
|
||||
}
|
||||
|
||||
if (reaction.authors.count() > 3) {
|
||||
text += i18ncp("%1 is the number of other users", " and %1 other", " and %1 others", reaction.authors.count() - 3);
|
||||
}
|
||||
|
||||
text = i18ncp("%2 is the users who reacted and %3 the emoji that was given",
|
||||
"%2 reacted with %3",
|
||||
"%2 reacted with %3",
|
||||
reaction.authors.count(),
|
||||
text,
|
||||
reaction.reaction);
|
||||
return text;
|
||||
}
|
||||
|
||||
if (role == AuthorsRole) {
|
||||
return reaction.authors;
|
||||
}
|
||||
|
||||
if (role == HasLocalUser) {
|
||||
for (auto author : reaction.authors) {
|
||||
if (author.toMap()["id"] == m_localUser->id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int ReactionModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_reactions.count();
|
||||
}
|
||||
|
||||
void ReactionModel::setReactions(QList<Reaction> reactions)
|
||||
{
|
||||
beginResetModel();
|
||||
m_reactions.clear();
|
||||
m_reactions = reactions;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ReactionModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TextRole, "text"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ToolTipRole, "toolTip"},
|
||||
{AuthorsRole, "authors"},
|
||||
{HasLocalUser, "hasLocalUser"},
|
||||
};
|
||||
}
|
||||
72
src/models/reactionmodel.h
Normal file
72
src/models/reactionmodel.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2023 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>
|
||||
|
||||
class NeoChatUser;
|
||||
|
||||
/**
|
||||
* @class ReactionModel
|
||||
*
|
||||
* This class defines the model for visualising a list of reactions to an event.
|
||||
*/
|
||||
class ReactionModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Definition of an reaction.
|
||||
*/
|
||||
struct Reaction {
|
||||
QString reaction; /**< The reaction emoji. */
|
||||
QVariantList authors; /**< The list of authors who sent the given reaction. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
TextRole = Qt::DisplayRole, /**< The text to show in the reaction. */
|
||||
ReactionRole, /**< The reaction emoji. */
|
||||
ToolTipRole, /**< The tool tip to show for the reaction. */
|
||||
AuthorsRole, /**< The list of authors who sent the given reaction. */
|
||||
HasLocalUser, /**< Whether the local user is in the list of authors. */
|
||||
};
|
||||
|
||||
ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, NeoChatUser *localUser = nullptr);
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* @brief Set the reactions data in the model.
|
||||
*/
|
||||
void setReactions(QList<Reaction> reactions);
|
||||
|
||||
private:
|
||||
QList<Reaction> m_reactions;
|
||||
|
||||
NeoChatUser *m_localUser;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ReactionModel *)
|
||||
@@ -4,67 +4,64 @@
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The reaction model to get the reactions from.
|
||||
*/
|
||||
property alias model: reactionRepeater.model
|
||||
|
||||
/**
|
||||
* @brief The given reaction has been clicked.
|
||||
*
|
||||
* Thrown when one of the reaction buttons in the flow is clicked.
|
||||
*/
|
||||
signal reactionClicked(string reaction)
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
model: reaction ?? null
|
||||
id: reactionRepeater
|
||||
model: root.model
|
||||
|
||||
delegate: QQC2.AbstractButton {
|
||||
width: Math.max(implicitWidth, height)
|
||||
width: Math.max(reactionTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 4, height)
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
id: reactionLabel
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData.reaction + " " + modelData.count
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: model.text
|
||||
|
||||
TextMetrics {
|
||||
id: reactionTextMetrics
|
||||
text: reactionLabel.text
|
||||
}
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
color: model.hasLocalUser ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
radius: height / 2
|
||||
shadow.size: Kirigami.Units.smallSpacing
|
||||
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
shadow.color: !model.hasLocalUser ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
checkable: true
|
||||
|
||||
checked: modelData.hasLocalUser
|
||||
|
||||
onToggled: currentRoom.toggleReaction(eventId, modelData.reaction)
|
||||
onClicked: reactionClicked(model.reaction)
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: {
|
||||
var text = "";
|
||||
|
||||
for (var i = 0; i < modelData.authors.length && i < 3; i++) {
|
||||
if (i !== 0) {
|
||||
if (i < modelData.authors.length - 1) {
|
||||
text += ", "
|
||||
} else {
|
||||
text += i18nc("Separate the usernames of users", " and ")
|
||||
}
|
||||
}
|
||||
text += currentRoom.htmlSafeMemberName(modelData.authors[i].id)
|
||||
}
|
||||
if (modelData.authors.length > 3) {
|
||||
text += i18ncp("%1 is the number of other users", " and %1 other", " and %1 others", modelData.authors.length - 3)
|
||||
}
|
||||
|
||||
text = i18ncp("%2 is the users who reacted and %3 the emoji that was given", "%2 reacted with %3", "%2 reacted with %3", modelData.authors.length, text, modelData.reaction)
|
||||
|
||||
return text
|
||||
}
|
||||
QQC2.ToolTip.text: model.toolTip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,10 @@ ColumnLayout {
|
||||
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
|
||||
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
|
||||
|
||||
visible: delegateType !== MessageEventModel.State && delegateType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
|
||||
visible: showReactions
|
||||
model: reaction
|
||||
|
||||
onReactionClicked: (reaction) => currentRoom.toggleReaction(eventId, reaction)
|
||||
}
|
||||
AvatarFlow {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
Reference in New Issue
Block a user