From 857d9cf2b6c649feb7557ed82bf7b98b63aa376e Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Thu, 6 Oct 2022 01:39:30 +0200
Subject: [PATCH] Basic thread filtering

The reply pagination logic is a bit weird rn though.
---
 CMakeLists.txt                  |  2 +
 resources/qml/MessageView.qml   | 17 ++++++--
 src/MainWindow.cpp              |  2 +
 src/timeline/TimelineFilter.cpp | 75 +++++++++++++++++++++++++++++++++
 src/timeline/TimelineFilter.h   | 43 +++++++++++++++++++
 5 files changed, 135 insertions(+), 4 deletions(-)
 create mode 100644 src/timeline/TimelineFilter.cpp
 create mode 100644 src/timeline/TimelineFilter.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index a1e0a41f6..c27e596b8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -364,6 +364,8 @@ set(SRC_FILES
 	src/timeline/Reaction.h
 	src/timeline/RoomlistModel.cpp
 	src/timeline/RoomlistModel.h
+	src/timeline/TimelineFilter.cpp
+	src/timeline/TimelineFilter.h
 	src/timeline/TimelineModel.cpp
 	src/timeline/TimelineModel.h
 	src/timeline/TimelineViewManager.cpp
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index a9ce8b556..bf336dfba 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -38,7 +38,14 @@ Item {
 
         displayMarginBeginning: height / 2
         displayMarginEnd: height / 2
-        model: room
+
+        TimelineFilter {
+            id: filteredTimeline
+            source: room
+            filterByThread: room ? room.thread : ""
+        }
+
+        model: filteredTimeline.filterByThread ? filteredTimeline : room
         // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
         //onModelChanged: if (room) room.sendReset()
         //reuseItems: true
@@ -215,16 +222,18 @@ Item {
             }
         }
 
+        // These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
+        // Better solution welcome.
         Shortcut {
             sequence: "Alt+Up"
-            onActivated: room.reply = chat.model.indexToId(room.reply ? chat.model.idToIndex(room.reply) + 1 : 0)
+            onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
         }
 
         Shortcut {
             sequence: "Alt+Down"
             onActivated: {
-                var idx = room.reply ? chat.model.idToIndex(room.reply) - 1 : -1;
-                room.reply = idx >= 0 ? chat.model.indexToId(idx) : null;
+                var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
+                room.reply = idx >= 0 ? room.indexToId(idx) : null;
             }
         }
 
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 63cf28440..1d7438449 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -46,6 +46,7 @@
 #include "encryption/DeviceVerificationFlow.h"
 #include "encryption/SelfVerificationStatus.h"
 #include "timeline/DelegateChooser.h"
+#include "timeline/TimelineFilter.h"
 #include "timeline/TimelineViewManager.h"
 #include "ui/HiddenEvents.h"
 #include "ui/MxcAnimatedImage.h"
@@ -186,6 +187,7 @@ MainWindow::registerQmlTypes()
     qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
     qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
     qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
+    qmlRegisterType<TimelineFilter>("im.nheko", 1, 0, "TimelineFilter");
     qmlRegisterUncreatableType<RoomSummary>(
       "im.nheko",
       1,
diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp
new file mode 100644
index 000000000..82bc7dd30
--- /dev/null
+++ b/src/timeline/TimelineFilter.cpp
@@ -0,0 +1,75 @@
+#include "TimelineFilter.h"
+
+#include "Logging.h"
+
+TimelineFilter::TimelineFilter(QObject *parent)
+  : QSortFilterProxyModel(parent)
+{
+    setDynamicSortFilter(true);
+}
+
+void
+TimelineFilter::setThreadId(const QString &t)
+{
+    nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString());
+    if (this->threadId != t) {
+        this->threadId = t;
+        invalidateFilter();
+    }
+    emit threadIdChanged();
+}
+
+void
+TimelineFilter::setSource(TimelineModel *s)
+{
+    if (auto orig = this->source(); orig != s) {
+        if (orig)
+            disconnect(orig,
+                       &TimelineModel::currentIndexChanged,
+                       this,
+                       &TimelineFilter::currentIndexChanged);
+        this->setSourceModel(s);
+        connect(s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged);
+        emit sourceChanged();
+        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;
+}
+
+bool
+TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const
+{
+    if (threadId.isEmpty())
+        return true;
+
+    if (auto s = sourceModel()) {
+        auto idx = s->index(source_row, 0);
+        return s->data(idx, TimelineModel::EventId) == threadId ||
+               s->data(idx, TimelineModel::ThreadId) == threadId;
+    } else {
+        return true;
+    }
+}
diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h
new file mode 100644
index 000000000..5c71a89ac
--- /dev/null
+++ b/src/timeline/TimelineFilter.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QSortFilterProxyModel>
+#include <QString>
+
+#include <mtx/events/power_levels.hpp>
+
+#include "TimelineModel.h"
+
+class TimelineFilter : public QSortFilterProxyModel
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged)
+    Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged)
+    Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
+
+public:
+    explicit TimelineFilter(QObject *parent = nullptr);
+
+    QString filterByThread() const { return threadId; }
+    TimelineModel *source() const;
+    int currentIndex() const;
+
+    void setThreadId(const QString &t);
+    void setSource(TimelineModel *t);
+    void setCurrentIndex(int idx);
+
+signals:
+    void threadIdChanged();
+    void sourceChanged();
+    void currentIndexChanged();
+
+protected:
+    bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+
+private:
+    QString threadId;
+};
-- 
GitLab