From 8071b192b8cecf0b0f422d74678038dd3afbe3bc Mon Sep 17 00:00:00 2001 From: Hiers <47784553+Hiers@users.noreply.github.com> Date: Sun, 11 Sep 2022 23:05:20 +0000 Subject: [PATCH] Line to indicate first unread message (#1147) * First draft of unread line feature. * Minor visual fix. * Removed unnecessary ternary operator. * Extended unread line functionality to work on minimised window or focusing another window. * Fix for unread line not showing when last read message is hidden. * Minor performance improvement. Fix for misbehaving event2order DB at application start. * Fix for possible performance issues when user has joined a large number of rooms. * Fix for breaking macos and clazy builds. * Changed on windows focus function to refresh unread line if room is unread. * Unread line is removed when user sends a message. * Linting. * Fixed unread line to work in standalone room windows. * Switch isRoomUnread for index 0. * Merged try/catch blocks. * Fix for crash on opening a room invite. * Call fullyReadEventId function when used instead of storing it and passing it through. * Function that was meant to sync the unread line was relying on an async function, oops. * Linting again. * More linting... * Minor changes. --- resources/qml/MessageView.qml | 1 + resources/qml/RoomList.qml | 2 + resources/qml/TimelineRow.qml | 17 +++++++- src/Cache.cpp | 79 ++++++++++++++++++++++++++++++---- src/Cache.h | 4 ++ src/Cache_p.h | 3 ++ src/ChatPage.h | 6 +-- src/timeline/RoomlistModel.cpp | 18 +++++--- src/timeline/RoomlistModel.h | 6 +-- src/timeline/TimelineModel.cpp | 47 ++++++++++++++++++++ src/timeline/TimelineModel.h | 9 ++++ 11 files changed, 172 insertions(+), 20 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index fd533229..2e1d9398 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -458,6 +458,7 @@ Item { encryptionError: wrapper.encryptionError timestamp: wrapper.timestamp status: wrapper.status + index: wrapper.index relatedEventCacheBuster: wrapper.relatedEventCacheBuster y: section.visible && section.active ? section.y + section.height : 0 diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 5ce69d97..a4628aa7 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -108,6 +108,8 @@ Page { timelineRoot: timelineView windowTarget: roomWindowW } + + onActiveChanged: { room.lastReadIdOnWindowFocus(); } } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 3a7bf561..dc640099 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -44,12 +44,13 @@ AbstractButton { required property int duration required property var timestamp required property int status + required property int index required property int relatedEventCacheBuster hoverEnabled: true width: parent.width - height: row.height+(reactionRow.height > 0 ? reactionRow.height-2 : 0 ) + height: row.height+(reactionRow.height > 0 ? reactionRow.height-2 : 0 )+unreadRow.height Rectangle { color: (Settings.messageHoverHighlight && hovered) ? Nheko.colors.alternateBase : "transparent" @@ -277,6 +278,7 @@ AbstractButton { } } } + Reactions { anchors { top: row.bottom @@ -292,4 +294,17 @@ AbstractButton { reactions: r.reactions eventId: r.eventId } + + Rectangle { + id: unreadRow + anchors { + top: reactionRow.bottom + topMargin: 5 + } + color: Nheko.colors.highlight + width: row.maxWidth + visible: (r.index > 0 && (chat.model.fullyReadEventId == r.eventId)) + height: visible ? 3 : 0 + + } } diff --git a/src/Cache.cpp b/src/Cache.cpp index 90e93bed..02b456db 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1537,6 +1537,21 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei } } +std::string +Cache::getFullyReadEventId(const std::string &room_id) +{ + auto txn = ro_txn(env_); + + if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { + if (auto fr = + std::get_if>( + &ev.value())) { + return fr->content.event_id; + } + } + return std::string(); +} + void Cache::calculateRoomReadStatus() { @@ -1561,14 +1576,7 @@ Cache::calculateRoomReadStatus(const std::string &room_id) const auto last_event_id = getLastEventId(txn, room_id); const auto localUser = utils::localUser().toStdString(); - std::string fullyReadEventId; - if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { - if (auto fr = - std::get_if>( - &ev.value())) { - fullyReadEventId = fr->content.event_id; - } - } + std::string fullyReadEventId = getFullyReadEventId(room_id); if (last_event_id.empty() || fullyReadEventId.empty()) return true; @@ -2503,6 +2511,50 @@ Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view even } } +std::optional> +Cache::lastVisibleEvent(const std::string &room_id, std::string_view event_id) +{ + if (room_id.empty() || event_id.empty()) + return {}; + + auto txn = ro_txn(env_); + lmdb::dbi orderDb; + lmdb::dbi eventOrderDb; + lmdb::dbi timelineDb; + try { + orderDb = getEventToOrderDb(txn, room_id); + eventOrderDb = getEventOrderDb(txn, room_id); + timelineDb = getMessageToOrderDb(txn, room_id); + + std::string_view indexVal; + + bool success = orderDb.get(txn, event_id, indexVal); + if (!success) { + return {}; + } + + uint64_t idx = lmdb::from_sv(indexVal); + std::string evId{event_id}; + + auto cursor = lmdb::cursor::open(txn, eventOrderDb); + if (cursor.get(indexVal, event_id, MDB_SET)) { + do { + evId = nlohmann::json::parse(event_id)["event_id"].get(); + std::string_view temp; + idx = lmdb::from_sv(indexVal); + if (timelineDb.get(txn, evId, temp)) { + return std::pair{idx, evId}; + } + } while (cursor.get(indexVal, event_id, MDB_PREV)); + } + + return std::pair{idx, evId}; + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Failed to get last visible event after {}", event_id, e.what()); + return {}; + } +} + std::optional Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) { @@ -5317,6 +5369,12 @@ lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) return instance_->lastInvisibleEventAfter(room_id, event_id); } +std::optional> +lastVisibleEvent(const std::string &room_id, std::string_view event_id) +{ + return instance_->lastVisibleEvent(room_id, event_id); +} + RoomInfo singleRoomInfo(const std::string &room_id) { @@ -5336,6 +5394,11 @@ getRoomInfo(const std::vector &rooms) //! Calculates which the read status of a room. //! Whether all the events in the timeline have been read. +std::string +getFullyReadEventId(const std::string &room_id) +{ + return instance_->getFullyReadEventId(room_id); +} bool calculateRoomReadStatus(const std::string &room_id) { diff --git a/src/Cache.h b/src/Cache.h index 1545f7e8..7ea659ec 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -152,6 +152,8 @@ std::optional getEventIndex(const std::string &room_id, std::string_view event_id); std::optional> lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); +std::optional> +lastVisibleEvent(const std::string &room_id, std::string_view event_id); RoomInfo singleRoomInfo(const std::string &room_id); @@ -160,6 +162,8 @@ getRoomInfo(const std::vector &rooms); //! Calculates which the read status of a room. //! Whether all the events in the timeline have been read. +std::string +getFullyReadEventId(const std::string &room_id); bool calculateRoomReadStatus(const std::string &room_id); void diff --git a/src/Cache_p.h b/src/Cache_p.h index cd42fa3e..839688f1 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -169,6 +169,7 @@ public: //! Calculates which the read status of a room. //! Whether all the events in the timeline have been read. + std::string getFullyReadEventId(const std::string &room_id); bool calculateRoomReadStatus(const std::string &room_id); void calculateRoomReadStatus(); @@ -212,6 +213,8 @@ public: std::optional getEventIndex(const std::string &room_id, std::string_view event_id); std::optional> lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); + std::optional> + lastVisibleEvent(const std::string &room_id, std::string_view event_id); std::optional getTimelineEventId(const std::string &room_id, uint64_t index); std::optional getArrivalIndex(const std::string &room_id, std::string_view event_id); diff --git a/src/ChatPage.h b/src/ChatPage.h index c0f0b559..fd1711c5 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -74,6 +74,9 @@ public: void startChat(QString userid, std::optional encryptionEnabled); + //! Check if the given room is currently open. + bool isRoomActive(const QString &room_id); + public slots: bool handleMatrixUri(QString uri); bool handleMatrixUri(const QUrl &uri); @@ -193,9 +196,6 @@ private: void getProfileInfo(); void getBackupVersion(); - //! Check if the given room is currently open. - bool isRoomActive(const QString &room_id); - using UserID = QString; using Membership = mtx::events::StateEvent; using Memberships = std::map; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 03abd3d5..6e95ef8e 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -283,6 +283,14 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) QSharedPointer newRoom(new TimelineModel(manager, room_id)); newRoom->setDecryptDescription(ChatPage::instance()->userSettings()->decryptSidebar()); + connect(this, + &RoomlistModel::currentRoomChanged, + newRoom.data(), + &TimelineModel::updateLastReadId); + connect(MainWindow::instance(), + &MainWindow::activeChanged, + newRoom.data(), + &TimelineModel::lastReadIdOnWindowFocus); connect(newRoom.data(), &TimelineModel::newEncryptedImage, MainWindow::instance()->imageProvider(), @@ -383,7 +391,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) currentRoomPreview_->roomid() == room_id) { currentRoom_ = models.value(room_id); currentRoomPreview_.reset(); - emit currentRoomChanged(); + emit currentRoomChanged(room_id); } for (auto p : previewsToAdd) { @@ -644,7 +652,7 @@ RoomlistModel::clear() invites.clear(); roomids.clear(); currentRoom_ = nullptr; - emit currentRoomChanged(); + emit currentRoomChanged(""); endResetModel(); } @@ -743,14 +751,14 @@ RoomlistModel::setCurrentRoom(QString roomid) if (roomid.isEmpty()) { currentRoom_ = nullptr; currentRoomPreview_ = {}; - emit currentRoomChanged(); + emit currentRoomChanged(""); } nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); if (models.contains(roomid)) { currentRoom_ = models.value(roomid); currentRoomPreview_.reset(); - emit currentRoomChanged(); + emit currentRoomChanged(currentRoom_->roomId()); nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) { currentRoom_ = nullptr; @@ -781,7 +789,7 @@ RoomlistModel::setCurrentRoom(QString roomid) currentRoomPreview_->roomid_.toStdString()); } - emit currentRoomChanged(); + emit currentRoomChanged(""); } } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 61bf2e7c..2f2ea066 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -116,7 +116,7 @@ public slots: { currentRoom_ = nullptr; currentRoomPreview_.reset(); - emit currentRoomChanged(); + emit currentRoomChanged(""); } private slots: @@ -124,7 +124,7 @@ private slots: signals: void totalUnreadMessageCountUpdated(int unreadMessages); - void currentRoomChanged(); + void currentRoomChanged(QString currentRoomId); void fetchedPreview(QString roomid, RoomInfo info); private: @@ -218,7 +218,7 @@ public slots: void updateHiddenTagsAndSpaces(); signals: - void currentRoomChanged(); + void currentRoomChanged(QString currentRoomId); private: short int calculateImportance(const QModelIndex &idx) const; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b7122db1..eaf85b2a 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -427,6 +427,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj setPaginationInProgress(false); updateLastMessage(); }); + connect(&events, &EventStore::fetchedMore, this, &TimelineModel::checkAfterFetch); connect(&events, &EventStore::startDMVerification, this, @@ -977,6 +978,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) emit encryptionChanged(); } } + updateLastMessage(); } @@ -1370,6 +1372,48 @@ TimelineModel::markEventsAsRead(const std::vector &event_ids) } } +void +TimelineModel::updateLastReadId(QString currentRoomId) +{ + if (currentRoomId == room_id_) { + last_event_id = cache::getFullyReadEventId(room_id_.toStdString()); + auto lastVisibleEventIndexAndId = + cache::lastVisibleEvent(room_id_.toStdString(), last_event_id); + if (lastVisibleEventIndexAndId) { + fullyReadEventId_ = lastVisibleEventIndexAndId->second; + emit fullyReadEventIdChanged(); + } + } +} + +void +TimelineModel::lastReadIdOnWindowFocus() +{ + /* this stops it from removing the line when focusing another window + * and from removing the line when refocusing nheko */ + if (ChatPage::instance()->isRoomActive(room_id_) && + cache::calculateRoomReadStatus(room_id_.toStdString())) { + updateLastReadId(room_id_); + } +} + +/* + * if the event2order db didn't have the messages we needed when the room was opened + * try again after these new messages were fetched + */ +void +TimelineModel::checkAfterFetch() +{ + if (fullyReadEventId_.empty()) { + auto lastVisibleEventIndexAndId = + cache::lastVisibleEvent(room_id_.toStdString(), last_event_id); + if (lastVisibleEventIndexAndId) { + fullyReadEventId_ = lastVisibleEventIndexAndId->second; + emit fullyReadEventIdChanged(); + } + } +} + template void TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent msg, mtx::events::EventType eventType) @@ -1550,6 +1594,9 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) event); std::visit(SendMessageVisitor{this}, event); + + fullyReadEventId_ = this->EventId; + emit fullyReadEventIdChanged(); } void diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 47fd27f1..295bc69b 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -189,6 +189,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QStringList widgetLinks READ widgetLinks NOTIFY widgetLinksChanged) Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) + Q_PROPERTY(QString fullyReadEventId READ fullyReadEventId NOTIFY fullyReadEventIdChanged) Q_PROPERTY(bool isSpace READ isSpace CONSTANT) Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged) Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged) @@ -325,6 +326,7 @@ public: bool isSpace() const { return isSpace_; } bool isEncrypted() const { return isEncrypted_; } + QString fullyReadEventId() const { return QString::fromStdString(fullyReadEventId_); } crypto::Trust trustlevel() const; int roomMemberCount() const; bool isDirect() const { return roomMemberCount() <= 2; } @@ -344,6 +346,9 @@ public slots: int currentIndex() const { return idToIndex(currentId); } void eventShown(); void markEventsAsRead(const std::vector &event_ids); + void updateLastReadId(QString currentRoomId); + void lastReadIdOnWindowFocus(); + void checkAfterFetch(); QVariantMap getDump(const QString &eventId, const QString &relatedTo) const; void updateTypingUsers(const std::vector &users) { @@ -427,6 +432,7 @@ signals: void updateFlowEventId(std::string event_id); void encryptionChanged(); + void fullyReadEventIdChanged(); void trustlevelChanged(); void roomNameChanged(); void roomTopicChanged(); @@ -480,6 +486,8 @@ private: bool m_paginationInProgress = false; bool isSpace_ = false; bool isEncrypted_ = false; + std::string last_event_id; + std::string fullyReadEventId_; }; template @@ -497,6 +505,7 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy msgCopy.type = eventType; emit newMessageToSend(msgCopy); } + resetReply(); resetEdit(); }