2023-02-22 01:48:49 +03:00
|
|
|
// SPDX-FileCopyrightText: Nheko Contributors
|
2022-10-06 22:59:59 +03:00
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
#include "TimelineFilter.h"
|
|
|
|
|
2022-12-19 05:24:22 +03:00
|
|
|
#include <QCoreApplication>
|
|
|
|
#include <QEvent>
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
#include "Logging.h"
|
2024-03-16 05:55:57 +03:00
|
|
|
#include "TimelineModel.h"
|
2022-10-06 02:39:30 +03:00
|
|
|
|
2022-12-19 05:30:54 +03:00
|
|
|
/// Searching currently can be done incrementally. For that we define a specific role to filter on
|
|
|
|
/// and then process that role in chunk. This is the `FilterRole`. Of course we need to then also
|
|
|
|
/// send proper update signals. Filtering then works as follows:
|
|
|
|
///
|
|
|
|
/// - At first no range is filtered (incrementalSearchIndex == 0).
|
|
|
|
/// - Then, when filtering is requested, we start posting events to the
|
|
|
|
/// event loop with lower than low priority (low prio - 1). The only thing those events do is
|
|
|
|
/// increment the incrementalSearchIndex and emit a dataChanged for that range of events.
|
|
|
|
/// - This then causes those events to be reevaluated if they should be visible.
|
|
|
|
|
2022-12-19 05:24:22 +03:00
|
|
|
static int FilterRole = Qt::UserRole * 3;
|
|
|
|
|
|
|
|
static QEvent::Type
|
|
|
|
getFilterEventType()
|
|
|
|
{
|
|
|
|
static QEvent::Type t = static_cast<QEvent::Type>(QEvent::registerEventType());
|
|
|
|
return t;
|
|
|
|
}
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
TimelineFilter::TimelineFilter(QObject *parent)
|
|
|
|
: QSortFilterProxyModel(parent)
|
|
|
|
{
|
|
|
|
setDynamicSortFilter(true);
|
2022-12-19 05:24:22 +03:00
|
|
|
setFilterRole(FilterRole);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TimelineFilter::startFiltering()
|
|
|
|
{
|
|
|
|
incrementalSearchIndex = 0;
|
2022-12-21 00:34:55 +03:00
|
|
|
emit isFilteringChanged();
|
2022-12-19 07:19:22 +03:00
|
|
|
invalidateFilter();
|
2023-04-04 21:25:41 +03:00
|
|
|
beginResetModel();
|
|
|
|
endResetModel();
|
2022-12-19 07:19:22 +03:00
|
|
|
|
2022-12-19 05:24:22 +03:00
|
|
|
continueFiltering();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TimelineFilter::continueFiltering()
|
|
|
|
{
|
2022-12-19 05:47:30 +03:00
|
|
|
if (auto s = source(); s) {
|
|
|
|
if (s->rowCount() > incrementalSearchIndex) {
|
|
|
|
auto ev = new QEvent(getFilterEventType());
|
|
|
|
// request filtering a new chunk with lower than low priority.
|
|
|
|
QCoreApplication::postEvent(this, ev, Qt::LowEventPriority - 1);
|
|
|
|
} else {
|
|
|
|
// We reached the end, so fetch more!
|
|
|
|
fetchAgain();
|
|
|
|
}
|
2022-12-19 05:24:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
TimelineFilter::event(QEvent *ev)
|
|
|
|
{
|
|
|
|
if (ev->type() == getFilterEventType()) {
|
2022-12-19 07:19:22 +03:00
|
|
|
if (incrementalSearchIndex < std::numeric_limits<int>::max()) {
|
|
|
|
int orgIndex = incrementalSearchIndex;
|
|
|
|
// process the next 100 events by claiming their "filterrole" data has changed.
|
|
|
|
incrementalSearchIndex += 100;
|
|
|
|
|
|
|
|
if (auto s = source(); s) {
|
|
|
|
auto count = s->rowCount();
|
|
|
|
if (incrementalSearchIndex >= count) {
|
|
|
|
incrementalSearchIndex = std::numeric_limits<int>::max();
|
|
|
|
}
|
|
|
|
nhlog::ui()->debug("Filter progress {}/{}", incrementalSearchIndex, count);
|
|
|
|
s->dataChanged(s->index(orgIndex),
|
|
|
|
s->index(std::min(incrementalSearchIndex, count - 1)),
|
|
|
|
{FilterRole});
|
|
|
|
|
2023-11-01 23:31:08 +03:00
|
|
|
continueFiltering();
|
2022-12-19 05:42:43 +03:00
|
|
|
}
|
2022-12-21 00:34:55 +03:00
|
|
|
emit isFilteringChanged();
|
2022-12-19 05:24:22 +03:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return QSortFilterProxyModel::event(ev);
|
2022-10-06 02:39:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TimelineFilter::setThreadId(const QString &t)
|
|
|
|
{
|
|
|
|
nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString());
|
|
|
|
if (this->threadId != t) {
|
|
|
|
this->threadId = t;
|
2022-11-04 01:26:59 +03:00
|
|
|
|
|
|
|
emit threadIdChanged();
|
2022-12-19 05:24:22 +03:00
|
|
|
startFiltering();
|
|
|
|
fetchMore({});
|
2022-10-06 02:39:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-06 22:59:59 +03:00
|
|
|
void
|
|
|
|
TimelineFilter::setContentFilter(const QString &c)
|
|
|
|
{
|
|
|
|
nhlog::ui()->debug("Filtering by content '{}'", c.toStdString());
|
|
|
|
if (this->contentFilter != c) {
|
|
|
|
this->contentFilter = c;
|
2022-11-04 01:26:59 +03:00
|
|
|
|
|
|
|
emit contentFilterChanged();
|
2022-12-19 05:24:22 +03:00
|
|
|
startFiltering();
|
|
|
|
fetchMore({});
|
2022-11-04 01:26:59 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TimelineFilter::fetchAgain()
|
|
|
|
{
|
|
|
|
if (threadId.isEmpty() && contentFilter.isEmpty())
|
|
|
|
return;
|
|
|
|
|
2022-12-19 07:19:22 +03:00
|
|
|
if (auto s = source(); s && incrementalSearchIndex == std::numeric_limits<int>::max()) {
|
2024-01-16 06:01:13 +03:00
|
|
|
if (this->rowCount() == cachedCount && s->canFetchMore(QModelIndex()) &&
|
|
|
|
// If we already have the event id of the thread in the timeline and we are filtering by
|
|
|
|
// thread, we can stop fetching more messages In theory an event could have been edited
|
|
|
|
// earlier in the timeline into the thread. So in theory this check is insufficient and
|
|
|
|
// we should instead verify that all events referring to this thread are in the timeline
|
|
|
|
// instead of just the thread root, but only Nheko supports that atm and the check would
|
|
|
|
// be expensive.
|
|
|
|
// TODO(Nico): check that all thread referrencing events are in the timeline by also
|
|
|
|
// checking all edits inside the thread.
|
|
|
|
(threadId.isEmpty() || s->idToIndex(threadId) == -1))
|
2022-11-04 01:26:59 +03:00
|
|
|
s->fetchMore(QModelIndex());
|
|
|
|
else
|
2022-12-19 07:19:22 +03:00
|
|
|
cachedCount = this->rowCount();
|
2022-10-06 22:59:59 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-19 05:24:22 +03:00
|
|
|
void
|
|
|
|
TimelineFilter::sourceDataChanged(const QModelIndex &topLeft,
|
|
|
|
const QModelIndex &bottomRight,
|
|
|
|
const QVector<int> &roles)
|
|
|
|
{
|
|
|
|
if (!roles.contains(TimelineModel::Roles::Body) && !roles.contains(TimelineModel::ThreadId))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (auto s = source()) {
|
|
|
|
s->dataChanged(topLeft, bottomRight, {FilterRole});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
void
|
|
|
|
TimelineFilter::setSource(TimelineModel *s)
|
|
|
|
{
|
|
|
|
if (auto orig = this->source(); orig != s) {
|
2022-12-19 05:24:22 +03:00
|
|
|
cachedCount = 0;
|
|
|
|
incrementalSearchIndex = 0;
|
2022-11-04 01:26:59 +03:00
|
|
|
|
|
|
|
if (orig) {
|
2022-10-06 02:39:30 +03:00
|
|
|
disconnect(orig,
|
|
|
|
&TimelineModel::currentIndexChanged,
|
|
|
|
this,
|
|
|
|
&TimelineFilter::currentIndexChanged);
|
2022-11-04 01:26:59 +03:00
|
|
|
disconnect(orig, &TimelineModel::fetchedMore, this, &TimelineFilter::fetchAgain);
|
2022-12-19 05:24:22 +03:00
|
|
|
disconnect(orig, &TimelineModel::dataChanged, this, &TimelineFilter::sourceDataChanged);
|
2022-11-04 01:26:59 +03:00
|
|
|
}
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
this->setSourceModel(s);
|
2022-11-04 01:26:59 +03:00
|
|
|
|
2023-10-18 23:43:45 +03:00
|
|
|
if (s) {
|
|
|
|
connect(
|
|
|
|
s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged);
|
|
|
|
connect(s,
|
|
|
|
&TimelineModel::fetchedMore,
|
|
|
|
this,
|
|
|
|
&TimelineFilter::fetchAgain,
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
connect(s,
|
|
|
|
&TimelineModel::dataChanged,
|
|
|
|
this,
|
|
|
|
&TimelineFilter::sourceDataChanged,
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
}
|
2022-11-04 01:26:59 +03:00
|
|
|
|
2022-12-19 05:30:54 +03:00
|
|
|
// reset the search index a second time just to be safe.
|
2022-12-19 05:24:22 +03:00
|
|
|
incrementalSearchIndex = 0;
|
2022-12-19 07:19:22 +03:00
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
emit sourceChanged();
|
2022-12-21 00:34:55 +03:00
|
|
|
emit isFilteringChanged();
|
2022-10-06 02:39:30 +03:00
|
|
|
invalidateFilter();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TimelineModel *
|
|
|
|
TimelineFilter::source() const
|
|
|
|
{
|
|
|
|
return qobject_cast<TimelineModel *>(sourceModel());
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TimelineFilter::setCurrentIndex(int idx)
|
|
|
|
{
|
|
|
|
// TODO: maybe send read receipt in thread timeline? Or not at all?
|
|
|
|
if (auto s = source()) {
|
|
|
|
s->setCurrentIndex(this->mapToSource(index(idx, 0)).row());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
TimelineFilter::currentIndex() const
|
|
|
|
{
|
|
|
|
if (auto s = source())
|
|
|
|
return this->mapFromSource(s->index(s->currentIndex())).row();
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-21 00:34:55 +03:00
|
|
|
bool
|
|
|
|
TimelineFilter::isFiltering() const
|
|
|
|
{
|
2022-12-21 01:15:59 +03:00
|
|
|
return incrementalSearchIndex != std::numeric_limits<int>::max() &&
|
|
|
|
!(threadId.isEmpty() && contentFilter.isEmpty());
|
2022-12-21 00:34:55 +03:00
|
|
|
}
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
bool
|
|
|
|
TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const
|
|
|
|
{
|
2022-12-19 05:30:54 +03:00
|
|
|
// this chunk is still unfiltered.
|
2022-12-19 05:24:22 +03:00
|
|
|
if (source_row > incrementalSearchIndex)
|
2022-12-19 07:19:22 +03:00
|
|
|
return false;
|
2022-12-19 05:24:22 +03:00
|
|
|
|
2022-10-06 22:59:59 +03:00
|
|
|
if (threadId.isEmpty() && contentFilter.isEmpty())
|
2022-10-06 02:39:30 +03:00
|
|
|
return true;
|
|
|
|
|
|
|
|
if (auto s = sourceModel()) {
|
|
|
|
auto idx = s->index(source_row, 0);
|
2022-10-06 22:59:59 +03:00
|
|
|
if (!contentFilter.isEmpty() && !s->data(idx, TimelineModel::Body)
|
|
|
|
.toString()
|
|
|
|
.contains(contentFilter, Qt::CaseInsensitive)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (threadId.isEmpty())
|
|
|
|
return true;
|
|
|
|
|
2022-10-06 02:39:30 +03:00
|
|
|
return s->data(idx, TimelineModel::EventId) == threadId ||
|
|
|
|
s->data(idx, TimelineModel::ThreadId) == threadId;
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2024-03-16 03:24:33 +03:00
|
|
|
|
|
|
|
#include "moc_TimelineFilter.cpp"
|