From 58ea229b67201246ffd8813392fbadc9b0e0e28b Mon Sep 17 00:00:00 2001 From: Azhar Momin Date: Sun, 28 Dec 2025 23:02:15 +0530 Subject: [PATCH] Add a button to cycle through unread highlights BUG: 465095 --- src/libneochat/neochatroom.cpp | 49 ++++++++++++++++++++++++ src/libneochat/neochatroom.h | 23 ++++++++++++ src/timeline/TimelineView.qml | 69 ++++++++++++++++++++++++---------- 3 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index c69668605..a0e07891a 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -169,6 +169,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS const auto neochatconnection = static_cast(connection); Q_ASSERT(neochatconnection); connect(neochatconnection, &NeoChatConnection::globalUrlPreviewEnabledChanged, this, &NeoChatRoom::urlPreviewEnabledChanged); + connect(this, &Room::fullyReadMarkerMoved, this, &NeoChatRoom::invalidateLastUnreadHighlightId); } bool NeoChatRoom::visible() const @@ -1843,4 +1844,52 @@ void NeoChatRoom::report(const QString &reason) connection()->callApi(id(), reason); } +QString NeoChatRoom::findNextUnreadHighlightId() +{ + const QString startEventId = !m_lastUnreadHighlightId.isEmpty() ? m_lastUnreadHighlightId : lastFullyReadEventId(); + const auto startIt = findInTimeline(startEventId); + if (startIt == historyEdge()) { + return {}; + } + + for (auto it = startIt.base(); it != messageEvents().cend(); ++it) { + const RoomEvent *ev = it->event(); + if (highlights.contains(ev)) { + m_lastUnreadHighlightId = ev->id(); + Q_EMIT highlightCycleStartedChanged(); + return m_lastUnreadHighlightId; + } + } + + if (!m_lastUnreadHighlightId.isEmpty()) { + m_lastUnreadHighlightId.clear(); + Q_EMIT highlightCycleStartedChanged(); + return findNextUnreadHighlightId(); + } + return {}; +} + +bool NeoChatRoom::highlightCycleStarted() const +{ + return !m_lastUnreadHighlightId.isEmpty(); +} + +void NeoChatRoom::invalidateLastUnreadHighlightId(const QString &fromEventId, const QString &toEventId) +{ + Q_UNUSED(fromEventId); + + if (m_lastUnreadHighlightId.isEmpty()) { + return; + } + + const auto lastIt = findInTimeline(m_lastUnreadHighlightId); + const auto newReadIt = findInTimeline(toEventId); + + // opposite comparision because both are reverse iterators :p + if (newReadIt <= lastIt) { + m_lastUnreadHighlightId.clear(); + Q_EMIT highlightCycleStartedChanged(); + } +} + #include "moc_neochatroom.cpp" diff --git a/src/libneochat/neochatroom.h b/src/libneochat/neochatroom.h index 7646c2393..720306acd 100644 --- a/src/libneochat/neochatroom.h +++ b/src/libneochat/neochatroom.h @@ -208,6 +208,11 @@ class NeoChatRoom : public Quotient::Room */ Q_PROPERTY(QString pinnedMessage READ pinnedMessage NOTIFY pinnedMessageChanged) + /** + * @brief Whether the highlight finding cycle has started. + */ + Q_PROPERTY(bool highlightCycleStarted READ highlightCycleStarted NOTIFY highlightCycleStartedChanged) + public: explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {}); @@ -627,6 +632,19 @@ public: */ Q_INVOKABLE void report(const QString &reason); + /** + * @brief Returns the ID of the next unread highlight in the room. + * + * Each call advances the internal highlight cursor. Once the last unread highlight + * is reached, the cycle is reset. + */ + Q_INVOKABLE QString findNextUnreadHighlightId(); + + /** + * @brief Whether the highlight finding cycle has started. + */ + bool highlightCycleStarted() const; + private: bool m_visible = false; @@ -662,11 +680,15 @@ private: QString m_pinnedMessage; void loadPinnedMessage(); + QString m_lastUnreadHighlightId; + private Q_SLOTS: void updatePushNotificationState(QString type); void cacheLastEvent(); + void invalidateLastUnreadHighlightId(const QString &fromEventId, const QString &toEventId); + Q_SIGNALS: void cachedInputChanged(); void busyChanged(); @@ -692,6 +714,7 @@ Q_SIGNALS: void extraEventNotFound(const QString &eventId); void inviteTimestampChanged(); void pinnedMessageChanged(); + void highlightCycleStartedChanged(); /** * @brief Request a message be shown to the user of the given type. diff --git a/src/timeline/TimelineView.qml b/src/timeline/TimelineView.qml index f331713f4..cc0b720c3 100644 --- a/src/timeline/TimelineView.qml +++ b/src/timeline/TimelineView.qml @@ -193,9 +193,7 @@ QQC2.ScrollView { room: _private.room } - KirigamiComponents.FloatingButton { - id: goReadMarkerFab - + ColumnLayout { anchors { right: parent.right top: parent.top @@ -203,28 +201,59 @@ QQC2.ScrollView { rightMargin: Kirigami.Units.largeSpacing } - implicitWidth: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 - implicitHeight: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 + spacing: Kirigami.Units.largeSpacing - padding: Kirigami.Units.largeSpacing + KirigamiComponents.FloatingButton { + id: goReadMarkerFab - z: 2 - visible: !_private.room?.partiallyReadStats.empty() + implicitWidth: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 + implicitHeight: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 - text: _private.room.readMarkerLoaded ? i18nc("@action:button", "Jump to first unread message") : i18nc("@action:button", "Jump to oldest loaded message") - icon.name: "go-up" - onClicked: { - goReadMarkerFab.textChanged() - root.goToEvent(_private.room.lastFullyReadEventId); - } - Kirigami.Action { - shortcut: "Shift+PgUp" - onTriggered: goReadMarkerFab.clicked() + padding: Kirigami.Units.largeSpacing + + z: 2 + visible: !_private.room?.partiallyReadStats.empty() + + text: _private.room.readMarkerLoaded ? i18nc("@action:button", "Jump to first unread message") : i18nc("@action:button", "Jump to oldest loaded message") + icon.name: "go-up" + onClicked: { + goReadMarkerFab.textChanged() + root.goToEvent(_private.room.lastFullyReadEventId); + } + Kirigami.Action { + shortcut: "Shift+PgUp" + onTriggered: goReadMarkerFab.clicked() + } + + QQC2.ToolTip.text: goReadMarkerFab.text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: goReadMarkerFab.hovered } + KirigamiComponents.FloatingButton { + id: goUnreadHighlightFab - QQC2.ToolTip.text: goReadMarkerFab.text - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - QQC2.ToolTip.visible: goReadMarkerFab.hovered + implicitWidth: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 + implicitHeight: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2 + + padding: Kirigami.Units.largeSpacing + + z: 2 + visible: _private.room?.highlightCount > 0 + + text: _private.room?.highlightCycleStarted ? i18nc("@action:button", "Jump to next unread highlight") : i18nc("@action:button", "Jump to first unread message") + icon.name: "mail-unread-symbolic" + onClicked: { + const eventId = _private.room.findNextUnreadHighlightId(); + if (eventId !== "") { + goUnreadHighlightFab.textChanged(); + root.goToEvent(eventId); + } + } + + QQC2.ToolTip.text: goUnreadHighlightFab.text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: goUnreadHighlightFab.hovered + } } KirigamiComponents.FloatingButton { id: goMarkAsReadFab