From 3d9d211d25a5246e570f3acfc7ae2703bbec7545 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 4 Jul 2025 14:36:36 +0100 Subject: [PATCH] Allow the condition for when messages are automatically marked as read to be configurable. Title this adds a number of options for when messages should be automatically marked as read for the user to choose from. ![image](/uploads/cef95f8c6c77bfdcabb7a8a309bc1fd2/image.png){width=480 height=262} --- autotests/timelinemessagemodeltest.cpp | 4 +- src/app/neochatconfig.kcfg | 6 +++ src/app/qml/RoomPage.qml | 3 ++ src/libneochat/CMakeLists.txt | 1 + .../enums/timelinemarkreadcondition.h | 32 +++++++++++++ src/settings/NeoChatGeneralPage.qml | 38 ++++++++++++++- src/timeline/TimelineView.qml | 24 ++++++++-- src/timeline/models/messagemodel.cpp | 47 ++++++++++++------- src/timeline/models/messagemodel.h | 15 ++++-- src/timeline/models/timelinemessagemodel.cpp | 30 +++++++----- src/timeline/models/timelinemessagemodel.h | 2 - 11 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 src/libneochat/enums/timelinemarkreadcondition.h diff --git a/autotests/timelinemessagemodeltest.cpp b/autotests/timelinemessagemodeltest.cpp index 3068a31ca..41358fdfd 100644 --- a/autotests/timelinemessagemodeltest.cpp +++ b/autotests/timelinemessagemodeltest.cpp @@ -57,7 +57,7 @@ void TimelineMessageModelTest::switchEmptyRoom() auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s); auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s); - QSignalSpy spy(model, SIGNAL(roomChanged())); + QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *))); QCOMPARE(model->room(), nullptr); model->setRoom(firstRoom); @@ -77,7 +77,7 @@ void TimelineMessageModelTest::switchSyncedRoom() auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s); auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s); - QSignalSpy spy(model, SIGNAL(roomChanged())); + QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *))); QCOMPARE(model->room(), nullptr); model->setRoom(firstRoom); diff --git a/src/app/neochatconfig.kcfg b/src/app/neochatconfig.kcfg index d338982b8..56a4e47a9 100644 --- a/src/app/neochatconfig.kcfg +++ b/src/app/neochatconfig.kcfg @@ -78,6 +78,12 @@ false + + + + + 2 + true diff --git a/src/app/qml/RoomPage.qml b/src/app/qml/RoomPage.qml index 429d56df1..ca92b74e4 100644 --- a/src/app/qml/RoomPage.qml +++ b/src/app/qml/RoomPage.qml @@ -104,11 +104,14 @@ Kirigami.Page { id: timelineViewLoader anchors.fill: parent active: root.currentRoom && !root.currentRoom.isInvite && !root.currentRoom.isSpace + // We need the loader to be active but invisible while the room is loading messages so signals in TimelineView work. + visible: !root.loading sourceComponent: TimelineView { id: timelineView messageFilterModel: root.messageFilterModel compactLayout: NeoChatConfig.compactLayout fileDropEnabled: !Controller.isFlatpak + markReadCondition: NeoChatConfig.markReadCondition } } diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index 0f4d462f4..a3a0229a0 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(LibNeoChat PRIVATE enums/pushrule.h enums/roomsortparameter.cpp enums/roomsortorder.h + enums/timelinemarkreadcondition.h events/imagepackevent.cpp events/pollevent.cpp jobs/neochatgetcommonroomsjob.cpp diff --git a/src/libneochat/enums/timelinemarkreadcondition.h b/src/libneochat/enums/timelinemarkreadcondition.h new file mode 100644 index 000000000..6646284cb --- /dev/null +++ b/src/libneochat/enums/timelinemarkreadcondition.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +/** + * @class TimelineMarkReadCondition + * + * This class is designed to define the TimelineMarkReadCondition enumeration. + */ +class TimelineMarkReadCondition : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + /** + * @brief The condition for marking messages as read. + */ + enum Condition { + Never = 0, /**< Messages should never be marked automatically. */ + Entry, /**< Messages should be marked automatically on entry to the room. */ + EntryVisible, /**< Messages should be marked automatically on entry to the room if all messages are visible. */ + Exit, /**< Messages should be marked automatically on exiting the room. */ + ExitVisible, /**< Messages should be marked automatically on exiting the room if all messages are visible. */ + }; + Q_ENUM(Condition); +}; diff --git a/src/settings/NeoChatGeneralPage.qml b/src/settings/NeoChatGeneralPage.qml index 1ed6d5c7c..13a70f332 100644 --- a/src/settings/NeoChatGeneralPage.qml +++ b/src/settings/NeoChatGeneralPage.qml @@ -134,9 +134,45 @@ FormCard.FormCardPage { } } FormCard.FormHeader { - title: i18n("Timeline Events") + title: i18nc("@title", "Timeline") } FormCard.FormCard { + FormCard.FormComboBoxDelegate { + id: markAsReadCombo + text: i18n("Mark messages as read when:") + textRole: "name" + valueRole: "value" + model: [ + { + name: i18n("Never"), + value: 0 + }, + { + name: i18nc("@item:inlistbox As in mark messages in the room as read when entering the room", "Entering the room"), + value: 1 + }, + { + name: i18nc("@item:inlistbox As in mark messages in the room as read when entering the room and all messages are visible on screen", "Entering the room and all unread messages are visible"), + value: 2 + }, + { + name: i18nc("@item:inlistbox As in mark messages in the room as read when exiting the room", "Exiting the room"), + value: 3 + }, + { + name: i18nc("@item:inlistbox As in mark messages in the room as read when exiting the room and all messages are visible on screen", "Exiting the room and all unread messages are visible"), + value: 4 + } + ] + Component.onCompleted: currentIndex = NeoChatConfig.markReadCondition + onCurrentValueChanged: NeoChatConfig.markReadCondition = currentValue + } + + FormCard.FormDelegateSeparator { + above: markAsReadCombo + below: showDeletedMessages + } + FormCard.FormCheckDelegate { id: showDeletedMessages text: i18n("Show deleted messages") diff --git a/src/timeline/TimelineView.qml b/src/timeline/TimelineView.qml index c4fd61745..0e7a8719a 100644 --- a/src/timeline/TimelineView.qml +++ b/src/timeline/TimelineView.qml @@ -35,6 +35,11 @@ QQC2.ScrollView { */ property bool fileDropEnabled: true + /** + * @brief The TimelineMarkReadCondition to use for when messages should be marked as read automatically. + */ + required property int markReadCondition + /** * @brief Shift the view to the given event ID. */ @@ -54,7 +59,6 @@ QQC2.ScrollView { * All messages will be marked as read. */ function goToLastMessage() { - _private.room.markAllMessagesAsRead(); messageListView.positionViewAtBeginning(); } @@ -154,8 +158,8 @@ QQC2.ScrollView { } function onReadMarkerAdded() { - if (messageListView.allUnreadVisible()) { - _private.room.markAllMessagesAsRead(); + if (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.EntryVisible && messageListView.allUnreadVisible()) { + root.room.markAllMessagesAsRead(); } } @@ -163,6 +167,20 @@ QQC2.ScrollView { messageListView.positionViewAtBeginning(); _private.room.markAllMessagesAsRead(); } + + function onRoomAboutToChange(oldRoom, newRoom) { + if (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.Exit || + (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.ExitVisible && messageListView.allUnreadVisible()) + ) { + oldRoom.markAllMessagesAsRead(); + } + } + + function onRoomChanged(oldRoom, newRoom) { + if (root.markReadCondition == LibNeoChat.TimelineMarkReadCondition.Entry) { + newRoom.markAllMessagesAsRead(); + } + } } onAtYEndChanged: if (atYEnd && _private.hasScrolledUpBefore) { diff --git a/src/timeline/models/messagemodel.cpp b/src/timeline/models/messagemodel.cpp index 31b7009fb..b80eea6bd 100644 --- a/src/timeline/models/messagemodel.cpp +++ b/src/timeline/models/messagemodel.cpp @@ -34,18 +34,15 @@ MessageModel::MessageModel(QObject *parent) { connect(this, &MessageModel::newEventAdded, this, &MessageModel::createEventObjects); - connect(this, &MessageModel::modelAboutToBeReset, this, [this]() { - resetting = true; + connect(this, &MessageModel::modelAboutToReset, this, [this]() { + m_resetting = true; }); connect(this, &MessageModel::modelReset, this, [this]() { - resetting = false; + m_resetting = false; }); connect(this, &MessageModel::threadsEnabledChanged, this, [this]() { - Q_EMIT modelAboutToBeReset(); - beginResetModel(); - endResetModel(); - Q_EMIT modelResetComplete(); + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {IsThreadedRole}); }); } @@ -60,17 +57,25 @@ void MessageModel::setRoom(NeoChatRoom *room) return; } + const auto oldRoom = m_room; + Q_EMIT roomAboutToChange(oldRoom, room); clearModel(); - Q_EMIT modelAboutToBeReset(); - beginResetModel(); + if (!m_resetting) { + m_resetting = true; + Q_EMIT modelAboutToReset(); + beginResetModel(); + } m_room = room; if (m_room != nullptr) { m_room->setVisible(true); } - Q_EMIT roomChanged(); - endResetModel(); - Q_EMIT modelResetComplete(); + if (m_resetting) { + endResetModel(); + Q_EMIT modelResetComplete(); + m_resetting = false; + } + Q_EMIT roomChanged(oldRoom, m_room); } int MessageModel::timelineServerIndex() const @@ -444,7 +449,7 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event) // If a model already exists but now has no reactions remove it if (m_readMarkerModels[eventId]->rowCount() <= 0) { m_readMarkerModels.remove(eventId); - if (!resetting) { + if (!m_resetting) { refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); } } @@ -456,7 +461,7 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event) auto newModel = QSharedPointer(new ReadMarkerModel(eventId, m_room)); if (newModel->rowCount() > 0) { m_readMarkerModels[eventId] = newModel; - if (!resetting) { + if (!m_resetting) { refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); } } @@ -512,12 +517,18 @@ void MessageModel::clearModel() // HACK: Reset the model to a null room first to make sure QML dismantles // last room's objects before the room is actually changed - Q_EMIT modelAboutToBeReset(); - beginResetModel(); + if (!m_resetting) { + m_resetting = true; + Q_EMIT modelAboutToReset(); + beginResetModel(); + } m_room->disconnect(this); m_room = nullptr; - endResetModel(); - Q_EMIT modelResetComplete(); + if (m_resetting) { + endResetModel(); + Q_EMIT modelResetComplete(); + m_resetting = false; + } // Because we don't want any of the object deleted before the model is cleared. oldRoom->setVisible(false); diff --git a/src/timeline/models/messagemodel.h b/src/timeline/models/messagemodel.h index 095101647..00fa5c930 100644 --- a/src/timeline/models/messagemodel.h +++ b/src/timeline/models/messagemodel.h @@ -125,7 +125,12 @@ Q_SIGNALS: /** * @brief Emitted when the room is changed. */ - void roomChanged(); + void roomAboutToChange(NeoChatRoom *oldRoom, NeoChatRoom *newRoom); + + /** + * @brief Emitted when the room is changed. + */ + void roomChanged(NeoChatRoom *oldRoom, NeoChatRoom *newRoom); /** * @brief Emitted when the reader marker is added. @@ -140,7 +145,7 @@ Q_SIGNALS: /** * @brief Emitted when the model is about to reset. */ - void modelAboutToBeReset(); + void modelAboutToReset(); /** * @brief Emitted when the model has been reset. @@ -169,6 +174,9 @@ protected: virtual int timelineServerIndex() const; virtual std::optional> getEventForIndex(QModelIndex index) const; + bool m_resetting = false; + bool m_movingEvent = false; + void fullEventRefresh(int row); int refreshEventRoles(const QString &eventId, const QList &roles = {}); void refreshEventRoles(int row, const QList &roles = {}); @@ -182,9 +190,6 @@ protected: bool event(QEvent *event) override; private: - bool resetting = false; - bool movingEvent = false; - QMap> m_readMarkerModels; void createEventObjects(const Quotient::RoomEvent *event); diff --git a/src/timeline/models/timelinemessagemodel.cpp b/src/timeline/models/timelinemessagemodel.cpp index 17e86aa2e..71a5fdc73 100644 --- a/src/timeline/models/timelinemessagemodel.cpp +++ b/src/timeline/models/timelinemessagemodel.cpp @@ -28,12 +28,16 @@ void TimelineMessageModel::connectNewRoom() } connect(m_room, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { - m_initialized = true; - beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1); + if (!m_resetting) { + m_initialized = true; + beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1); + } }); connect(m_room, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { - m_initialized = true; - beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); + if (!m_resetting) { + m_initialized = true; + beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); + } }); connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) { if (m_initialized) { @@ -58,17 +62,21 @@ void TimelineMessageModel::connectNewRoom() }); #if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0) connect(m_room, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) { - m_initialized = true; Q_EMIT newEventAdded(event); Q_EMIT newLocalUserEventAdded(); - beginInsertRows({}, 0, 0); - endInsertRows(); + if (!m_resetting) { + beginInsertRows({}, 0, 0); + endInsertRows(); + } }); #else connect(m_room, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) { m_initialized = true; Q_EMIT newEventAdded(event); - beginInsertRows({}, 0, 0); + if (!m_resetting) { + beginInsertRows({}, 0, 0); + endInsertRows(); + } }); connect(m_room, &Room::pendingEventAdded, this, &TimelineMessageModel::endInsertRows); #endif @@ -78,15 +86,15 @@ void TimelineMessageModel::connectNewRoom() return; // No need to move anything, just refresh } - movingEvent = true; + m_movingEvent = true; // Reverse i because row 0 is bottommost in the model const auto row = timelineServerIndex() - i - 1; beginMoveRows({}, row, row, {}, timelineServerIndex()); }); connect(m_room, &Room::pendingEventMerged, this, [this] { - if (movingEvent) { + if (m_movingEvent) { endMoveRows(); - movingEvent = false; + m_movingEvent = false; } fullEventRefresh(timelineServerIndex()); refreshLastUserEvents(0); diff --git a/src/timeline/models/timelinemessagemodel.h b/src/timeline/models/timelinemessagemodel.h index e26d71d38..9c3494971 100644 --- a/src/timeline/models/timelinemessagemodel.h +++ b/src/timeline/models/timelinemessagemodel.h @@ -48,8 +48,6 @@ private: std::optional> getEventForIndex(QModelIndex index) const override; int rowBelowInserted = -1; - bool resetting = false; - bool movingEvent = false; int timelineServerIndex() const override;