From f1c1f18f815d93583f524a89e4c2f5f954b07b43 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Thu, 6 Oct 2022 21:59:59 +0200
Subject: [PATCH] Add a slow way to search a room

---
 resources/qml/MessageView.qml   | 11 ++--
 resources/qml/TimelineView.qml  |  3 ++
 resources/qml/TopBar.qml        | 89 +++++++++++++++++++++++++--------
 src/timeline/EventStore.cpp     |  1 +
 src/timeline/TimelineFilter.cpp | 26 +++++++++-
 src/timeline/TimelineFilter.h   | 12 ++++-
 src/timeline/TimelineModel.cpp  | 19 -------
 src/timeline/TimelineModel.h    |  7 +--
 8 files changed, 120 insertions(+), 48 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index bf336dfba..202c3e3d6 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -22,6 +22,8 @@ Item {
 
     property int availableWidth: width
 
+    property string searchString: ""
+
     ScrollBar {
         id: scrollbar
         parent: chat.parent
@@ -43,9 +45,10 @@ Item {
             id: filteredTimeline
             source: room
             filterByThread: room ? room.thread : ""
+            filterByContent: chatRoot.searchString
         }
 
-        model: filteredTimeline.filterByThread ? filteredTimeline : room
+        model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? 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
@@ -403,7 +406,7 @@ Item {
             required property bool isEditable
             required property bool isEdited
             required property bool isStateEvent
-            required property bool previousMessageIsStateEvent
+            property bool previousMessageIsStateEvent: chat.model.dataByIndex(index+1, Room.IsStateEvent)
             required property string replyTo
             required property string threadId
             required property string userId
@@ -417,9 +420,9 @@ Item {
             required property int status
             required property int index
             required property int relatedEventCacheBuster
-            required property string previousMessageUserId
             required property string day
-            required property string previousMessageDay
+            property string previousMessageUserId: chat.model.dataByIndex(index+1, Room.UserId)
+            property string previousMessageDay: chat.model.dataByIndex(index+1, Room.Day)
             required property string userName
             property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
 
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 7722411e1..ab1bbc281 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -66,6 +66,8 @@ Item {
         spacing: 0
 
         TopBar {
+            id: topBar
+
             showBackButton: timelineView.showBackButton
         }
 
@@ -102,6 +104,7 @@ Item {
 
                     MessageView {
                         implicitHeight: msgView.height - typingIndicator.height
+                        searchString: topBar.searchString
                         Layout.fillWidth: true
                     }
 
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 39365cd17..716a5e105 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -25,6 +25,14 @@ Pane {
     property bool isDirect: room ? room.isDirect : false
     property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
 
+    property string searchString: ""
+
+    onRoomIdChanged: {
+        searchString = "";
+        searchButton.searchActive = false;
+        searchField.text = ""
+    }
+
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
@@ -177,12 +185,42 @@ Pane {
                 text: roomTopic
             }
 
-            AbstractButton {
+            ImageButton {
+                id: pinButton
+
+                property bool pinsShown: !Settings.hiddenPins.includes(roomId)
+
+                visible: !!room && room.pinnedMessages.length > 0
                 Layout.column: 3
                 Layout.row: 1
                 Layout.rowSpan: 2
+                Layout.alignment: Qt.AlignVCenter
                 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
                 Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
+                ToolTip.visible: hovered
+                ToolTip.text: qsTr("Show or hide pinned messages")
+                onClicked: {
+                    var ps = Settings.hiddenPins;
+                    if (pinsShown) {
+                        ps.push(roomId);
+                    } else {
+                        const index = ps.indexOf(roomId);
+                        if (index > -1) {
+                            ps.splice(index, 1);
+                        }
+                    }
+                    Settings.hiddenPins = ps;
+                }
+
+            }
+
+            AbstractButton {
+                Layout.column: 4
+                Layout.row: 1
+                Layout.rowSpan: 2
+                Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.maximumWidth: Nheko.avatarSize - Nheko.paddingMedium
 
                 contentItem: EncryptionIndicator {
                     sourceSize.height: parent.Layout.preferredHeight * Screen.devicePixelRatio
@@ -216,40 +254,37 @@ Pane {
             }
 
             ImageButton {
-                id: pinButton
+                id: searchButton
 
-                property bool pinsShown: !Settings.hiddenPins.includes(roomId)
+                property bool searchActive: false
 
-                visible: !!room && room.pinnedMessages.length > 0
-                Layout.column: 4
+                visible: !!room
+                Layout.column: 5
                 Layout.row: 1
                 Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
                 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
                 Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-                image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
+                image: ":/icons/icons/ui/search.svg"
                 ToolTip.visible: hovered
-                ToolTip.text: qsTr("Show or hide pinned messages")
+                ToolTip.text: qsTr("Search this room")
                 onClicked: {
-                    var ps = Settings.hiddenPins;
-                    if (pinsShown) {
-                        ps.push(roomId);
-                    } else {
-                        const index = ps.indexOf(roomId);
-                        if (index > -1) {
-                            ps.splice(index, 1);
-                        }
+                    searchActive = !searchActive
+                    if (searchActive) {
+                        searchField.forceActiveFocus();
+                    }
+                    else {
+                        searchField.clear();
+                        topBar.searchString = "";
                     }
-                    Settings.hiddenPins = ps;
                 }
-
             }
 
             ImageButton {
                 id: roomOptionsButton
 
                 visible: !!room
-                Layout.column: 5
+                Layout.column: 6
                 Layout.row: 1
                 Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
@@ -293,7 +328,7 @@ Pane {
 
                 Layout.row: 3
                 Layout.column: 2
-                Layout.columnSpan: 3
+                Layout.columnSpan: 4
 
                 Layout.fillWidth: true
                 Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
@@ -374,7 +409,7 @@ Pane {
 
                 Layout.row: 4
                 Layout.column: 2
-                Layout.columnSpan: 1
+                Layout.columnSpan: 4
 
                 Layout.fillWidth: true
                 Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
@@ -404,6 +439,20 @@ Pane {
                     }
                 }
             }
+
+            MatrixTextField {
+                id: searchField
+                visible: searchButton.searchActive
+
+                Layout.row: 5
+                Layout.column: 2
+                Layout.columnSpan: 4
+
+                Layout.fillWidth: true
+
+                placeholderText: qsTr("Enter search query")
+                onAccepted: topBar.searchString = text
+            }
         }
 
         CursorShape {
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 0b7a7b1bf..de813196a 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -898,6 +898,7 @@ EventStore::fetchMore()
     mtx::http::MessagesOpts opts;
     opts.room_id = room_id_;
     opts.from    = cache::client()->previousBatchToken(room_id_);
+    opts.limit   = 80;
 
     nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from);
 
diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp
index 82bc7dd30..15d2590cc 100644
--- a/src/timeline/TimelineFilter.cpp
+++ b/src/timeline/TimelineFilter.cpp
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
 #include "TimelineFilter.h"
 
 #include "Logging.h"
@@ -19,6 +23,17 @@ TimelineFilter::setThreadId(const QString &t)
     emit threadIdChanged();
 }
 
+void
+TimelineFilter::setContentFilter(const QString &c)
+{
+    nhlog::ui()->debug("Filtering by content '{}'", c.toStdString());
+    if (this->contentFilter != c) {
+        this->contentFilter = c;
+        invalidateFilter();
+    }
+    emit contentFilterChanged();
+}
+
 void
 TimelineFilter::setSource(TimelineModel *s)
 {
@@ -62,11 +77,20 @@ TimelineFilter::currentIndex() const
 bool
 TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const
 {
-    if (threadId.isEmpty())
+    if (threadId.isEmpty() && contentFilter.isEmpty())
         return true;
 
     if (auto s = sourceModel()) {
         auto idx = s->index(source_row, 0);
+        if (!contentFilter.isEmpty() && !s->data(idx, TimelineModel::Body)
+                                           .toString()
+                                           .contains(contentFilter, Qt::CaseInsensitive)) {
+            return false;
+        }
+
+        if (threadId.isEmpty())
+            return true;
+
         return s->data(idx, TimelineModel::EventId) == threadId ||
                s->data(idx, TimelineModel::ThreadId) == threadId;
     } else {
diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h
index 5c71a89ac..3b04650e6 100644
--- a/src/timeline/TimelineFilter.h
+++ b/src/timeline/TimelineFilter.h
@@ -16,6 +16,8 @@ class TimelineFilter : public QSortFilterProxyModel
     Q_OBJECT
 
     Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged)
+    Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY
+                 contentFilterChanged)
     Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged)
     Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
 
@@ -23,15 +25,23 @@ public:
     explicit TimelineFilter(QObject *parent = nullptr);
 
     QString filterByThread() const { return threadId; }
+    QString filterByContent() const { return contentFilter; }
     TimelineModel *source() const;
     int currentIndex() const;
 
     void setThreadId(const QString &t);
+    void setContentFilter(const QString &t);
     void setSource(TimelineModel *t);
     void setCurrentIndex(int idx);
 
+    Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const
+    {
+        return data(index(i, 0), role);
+    }
+
 signals:
     void threadIdChanged();
+    void contentFilterChanged();
     void sourceChanged();
     void currentIndexChanged();
 
@@ -39,5 +49,5 @@ protected:
     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
 
 private:
-    QString threadId;
+    QString threadId, contentFilter;
 };
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 6c967633a..3ce854a4e 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -481,12 +481,9 @@ TimelineModel::roleNames() const
       {IsOnlyEmoji, "isOnlyEmoji"},
       {Body, "body"},
       {FormattedBody, "formattedBody"},
-      {PreviousMessageUserId, "previousMessageUserId"},
       {IsSender, "isSender"},
       {UserId, "userId"},
       {UserName, "userName"},
-      {PreviousMessageDay, "previousMessageDay"},
-      {PreviousMessageIsStateEvent, "previousMessageIsStateEvent"},
       {Day, "day"},
       {Timestamp, "timestamp"},
       {Url, "url"},
@@ -804,22 +801,6 @@ TimelineModel::data(const QModelIndex &index, int role) const
     if (!event)
         return "";
 
-    if (role == PreviousMessageDay || role == PreviousMessageUserId ||
-        role == PreviousMessageIsStateEvent) {
-        int prevIdx = rowCount() - index.row() - 2;
-        if (prevIdx < 0)
-            return {};
-        auto tempEv = events.get(prevIdx);
-        if (!tempEv)
-            return {};
-        if (role == PreviousMessageUserId)
-            return data(*tempEv, UserId);
-        else if (role == PreviousMessageDay)
-            return data(*tempEv, Day);
-        else
-            return data(*tempEv, IsStateEvent);
-    }
-
     return data(*event, role);
 }
 
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3a862cc9c..ee84486e4 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -214,12 +214,9 @@ public:
         IsOnlyEmoji,
         Body,
         FormattedBody,
-        PreviousMessageUserId,
         IsSender,
         UserId,
         UserName,
-        PreviousMessageDay,
-        PreviousMessageIsStateEvent,
         Day,
         Timestamp,
         Url,
@@ -257,6 +254,10 @@ public:
     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
     QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
     Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo);
+    Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const
+    {
+        return data(index(i), role);
+    }
 
     bool canFetchMore(const QModelIndex &) const override;
     void fetchMore(const QModelIndex &) override;
-- 
GitLab