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"