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}
This commit is contained in:
James Graham
2025-07-04 14:36:36 +01:00
parent d5d291396d
commit 3d9d211d25
11 changed files with 160 additions and 42 deletions

View File

@@ -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);

View File

@@ -78,6 +78,12 @@
<label>Use a compact room list layout</label>
<default>false</default>
</entry>
<entry name="MarkReadCondition" type="Enum">
<label>The sort order for the rooms in the list.</label>
<choices name="::TimelineMarkReadCondition::Condition">
</choices>
<default>2</default>
</entry>
<entry name="ShowStateEvent" type="bool">
<label>Show state events in the timeline</label>
<default>true</default>

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @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);
};

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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<ReadMarkerModel>(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);

View File

@@ -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<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const;
bool m_resetting = false;
bool m_movingEvent = false;
void fullEventRefresh(int row);
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
void refreshEventRoles(int row, const QList<int> &roles = {});
@@ -182,9 +190,6 @@ protected:
bool event(QEvent *event) override;
private:
bool resetting = false;
bool movingEvent = false;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
void createEventObjects(const Quotient::RoomEvent *event);

View File

@@ -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);

View File

@@ -48,8 +48,6 @@ private:
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
int rowBelowInserted = -1;
bool resetting = false;
bool movingEvent = false;
int timelineServerIndex() const override;