File indexing completed on 2024-12-08 07:33:45
0001 // SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de> 0002 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0003 0004 #include "messagefiltermodel.h" 0005 0006 #include <KLocalizedString> 0007 0008 #include "enums/delegatetype.h" 0009 #include "messageeventmodel.h" 0010 #include "neochatconfig.h" 0011 #include "timelinemodel.h" 0012 0013 using namespace Quotient; 0014 0015 MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel) 0016 : QSortFilterProxyModel(parent) 0017 { 0018 Q_ASSERT(sourceModel); 0019 setSourceModel(sourceModel); 0020 0021 connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] { 0022 invalidateFilter(); 0023 }); 0024 connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] { 0025 invalidateFilter(); 0026 }); 0027 connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] { 0028 invalidateFilter(); 0029 }); 0030 connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] { 0031 invalidateFilter(); 0032 }); 0033 connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] { 0034 invalidateFilter(); 0035 }); 0036 } 0037 0038 bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const 0039 { 0040 const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); 0041 0042 // Don't show redacted (i.e. deleted) messages. 0043 if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) { 0044 return false; 0045 } 0046 0047 // Don't show hidden or replaced messages. 0048 const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt(); 0049 if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) { 0050 return false; 0051 } 0052 0053 // Don't show events with an unknown type. 0054 const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt(); 0055 if (eventType == DelegateType::Other) { 0056 return false; 0057 } 0058 0059 // Don't show state events that are not the first in a consecutive group on the 0060 // same day as they will be grouped as a single delegate. 0061 const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1; 0062 const bool previousEventIsState = notLastRow 0063 ? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State 0064 : false; 0065 const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool(); 0066 if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) { 0067 return false; 0068 } 0069 0070 return true; 0071 } 0072 0073 QVariant MessageFilterModel::data(const QModelIndex &index, int role) const 0074 { 0075 if (role == AggregateDisplayRole) { 0076 return aggregateEventToString(mapToSource(index).row()); 0077 } else if (role == StateEventsRole) { 0078 return stateEventsList(mapToSource(index).row()); 0079 } else if (role == AuthorListRole) { 0080 return authorList(mapToSource(index).row()); 0081 } else if (role == ExcessAuthorsRole) { 0082 return excessAuthors(mapToSource(index).row()); 0083 } 0084 return sourceModel()->data(mapToSource(index), role); 0085 } 0086 0087 QHash<int, QByteArray> MessageFilterModel::roleNames() const 0088 { 0089 auto roles = sourceModel() ? sourceModel()->roleNames() : QHash<int, QByteArray>(); 0090 roles[AggregateDisplayRole] = "aggregateDisplay"; 0091 roles[StateEventsRole] = "stateEvents"; 0092 roles[AuthorListRole] = "authorList"; 0093 roles[ExcessAuthorsRole] = "excessAuthors"; 0094 return roles; 0095 } 0096 0097 QString MessageFilterModel::aggregateEventToString(int sourceRow) const 0098 { 0099 QStringList parts; 0100 QVariantList uniqueAuthors; 0101 for (int i = sourceRow; i >= 0; i--) { 0102 parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString(); 0103 QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); 0104 if (!uniqueAuthors.contains(nextAuthor)) { 0105 uniqueAuthors.append(nextAuthor); 0106 } 0107 if (i > 0 0108 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0109 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0110 )) { 0111 break; 0112 } 0113 } 0114 parts.sort(); // Sort them so that all identical events can be collected. 0115 if (!parts.isEmpty()) { 0116 QStringList chunks; 0117 while (!parts.isEmpty()) { 0118 chunks += QString(); 0119 int count = 1; 0120 auto part = parts.takeFirst(); 0121 while (!parts.isEmpty() && parts.first() == part) { 0122 parts.removeFirst(); 0123 count++; 0124 } 0125 chunks.last() += i18ncp("%1: What's being done; %2: How often it is done.", " %1", " %1 %2 times", part, count); 0126 } 0127 chunks.removeDuplicates(); 0128 // The author text is either "n users" if > 1 user or the matrix.to link to a single user. 0129 QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length()) 0130 : QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ") 0131 .arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(), 0132 uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(), 0133 uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped()); 0134 0135 QString chunksText; 0136 chunksText += chunks.takeFirst(); 0137 if (chunks.size() > 0) { 0138 while (chunks.size() > 1) { 0139 chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", "); 0140 chunksText += chunks.takeFirst(); 0141 } 0142 chunksText += 0143 uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and "); 0144 chunksText += chunks.takeFirst(); 0145 } 0146 return i18nc( 0147 "userText (%1) is either a Matrix username if a single user sent all the states or n users if they were sent by multiple users." 0148 "chunksText (%2) is a list of comma separated actions for each of the state events in the group.", 0149 "<style>a {text-decoration: none;}</style>%1 %2", 0150 userText, 0151 chunksText); 0152 } else { 0153 return {}; 0154 } 0155 } 0156 0157 QVariantList MessageFilterModel::stateEventsList(int sourceRow) const 0158 { 0159 QVariantList stateEvents; 0160 for (int i = sourceRow; i >= 0; i--) { 0161 auto nextState = QVariantMap{ 0162 {QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)}, 0163 {QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()}, 0164 {QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()}, 0165 }; 0166 stateEvents.append(nextState); 0167 if (i > 0 0168 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0169 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0170 )) { 0171 break; 0172 } 0173 } 0174 return stateEvents; 0175 } 0176 0177 QVariantList MessageFilterModel::authorList(int sourceRow) const 0178 { 0179 QVariantList uniqueAuthors; 0180 for (int i = sourceRow; i >= 0; i--) { 0181 QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); 0182 if (!uniqueAuthors.contains(nextAvatar)) { 0183 uniqueAuthors.append(nextAvatar); 0184 } 0185 if (i > 0 0186 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0187 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0188 )) { 0189 break; 0190 } 0191 } 0192 0193 if (uniqueAuthors.count() > 5) { 0194 uniqueAuthors = uniqueAuthors.mid(0, 5); 0195 } 0196 return uniqueAuthors; 0197 } 0198 0199 QString MessageFilterModel::excessAuthors(int row) const 0200 { 0201 QVariantList uniqueAuthors; 0202 for (int i = row; i >= 0; i--) { 0203 QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); 0204 if (!uniqueAuthors.contains(nextAvatar)) { 0205 uniqueAuthors.append(nextAvatar); 0206 } 0207 if (i > 0 0208 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0209 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0210 )) { 0211 break; 0212 } 0213 } 0214 0215 int excessAuthors; 0216 if (uniqueAuthors.count() > 5) { 0217 excessAuthors = uniqueAuthors.count() - 5; 0218 } else { 0219 excessAuthors = 0; 0220 } 0221 QString excessAuthorsString; 0222 if (excessAuthors == 0) { 0223 return QString(); 0224 } else { 0225 return QStringLiteral("+ %1").arg(excessAuthors); 0226 } 0227 } 0228 0229 #include "moc_messagefiltermodel.cpp"