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.
{width=480 height=262}
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
32
src/libneochat/enums/timelinemarkreadcondition.h
Normal file
32
src/libneochat/enums/timelinemarkreadcondition.h
Normal 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);
|
||||
};
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user