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"