File indexing completed on 2024-12-01 07:40:35

0001 // SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
0002 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003 
0004 #include "reactionmodel.h"
0005 #include "neochatroom.h"
0006 
0007 #include <QDebug>
0008 #ifdef HAVE_ICU
0009 #include <QTextBoundaryFinder>
0010 #include <QTextCharFormat>
0011 #include <unicode/uchar.h>
0012 #include <unicode/urename.h>
0013 #endif
0014 
0015 #include <KLocalizedString>
0016 
0017 #include <Quotient/user.h>
0018 
0019 ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room)
0020     : QAbstractListModel(nullptr)
0021     , m_room(room)
0022     , m_event(event)
0023 {
0024     if (m_event != nullptr && m_room != nullptr) {
0025         connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) {
0026             if (m_event->id() == eventId) {
0027                 updateReactions();
0028             }
0029         });
0030 
0031         updateReactions();
0032     }
0033 }
0034 
0035 QVariant ReactionModel::data(const QModelIndex &index, int role) const
0036 {
0037     if (!index.isValid()) {
0038         return {};
0039     }
0040 
0041     if (index.row() >= rowCount()) {
0042         qDebug() << "ReactionModel, something's wrong: index.row() >= rowCount()";
0043         return {};
0044     }
0045 
0046     const auto &reaction = m_reactions.at(index.row());
0047 
0048     if (role == TextContentRole) {
0049         if (reaction.authors.count() > 1) {
0050             return QStringLiteral("%1  %2").arg(reactionText(reaction.reaction), QString::number(reaction.authors.count()));
0051         } else {
0052             return reactionText(reaction.reaction);
0053         }
0054     }
0055 
0056     if (role == ReactionRole) {
0057         return reaction.reaction;
0058     }
0059 
0060     if (role == ToolTipRole) {
0061         QString text;
0062 
0063         for (int i = 0; i < reaction.authors.count() && i < 3; i++) {
0064             if (i != 0) {
0065                 if (i < reaction.authors.count() - 1) {
0066                     text += QStringLiteral(", ");
0067                 } else {
0068                     text += i18nc("Separate the usernames of users", " and ");
0069                 }
0070             }
0071             text += reaction.authors.at(i).toMap()[QStringLiteral("displayName")].toString();
0072         }
0073 
0074         if (reaction.authors.count() > 3) {
0075             text += i18ncp("%1 is the number of other users", " and %1 other", " and %1 others", reaction.authors.count() - 3);
0076         }
0077 
0078         text = i18ncp("%2 is the users who reacted and %3 the emoji that was given",
0079                       "%2 reacted with %3",
0080                       "%2 reacted with %3",
0081                       reaction.authors.count(),
0082                       text,
0083                       reactionText(reaction.reaction));
0084         return text;
0085     }
0086 
0087     if (role == AuthorsRole) {
0088         return reaction.authors;
0089     }
0090 
0091     if (role == HasLocalUser) {
0092         for (auto author : reaction.authors) {
0093             if (author.toMap()[QStringLiteral("id")] == m_room->localUser()->id()) {
0094                 return true;
0095             }
0096         }
0097         return false;
0098     }
0099 
0100     return {};
0101 }
0102 
0103 int ReactionModel::rowCount(const QModelIndex &parent) const
0104 {
0105     Q_UNUSED(parent)
0106     return m_reactions.count();
0107 }
0108 
0109 void ReactionModel::updateReactions()
0110 {
0111     beginResetModel();
0112 
0113     m_reactions.clear();
0114 
0115     const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType);
0116     if (annotations.isEmpty()) {
0117         endResetModel();
0118         return;
0119     };
0120 
0121     QMap<QString, QList<Quotient::User *>> reactions = {};
0122     for (const auto &a : annotations) {
0123         if (a->isRedacted()) { // Just in case?
0124             continue;
0125         }
0126         if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) {
0127             reactions[e->key()].append(m_room->user(e->senderId()));
0128         }
0129     }
0130 
0131     if (reactions.isEmpty()) {
0132         endResetModel();
0133         return;
0134     }
0135 
0136     auto i = reactions.constBegin();
0137     while (i != reactions.constEnd()) {
0138         QVariantList authors;
0139         for (const auto &author : i.value()) {
0140             authors.append(m_room->getUser(author));
0141         }
0142 
0143         m_reactions.append(ReactionModel::Reaction{i.key(), authors});
0144         ++i;
0145     }
0146 
0147     endResetModel();
0148 }
0149 
0150 QHash<int, QByteArray> ReactionModel::roleNames() const
0151 {
0152     return {
0153         {TextContentRole, "textContent"},
0154         {ReactionRole, "reaction"},
0155         {ToolTipRole, "toolTip"},
0156         {AuthorsRole, "authors"},
0157         {HasLocalUser, "hasLocalUser"},
0158     };
0159 }
0160 
0161 QString ReactionModel::reactionText(const QString &text)
0162 {
0163     const auto isEmoji = [](const QString &text) {
0164 #ifdef HAVE_ICU
0165         QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
0166         int from = 0;
0167         while (finder.toNextBoundary() != -1) {
0168             auto to = finder.position();
0169             if (text[from].isSpace()) {
0170                 from = to;
0171                 continue;
0172             }
0173 
0174             auto first = text.mid(from, to - from).toUcs4()[0];
0175             if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
0176                 return false;
0177             }
0178             from = to;
0179         }
0180         return true;
0181 #else
0182         return false;
0183 #endif
0184     };
0185 
0186     return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
0187 }
0188 
0189 #include "moc_reactionmodel.cpp"