summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorHiers <47784553+Hiers@users.noreply.github.com>2022-09-11 23:05:20 +0000
committerGitHub <noreply@github.com>2022-09-11 23:05:20 +0000
commit8071b192b8cecf0b0f422d74678038dd3afbe3bc (patch)
treee5340dccf4ad742a86199e82758a9de5eff038fb /src
parentMerge pull request #1163 from foresto/log-options (diff)
downloadnheko-8071b192b8cecf0b0f422d74678038dd3afbe3bc.tar.xz
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.
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp79
-rw-r--r--src/Cache.h4
-rw-r--r--src/Cache_p.h3
-rw-r--r--src/ChatPage.h6
-rw-r--r--src/timeline/RoomlistModel.cpp18
-rw-r--r--src/timeline/RoomlistModel.h6
-rw-r--r--src/timeline/TimelineModel.cpp47
-rw-r--r--src/timeline/TimelineModel.h9
8 files changed, 153 insertions, 19 deletions
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<mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(
+                &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<mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(
-                    &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<std::pair<uint64_t, std::string>>
+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<uint64_t>(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>();
+                std::string_view temp;
+                idx = lmdb::from_sv<uint64_t>(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<uint64_t>
 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<std::pair<uint64_t, std::string>>
+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<std::string> &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<uint64_t>
 getEventIndex(const std::string &room_id, std::string_view event_id);
 std::optional<std::pair<uint64_t, std::string>>
 lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
+std::optional<std::pair<uint64_t, std::string>>
+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<std::string> &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<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id);
     std::optional<std::pair<uint64_t, std::string>>
     lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
+    std::optional<std::pair<uint64_t, std::string>>
+    lastVisibleEvent(const std::string &room_id, std::string_view event_id);
     std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
     std::optional<uint64_t> 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<bool> 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<mtx::events::state::Member>;
     using Memberships = std::map<std::string, Membership>;
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<TimelineModel> 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<QString> &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<typename T>
 void
 TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> 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<QString> &event_ids);
+    void updateLastReadId(QString currentRoomId);
+    void lastReadIdOnWindowFocus();
+    void checkAfterFetch();
     QVariantMap getDump(const QString &eventId, const QString &relatedTo) const;
     void updateTypingUsers(const std::vector<QString> &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<class T>
@@ -497,6 +505,7 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy
         msgCopy.type                      = eventType;
         emit newMessageToSend(msgCopy);
     }
+
     resetReply();
     resetEdit();
 }