diff --git a/imports/NeoChat/Component/Timeline/TimelineContainer.qml b/imports/NeoChat/Component/Timeline/TimelineContainer.qml index 0e3d4c320..adfbe22eb 100644 --- a/imports/NeoChat/Component/Timeline/TimelineContainer.qml +++ b/imports/NeoChat/Component/Timeline/TimelineContainer.qml @@ -182,13 +182,4 @@ QQC2.ItemDelegate { visible: active 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 - } } diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index 03ad2c2df..5ecaeecf4 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -232,22 +232,11 @@ Kirigami.ScrollablePage { model: !isLoaded ? undefined : sortedMessageEventModel + onContentYChanged: fetchMoreContent() - onContentYChanged: updateReadMarker() - onCountChanged: updateReadMarker() - - function updateReadMarker() { - if(!noNeedMoreContent && contentY - 5000 < originY) + function fetchMoreContent() { + if(!noNeedMoreContent && contentY - 5000 < originY) { 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 { roleValue: "other" diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 2c8e7c1c0..88f25e74c 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -19,6 +19,7 @@ #include #include +#include #include "utils.h" @@ -34,7 +35,6 @@ QHash MessageEventModel::roleNames() const roles[ContentRole] = "content"; roles[ContentTypeRole] = "contentType"; roles[HighlightRole] = "isHighlighted"; - roles[ReadMarkerRole] = "readMarker"; roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; roles[AnnotationRole] = "annotation"; @@ -90,6 +90,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) m_currentRoom = room; if (room) { + m_lastReadEventIndex = QPersistentModelIndex(QModelIndex()); room->setDisplayed(); if (m_currentRoom->timelineSize() < 10) { room->getPreviousContent(50); @@ -141,6 +142,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room) }); connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) { 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()) { auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; refreshEventRoles(rowBelowInserted, {ShowAuthorRole}); @@ -170,9 +175,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room) } refreshRow(timelineBaseIndex()); // Refresh the looks refreshLastUserEvents(0); - if (m_currentRoom->timelineSize() > 1) { // Refresh above - refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole}); - } if (timelineBaseIndex() > 0) { // Refresh below, see #312 refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole}); } @@ -182,10 +184,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room) beginRemoveRows({}, i, i); }); connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows); - connect(m_currentRoom, &Room::readMarkerMoved, this, [this] { - refreshEventRoles(std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()), {ReadMarkerRole}); - refreshEventRoles(lastReadEventId, {ReadMarkerRole}); - }); + connect(m_currentRoom, &Room::readMarkerMoved, this, &MessageEventModel::moveReadMarker); connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { 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::fileTransferFailed, 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, [=] { beginResetModel(); endResetModel(); @@ -235,6 +230,43 @@ void MessageEventModel::refreshEventRoles(int row, const QVector &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 &roles) { // 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()) { 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) @@ -354,7 +394,22 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const }; 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 &evt = isPending ? **pendingIt : **timelineIt; @@ -457,10 +512,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return m_currentRoom->isEventHighlighted(&evt); } - if (role == ReadMarkerRole) { - return evt.id() == lastReadEventId && row > timelineBaseIndex(); - } - if (role == SpecialMarksRole) { if (isPending) { return pendingIt->deliveryStatus(); diff --git a/src/messageeventmodel.h b/src/messageeventmodel.h index 443d92eae..df0723a99 100644 --- a/src/messageeventmodel.h +++ b/src/messageeventmodel.h @@ -24,7 +24,6 @@ public: ContentRole, ContentTypeRole, HighlightRole, - ReadMarkerRole, SpecialMarksRole, LongOperationRole, AnnotationRole, @@ -45,13 +44,6 @@ public: }; Q_ENUM(EventRoles) - enum BubbleShapes { - NoShape = 0, - BeginShape, - MiddleShape, - EndShape, - }; - explicit MessageEventModel(QObject *parent = nullptr); ~MessageEventModel() override; @@ -76,6 +68,7 @@ private Q_SLOTS: private: NeoChatRoom *m_currentRoom = nullptr; QString lastReadEventId; + QPersistentModelIndex m_lastReadEventIndex; int rowBelowInserted = -1; bool movingEvent = false; @@ -86,6 +79,7 @@ private: void refreshLastUserEvents(int baseTimelineRow); void refreshEventRoles(int row, const QVector &roles = {}); int refreshEventRoles(const QString &eventId, const QVector &roles = {}); + void moveReadMarker(const QString &fromEventId, const QString &toEventId); Q_SIGNALS: void roomChanged();