File indexing completed on 2024-05-05 05:01:24
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\">%3</a> ") 0131 .arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(), 0132 uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped()); 0133 0134 QString chunksText; 0135 chunksText += chunks.takeFirst(); 0136 if (chunks.size() > 0) { 0137 while (chunks.size() > 1) { 0138 chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", "); 0139 chunksText += chunks.takeFirst(); 0140 } 0141 chunksText += 0142 uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and "); 0143 chunksText += chunks.takeFirst(); 0144 } 0145 return i18nc( 0146 "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." 0147 "chunksText (%2) is a list of comma separated actions for each of the state events in the group.", 0148 "<style>a {text-decoration: none;}</style>%1 %2", 0149 userText, 0150 chunksText); 0151 } else { 0152 return {}; 0153 } 0154 } 0155 0156 QVariantList MessageFilterModel::stateEventsList(int sourceRow) const 0157 { 0158 QVariantList stateEvents; 0159 for (int i = sourceRow; i >= 0; i--) { 0160 auto nextState = QVariantMap{ 0161 {QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)}, 0162 {QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()}, 0163 {QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()}, 0164 }; 0165 stateEvents.append(nextState); 0166 if (i > 0 0167 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0168 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0169 )) { 0170 break; 0171 } 0172 } 0173 return stateEvents; 0174 } 0175 0176 QVariantList MessageFilterModel::authorList(int sourceRow) const 0177 { 0178 QVariantList uniqueAuthors; 0179 for (int i = sourceRow; i >= 0; i--) { 0180 QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); 0181 if (!uniqueAuthors.contains(nextAvatar)) { 0182 uniqueAuthors.append(nextAvatar); 0183 } 0184 if (i > 0 0185 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0186 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0187 )) { 0188 break; 0189 } 0190 } 0191 0192 if (uniqueAuthors.count() > 5) { 0193 uniqueAuthors = uniqueAuthors.mid(0, 5); 0194 } 0195 return uniqueAuthors; 0196 } 0197 0198 QString MessageFilterModel::excessAuthors(int row) const 0199 { 0200 QVariantList uniqueAuthors; 0201 for (int i = row; i >= 0; i--) { 0202 QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); 0203 if (!uniqueAuthors.contains(nextAvatar)) { 0204 uniqueAuthors.append(nextAvatar); 0205 } 0206 if (i > 0 0207 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event 0208 || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible 0209 )) { 0210 break; 0211 } 0212 } 0213 0214 int excessAuthors; 0215 if (uniqueAuthors.count() > 5) { 0216 excessAuthors = uniqueAuthors.count() - 5; 0217 } else { 0218 excessAuthors = 0; 0219 } 0220 QString excessAuthorsString; 0221 if (excessAuthors == 0) { 0222 return QString(); 0223 } else { 0224 return QStringLiteral("+ %1").arg(excessAuthors); 0225 } 0226 } 0227 0228 #include "moc_messagefiltermodel.cpp"