From 53cd31d181ac2e2398cf20d1901d3e26efc65016 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 13 Sep 2025 01:01:06 +0200 Subject: [PATCH] Allow filtering the timeline for notifications --- resources/icons/ui/alert.svg | 1 + resources/qml/MessageView.qml | 4 +++- resources/qml/TimelineView.qml | 1 + resources/qml/TopBar.qml | 33 ++++++++++++++++++++++------ resources/res.qrc | 1 + src/timeline/TimelineFilter.cpp | 38 ++++++++++++++++++++++++++++++--- src/timeline/TimelineFilter.h | 6 ++++++ 7 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 resources/icons/ui/alert.svg diff --git a/resources/icons/ui/alert.svg b/resources/icons/ui/alert.svg new file mode 100644 index 00000000..7b730a59 --- /dev/null +++ b/resources/icons/ui/alert.svg @@ -0,0 +1 @@ + diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 8a457afb..0c71573a 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -16,6 +16,7 @@ Item { property int availableWidth: width property int padding: Nheko.paddingMedium property string searchString: "" + property bool filterByNotifications: false property Room roommodel: room // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu @@ -60,7 +61,7 @@ Item { boundsBehavior: Flickable.StopAtBounds displayMarginBeginning: height / 4 displayMarginEnd: height / 4 - model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room + model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent || filteredTimeline.filterByNotifications) ? filteredTimeline : room //pixelAligned: true spacing: 2 verticalLayoutDirection: ListView.BottomToTop @@ -145,6 +146,7 @@ Item { id: filteredTimeline filterByContent: chatRoot.searchString + filterByNotifications: chatRoot.filterByNotifications filterByThread: room ? room.thread : "" source: room } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 8e83cc1f..64493c4e 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -119,6 +119,7 @@ Item { Layout.fillWidth: true implicitHeight: msgView.height - typingIndicator.height searchString: topBar.searchString + filterByNotifications: topBar.filterNotifications } Loader { source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? (Qt.platform.os != "windows" ? "voip/VideoCall.qml" : "voip/VideoCallD3D11.qml") : "" diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 0bdd4ab8..900e59e8 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -22,6 +22,7 @@ Pane { property bool searchHasFocus: searchField.focus && searchField.enabled property string searchString: "" property bool showBackButton: false + property bool filterNotifications: false property int trustlevel: room ? room.trustlevel : Crypto.Unverified Layout.fillWidth: true @@ -129,13 +130,30 @@ Pane { selectByMouse: false text: roomTopic } + ImageButton { + id: notificationsButton + + Layout.alignment: Qt.AlignRight + Layout.column: 3 + Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium + Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium + Layout.row: 1 + Layout.rowSpan: 2 + ToolTip.text: qsTr("Show only notifications") + ToolTip.visible: hovered + image: ":/icons/icons/ui/alert.svg" + + onClicked: { + topBar.filterNotifications = !topBar.filterNotifications + } + } ImageButton { id: pinButton property bool pinsShown: !Settings.hiddenPins.includes(roomId) Layout.alignment: Qt.AlignVCenter - Layout.column: 3 + Layout.column: 4 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.row: 1 @@ -160,7 +178,7 @@ Pane { } AbstractButton { id: memberButton - Layout.column: 4 + Layout.column: 5 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.row: 1 @@ -200,7 +218,7 @@ Pane { property bool searchActive: false Layout.alignment: Qt.AlignVCenter - Layout.column: 5 + Layout.column: 6 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.row: 1 @@ -224,7 +242,7 @@ Pane { id: roomOptionsButton Layout.alignment: Qt.AlignVCenter - Layout.column: 6 + Layout.column: 7 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.row: 1 @@ -273,7 +291,7 @@ Pane { id: pinnedMessages Layout.column: 2 - Layout.columnSpan: 4 + Layout.columnSpan: 5 Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4) Layout.row: 3 @@ -330,7 +348,7 @@ Pane { id: widgets Layout.column: 2 - Layout.columnSpan: 4 + Layout.columnSpan: 5 Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5) Layout.row: 4 @@ -356,7 +374,7 @@ Pane { id: searchField Layout.column: 2 - Layout.columnSpan: 4 + Layout.columnSpan: 5 Layout.fillWidth: true Layout.row: 5 enabled: visible @@ -378,6 +396,7 @@ Pane { searchString = ""; searchButton.searchActive = false; searchField.text = ""; + filterNotifications = false; } // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu diff --git a/resources/res.qrc b/resources/res.qrc index 642bc220..13d4c371 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -1,6 +1,7 @@ icons/ui/add-square-button.svg + icons/ui/alert.svg icons/ui/angle-arrow-left.svg icons/ui/attach.svg icons/ui/ban.svg diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp index 0833900e..8e323ff6 100644 --- a/src/timeline/TimelineFilter.cpp +++ b/src/timeline/TimelineFilter.cpp @@ -96,6 +96,10 @@ TimelineFilter::setThreadId(const QString &t) { nhlog::ui()->debug("Filtering by thread '{}'", t.toStdString()); if (this->threadId != t) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + beginFilterChange(); +#endif + this->threadId = t; emit threadIdChanged(); @@ -104,11 +108,30 @@ TimelineFilter::setThreadId(const QString &t) } } +void +TimelineFilter::setFilterNotifications(bool filter) +{ + nhlog::ui()->debug("Filtering by notifications '{}'", filter); + if (this->filterByNotifications_ != filter) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + beginFilterChange(); +#endif + this->filterByNotifications_ = filter; + + emit filterNotificationsChanged(); + startFiltering(); + fetchMore({}); + } +} + void TimelineFilter::setContentFilter(const QString &c) { nhlog::ui()->debug("Filtering by content '{}'", c.toStdString()); if (this->contentFilter != c) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + beginFilterChange(); +#endif this->contentFilter = c; emit contentFilterChanged(); @@ -145,7 +168,8 @@ TimelineFilter::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { - if (!roles.contains(TimelineModel::Roles::Body) && !roles.contains(TimelineModel::ThreadId)) + if (!roles.contains(TimelineModel::Roles::Body) && !roles.contains(TimelineModel::ThreadId) && + !roles.contains(TimelineModel::Notificationlevel)) return; if (auto s = source()) { @@ -233,19 +257,27 @@ TimelineFilter::filterAcceptsRow(int source_row, const QModelIndex &) const if (source_row > incrementalSearchIndex) return false; - if (threadId.isEmpty() && contentFilter.isEmpty()) + if (threadId.isEmpty() && contentFilter.isEmpty() && !filterByNotifications_) 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()) + if (filterByNotifications_ && s->data(idx, TimelineModel::Notificationlevel) + .value() != + qml_mtx_events::NotificationLevel::Highlight) { + return false; + } + + if (threadId.isEmpty()) { return true; + } return s->data(idx, TimelineModel::EventId) == threadId || s->data(idx, TimelineModel::ThreadId) == threadId; diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h index 336339e2..76d5bf52 100644 --- a/src/timeline/TimelineFilter.h +++ b/src/timeline/TimelineFilter.h @@ -18,6 +18,8 @@ class TimelineFilter : public QSortFilterProxyModel QML_ELEMENT Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged) + Q_PROPERTY(bool filterByNotifications READ filterByNotifications WRITE setFilterNotifications + NOTIFY filterNotificationsChanged) Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY contentFilterChanged) Q_PROPERTY(TimelineModel *source READ source WRITE setSource NOTIFY sourceChanged) @@ -28,12 +30,14 @@ public: explicit TimelineFilter(QObject *parent = nullptr); QString filterByThread() const { return threadId; } + bool filterByNotifications() const { return filterByNotifications_; } QString filterByContent() const { return contentFilter; } TimelineModel *source() const; int currentIndex() const; bool isFiltering() const; void setThreadId(const QString &t); + void setFilterNotifications(bool v); void setContentFilter(const QString &t); void setSource(TimelineModel *t); void setCurrentIndex(int idx); @@ -47,6 +51,7 @@ public: signals: void threadIdChanged(); + void filterNotificationsChanged(); void contentFilterChanged(); void sourceChanged(); void currentIndexChanged(); @@ -67,4 +72,5 @@ private: QString threadId, contentFilter; int cachedCount = 0, incrementalSearchIndex = 0; + bool filterByNotifications_ = false; };