// SPDX-FileCopyrightText: 2021 Nicolas Fella // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL #include "messagefiltermodel.h" #include #include #include "enums/delegatetype.h" #include "messagecontentmodel.h" #include "neochatconfig.h" #include "neochatroommember.h" using namespace Quotient; MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel) : QSortFilterProxyModel(parent) { Q_ASSERT(sourceModel); setSourceModel(sourceModel); connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] { invalidateFilter(); }); connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] { invalidateFilter(); }); connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] { invalidateFilter(); }); connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] { invalidateFilter(); }); connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] { invalidateFilter(); }); } bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { if (NeoChatConfig::self()->showAllEvents()) { return true; } return eventIsVisible(sourceRow, sourceParent); } bool MessageFilterModel::eventIsVisible(int sourceRow, const QModelIndex &sourceParent) const { const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); // Don't show redacted (i.e. deleted) messages. if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) { return false; } // Don't show hidden or replaced messages. const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt(); if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) { return false; } // Don't show events with an unknown type. const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt(); if (eventType == DelegateType::Other) { return false; } // Don't show state events that are not the first in a consecutive group on the // same day as they will be grouped as a single delegate. const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1; const bool previousEventIsState = notLastRow ? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State : false; const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool(); if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) { return false; } return true; } QVariant MessageFilterModel::data(const QModelIndex &index, int role) const { if (role == MessageEventModel::DelegateTypeRole && NeoChatConfig::self()->showAllEvents()) { if (!eventIsVisible(index.row(), index.parent())) { return DelegateType::Other; } } else if (role == AggregateDisplayRole) { return aggregateEventToString(mapToSource(index).row()); } else if (role == StateEventsRole) { return stateEventsList(mapToSource(index).row()); } else if (role == AuthorListRole) { return authorList(mapToSource(index).row()); } else if (role == ExcessAuthorsRole) { return excessAuthors(mapToSource(index).row()); } else if (role == MessageEventModel::ContentModelRole) { const auto model = qvariant_cast(mapToSource(index).data(MessageEventModel::ContentModelRole)); if (model != nullptr && !showAuthor(index)) { model->setShowAuthor(false); } return QVariant::fromValue(model); } return QSortFilterProxyModel::data(index, role); } QHash MessageFilterModel::roleNames() const { auto roles = sourceModel() ? sourceModel()->roleNames() : QHash(); roles[AggregateDisplayRole] = "aggregateDisplay"; roles[StateEventsRole] = "stateEvents"; roles[AuthorListRole] = "authorList"; roles[ExcessAuthorsRole] = "excessAuthors"; return roles; } bool MessageFilterModel::showAuthor(QModelIndex index) const { for (auto r = index.row() + 1; r < rowCount(); ++r) { auto i = this->index(r, 0); // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole) || data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State || data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000 || data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day() != data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day(); } } return true; } QString MessageFilterModel::aggregateEventToString(int sourceRow) const { QString aggregateString; for (int i = sourceRow; i >= 0; i--) { aggregateString += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString(); aggregateString += ", "_ls; QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); if (i > 0 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } } aggregateString = aggregateString.trimmed(); if (aggregateString.endsWith(u',')) { aggregateString.removeLast(); } return aggregateString; } QVariantList MessageFilterModel::stateEventsList(int sourceRow) const { QVariantList stateEvents; for (int i = sourceRow; i >= 0; i--) { auto nextState = QVariantMap{ {QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)}, {QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()}, {QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()}, }; stateEvents.append(nextState); if (i > 0 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } } return stateEvents; } QVariantList MessageFilterModel::authorList(int sourceRow) const { QVariantList uniqueAuthors; for (int i = sourceRow; i >= 0; i--) { QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); if (!uniqueAuthors.contains(nextAvatar)) { uniqueAuthors.append(nextAvatar); } if (i > 0 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } } if (uniqueAuthors.count() > 5) { uniqueAuthors = uniqueAuthors.mid(0, 5); } return uniqueAuthors; } QString MessageFilterModel::excessAuthors(int row) const { QVariantList uniqueAuthors; for (int i = row; i >= 0; i--) { QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); if (!uniqueAuthors.contains(nextAvatar)) { uniqueAuthors.append(nextAvatar); } if (i > 0 && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } } int excessAuthors; if (uniqueAuthors.count() > 5) { excessAuthors = uniqueAuthors.count() - 5; } else { excessAuthors = 0; } QString excessAuthorsString; if (excessAuthors == 0) { return QString(); } else { return QStringLiteral("+ %1").arg(excessAuthors); } } #include "moc_messagefiltermodel.cpp"