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;