Treat read markers as item in the model
This commit is contained in:
@@ -182,13 +182,4 @@ QQC2.ItemDelegate {
|
|||||||
visible: active
|
visible: active
|
||||||
sourceComponent: ReactionDelegate { }
|
sourceComponent: ReactionDelegate { }
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width * 0.9
|
|
||||||
x: parent.width * 0.05
|
|
||||||
height: Kirigami.Units.smallSpacing / 2
|
|
||||||
anchors.top: loader.bottom
|
|
||||||
visible: readMarker
|
|
||||||
color: Kirigami.Theme.positiveTextColor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,22 +232,11 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
model: !isLoaded ? undefined : sortedMessageEventModel
|
model: !isLoaded ? undefined : sortedMessageEventModel
|
||||||
|
|
||||||
|
onContentYChanged: fetchMoreContent()
|
||||||
|
|
||||||
onContentYChanged: updateReadMarker()
|
function fetchMoreContent() {
|
||||||
onCountChanged: updateReadMarker()
|
if(!noNeedMoreContent && contentY - 5000 < originY) {
|
||||||
|
|
||||||
function updateReadMarker() {
|
|
||||||
if(!noNeedMoreContent && contentY - 5000 < originY)
|
|
||||||
currentRoom.getPreviousContent(20);
|
currentRoom.getPreviousContent(20);
|
||||||
const index = currentRoom.readMarkerEventId ? eventToIndex(currentRoom.readMarkerEventId) : 0
|
|
||||||
if(index === -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(firstVisibleIndex() === -1 || lastVisibleIndex() === -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(index < firstVisibleIndex() && index > lastVisibleIndex()) {
|
|
||||||
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIndex(), 0), MessageEventModel.EventIdRole)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +525,71 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "readMarker"
|
||||||
|
delegate: QQC2.ItemDelegate {
|
||||||
|
padding: Kirigami.Units.largeSpacing
|
||||||
|
topInset: Kirigami.Units.largeSpacing
|
||||||
|
topPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
width: ListView.view.width - Kirigami.Units.gridUnit
|
||||||
|
x: Kirigami.Units.gridUnit / 2
|
||||||
|
contentItem: QQC2.Label {
|
||||||
|
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Kirigami.ShadowedRectangle {
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
opacity: 0.6
|
||||||
|
radius: Kirigami.Units.smallSpacing
|
||||||
|
shadow.size: Kirigami.Units.smallSpacing
|
||||||
|
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
|
||||||
|
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
|
||||||
|
border.width: Kirigami.Units.devicePixelRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: makeMeDisapearTimer
|
||||||
|
interval: Kirigami.Units.humanMoment * 2
|
||||||
|
onTriggered: currentRoom.markAllMessagesAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView.onPooled: makeMeDisapearTimer.stop()
|
||||||
|
|
||||||
|
ListView.onAdd: {
|
||||||
|
const view = ListView.view;
|
||||||
|
if (view.atYEnd) {
|
||||||
|
makeMeDisapearTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the read marker is visible and we are at the end of the list,
|
||||||
|
// start the makeMeDisapearTimer
|
||||||
|
Connections {
|
||||||
|
target: ListView.view
|
||||||
|
function onAtYEndChanged() {
|
||||||
|
makeMeDisapearTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ListView.onRemove: {
|
||||||
|
const view = ListView.view;
|
||||||
|
|
||||||
|
if (view.atYEnd) {
|
||||||
|
// easy case just mark everything as read
|
||||||
|
currentRoom.markAllMessagesAsRead();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the last visible index
|
||||||
|
const lastVisibleIdx = lastVisibleIndex();
|
||||||
|
|
||||||
|
if (lastVisibleIdx < index) {
|
||||||
|
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: "other"
|
roleValue: "other"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <QTimeZone>
|
#include <QTimeZone>
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
#include <KFormat>
|
||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
@@ -34,7 +35,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[ContentRole] = "content";
|
roles[ContentRole] = "content";
|
||||||
roles[ContentTypeRole] = "contentType";
|
roles[ContentTypeRole] = "contentType";
|
||||||
roles[HighlightRole] = "isHighlighted";
|
roles[HighlightRole] = "isHighlighted";
|
||||||
roles[ReadMarkerRole] = "readMarker";
|
|
||||||
roles[SpecialMarksRole] = "marks";
|
roles[SpecialMarksRole] = "marks";
|
||||||
roles[LongOperationRole] = "progressInfo";
|
roles[LongOperationRole] = "progressInfo";
|
||||||
roles[AnnotationRole] = "annotation";
|
roles[AnnotationRole] = "annotation";
|
||||||
@@ -90,6 +90,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
|
|
||||||
m_currentRoom = room;
|
m_currentRoom = room;
|
||||||
if (room) {
|
if (room) {
|
||||||
|
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
|
||||||
room->setDisplayed();
|
room->setDisplayed();
|
||||||
if (m_currentRoom->timelineSize() < 10) {
|
if (m_currentRoom->timelineSize() < 10) {
|
||||||
room->getPreviousContent(50);
|
room->getPreviousContent(50);
|
||||||
@@ -141,6 +142,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) {
|
connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) {
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
if (!m_lastReadEventIndex.isValid()) {
|
||||||
|
// no read marker, so see if we need to create one.
|
||||||
|
moveReadMarker(QString(), m_currentRoom->readMarkerEventId());
|
||||||
|
}
|
||||||
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
if (biggest < m_currentRoom->maxTimelineIndex()) {
|
||||||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
|
||||||
refreshEventRoles(rowBelowInserted, {ShowAuthorRole});
|
refreshEventRoles(rowBelowInserted, {ShowAuthorRole});
|
||||||
@@ -170,9 +175,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
refreshRow(timelineBaseIndex()); // Refresh the looks
|
refreshRow(timelineBaseIndex()); // Refresh the looks
|
||||||
refreshLastUserEvents(0);
|
refreshLastUserEvents(0);
|
||||||
if (m_currentRoom->timelineSize() > 1) { // Refresh above
|
|
||||||
refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole});
|
|
||||||
}
|
|
||||||
if (timelineBaseIndex() > 0) { // Refresh below, see #312
|
if (timelineBaseIndex() > 0) { // Refresh below, see #312
|
||||||
refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole});
|
refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole});
|
||||||
}
|
}
|
||||||
@@ -182,10 +184,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
beginRemoveRows({}, i, i);
|
beginRemoveRows({}, i, i);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
|
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
|
||||||
connect(m_currentRoom, &Room::readMarkerMoved, this, [this] {
|
connect(m_currentRoom, &Room::readMarkerMoved, this, &MessageEventModel::moveReadMarker);
|
||||||
refreshEventRoles(std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()), {ReadMarkerRole});
|
|
||||||
refreshEventRoles(lastReadEventId, {ReadMarkerRole});
|
|
||||||
});
|
|
||||||
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
|
||||||
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
|
||||||
});
|
});
|
||||||
@@ -199,10 +198,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
|
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
|
||||||
connect(m_currentRoom, &Room::readMarkerForUserMoved, this, [=](User *, const QString &fromEventId, const QString &toEventId) {
|
|
||||||
refreshEventRoles(fromEventId, {UserMarkerRole});
|
|
||||||
refreshEventRoles(toEventId, {UserMarkerRole});
|
|
||||||
});
|
|
||||||
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [=] {
|
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [=] {
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -235,6 +230,43 @@ void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles)
|
|||||||
Q_EMIT dataChanged(idx, idx, roles);
|
Q_EMIT dataChanged(idx, idx, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MessageEventModel::moveReadMarker(const QString &fromEventId, const QString &toEventId)
|
||||||
|
{
|
||||||
|
const auto timelineIt = m_currentRoom->findInTimeline(toEventId);
|
||||||
|
if (timelineIt == m_currentRoom->timelineEdge()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newRow = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
||||||
|
|
||||||
|
if (!m_lastReadEventIndex.isValid()) {
|
||||||
|
// Not valid index means we don't display any marker yet, in this case
|
||||||
|
// we create the new index and insert the row in case the read marker
|
||||||
|
// need to be displayed.
|
||||||
|
if (newRow > timelineBaseIndex()) {
|
||||||
|
// The user didn't read all the messages yet.
|
||||||
|
beginInsertRows({}, newRow, newRow);
|
||||||
|
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||||
|
endInsertRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The user read all the messages and we didn't display any read marker yet
|
||||||
|
// => do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newRow <= timelineBaseIndex()) {
|
||||||
|
// The user read all the messages => remove read marker
|
||||||
|
beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row());
|
||||||
|
m_lastReadEventIndex = QModelIndex();
|
||||||
|
endRemoveRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user didn't read all the messages yet but moved the reader marker.
|
||||||
|
beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow);
|
||||||
|
m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0));
|
||||||
|
endMoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &roles)
|
int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &roles)
|
||||||
{
|
{
|
||||||
// On 64-bit platforms, difference_type for std containers is long long
|
// On 64-bit platforms, difference_type for std containers is long long
|
||||||
@@ -327,7 +359,15 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
|
|||||||
if (!m_currentRoom || parent.isValid()) {
|
if (!m_currentRoom || parent.isValid()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return m_currentRoom->timelineSize();
|
|
||||||
|
const auto firstIt = m_currentRoom->messageEvents().crbegin();
|
||||||
|
if (firstIt != m_currentRoom->messageEvents().crend()) {
|
||||||
|
const auto &firstEvt = **firstIt;
|
||||||
|
return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0);
|
||||||
|
} else {
|
||||||
|
return m_currentRoom->timelineSize();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
|
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
|
||||||
@@ -354,7 +394,22 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool isPending = row < timelineBaseIndex();
|
bool isPending = row < timelineBaseIndex();
|
||||||
const auto timelineIt = m_currentRoom->messageEvents().crbegin() + std::max(0, row - timelineBaseIndex());
|
|
||||||
|
if (m_lastReadEventIndex.row() == row) {
|
||||||
|
switch(role) {
|
||||||
|
case EventTypeRole:
|
||||||
|
return QStringLiteral("readMarker");
|
||||||
|
case TimeRole:
|
||||||
|
{
|
||||||
|
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
|
||||||
|
const KFormat format;
|
||||||
|
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto timelineIt = m_currentRoom->messageEvents().crbegin() + std::max(0, row - timelineBaseIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0));
|
||||||
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
||||||
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
||||||
|
|
||||||
@@ -457,10 +512,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return m_currentRoom->isEventHighlighted(&evt);
|
return m_currentRoom->isEventHighlighted(&evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReadMarkerRole) {
|
|
||||||
return evt.id() == lastReadEventId && row > timelineBaseIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == SpecialMarksRole) {
|
if (role == SpecialMarksRole) {
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return pendingIt->deliveryStatus();
|
return pendingIt->deliveryStatus();
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public:
|
|||||||
ContentRole,
|
ContentRole,
|
||||||
ContentTypeRole,
|
ContentTypeRole,
|
||||||
HighlightRole,
|
HighlightRole,
|
||||||
ReadMarkerRole,
|
|
||||||
SpecialMarksRole,
|
SpecialMarksRole,
|
||||||
LongOperationRole,
|
LongOperationRole,
|
||||||
AnnotationRole,
|
AnnotationRole,
|
||||||
@@ -45,13 +44,6 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(EventRoles)
|
Q_ENUM(EventRoles)
|
||||||
|
|
||||||
enum BubbleShapes {
|
|
||||||
NoShape = 0,
|
|
||||||
BeginShape,
|
|
||||||
MiddleShape,
|
|
||||||
EndShape,
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit MessageEventModel(QObject *parent = nullptr);
|
explicit MessageEventModel(QObject *parent = nullptr);
|
||||||
~MessageEventModel() override;
|
~MessageEventModel() override;
|
||||||
|
|
||||||
@@ -76,6 +68,7 @@ private Q_SLOTS:
|
|||||||
private:
|
private:
|
||||||
NeoChatRoom *m_currentRoom = nullptr;
|
NeoChatRoom *m_currentRoom = nullptr;
|
||||||
QString lastReadEventId;
|
QString lastReadEventId;
|
||||||
|
QPersistentModelIndex m_lastReadEventIndex;
|
||||||
int rowBelowInserted = -1;
|
int rowBelowInserted = -1;
|
||||||
bool movingEvent = false;
|
bool movingEvent = false;
|
||||||
|
|
||||||
@@ -86,6 +79,7 @@ private:
|
|||||||
void refreshLastUserEvents(int baseTimelineRow);
|
void refreshLastUserEvents(int baseTimelineRow);
|
||||||
void refreshEventRoles(int row, const QVector<int> &roles = {});
|
void refreshEventRoles(int row, const QVector<int> &roles = {});
|
||||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||||
|
void moveReadMarker(const QString &fromEventId, const QString &toEventId);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
|||||||
Reference in New Issue
Block a user