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"