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.
This commit is contained in:
Hiers 2022-09-11 23:05:20 +00:00 committed by GitHub
parent 02adcfdc38
commit 8071b192b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 172 additions and 20 deletions

View file

@ -458,6 +458,7 @@ Item {
encryptionError: wrapper.encryptionError encryptionError: wrapper.encryptionError
timestamp: wrapper.timestamp timestamp: wrapper.timestamp
status: wrapper.status status: wrapper.status
index: wrapper.index
relatedEventCacheBuster: wrapper.relatedEventCacheBuster relatedEventCacheBuster: wrapper.relatedEventCacheBuster
y: section.visible && section.active ? section.y + section.height : 0 y: section.visible && section.active ? section.y + section.height : 0

View file

@ -108,6 +108,8 @@ Page {
timelineRoot: timelineView timelineRoot: timelineView
windowTarget: roomWindowW windowTarget: roomWindowW
} }
onActiveChanged: { room.lastReadIdOnWindowFocus(); }
} }
} }

View file

@ -44,12 +44,13 @@ AbstractButton {
required property int duration required property int duration
required property var timestamp required property var timestamp
required property int status required property int status
required property int index
required property int relatedEventCacheBuster required property int relatedEventCacheBuster
hoverEnabled: true hoverEnabled: true
width: parent.width 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 { Rectangle {
color: (Settings.messageHoverHighlight && hovered) ? Nheko.colors.alternateBase : "transparent" color: (Settings.messageHoverHighlight && hovered) ? Nheko.colors.alternateBase : "transparent"
@ -277,6 +278,7 @@ AbstractButton {
} }
} }
} }
Reactions { Reactions {
anchors { anchors {
top: row.bottom top: row.bottom
@ -292,4 +294,17 @@ AbstractButton {
reactions: r.reactions reactions: r.reactions
eventId: r.eventId 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
}
} }

View file

@ -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 void
Cache::calculateRoomReadStatus() Cache::calculateRoomReadStatus()
{ {
@ -1561,14 +1576,7 @@ Cache::calculateRoomReadStatus(const std::string &room_id)
const auto last_event_id = getLastEventId(txn, room_id); const auto last_event_id = getLastEventId(txn, room_id);
const auto localUser = utils::localUser().toStdString(); const auto localUser = utils::localUser().toStdString();
std::string fullyReadEventId; std::string fullyReadEventId = getFullyReadEventId(room_id);
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;
}
}
if (last_event_id.empty() || fullyReadEventId.empty()) if (last_event_id.empty() || fullyReadEventId.empty())
return true; 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> std::optional<uint64_t>
Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) 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); 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 RoomInfo
singleRoomInfo(const std::string &room_id) 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. //! Calculates which the read status of a room.
//! Whether all the events in the timeline have been read. //! Whether all the events in the timeline have been read.
std::string
getFullyReadEventId(const std::string &room_id)
{
return instance_->getFullyReadEventId(room_id);
}
bool bool
calculateRoomReadStatus(const std::string &room_id) calculateRoomReadStatus(const std::string &room_id)
{ {

View file

@ -152,6 +152,8 @@ std::optional<uint64_t>
getEventIndex(const std::string &room_id, std::string_view event_id); getEventIndex(const std::string &room_id, std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>> std::optional<std::pair<uint64_t, std::string>>
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); 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 RoomInfo
singleRoomInfo(const std::string &room_id); 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. //! Calculates which the read status of a room.
//! Whether all the events in the timeline have been read. //! Whether all the events in the timeline have been read.
std::string
getFullyReadEventId(const std::string &room_id);
bool bool
calculateRoomReadStatus(const std::string &room_id); calculateRoomReadStatus(const std::string &room_id);
void void

View file

@ -169,6 +169,7 @@ public:
//! Calculates which the read status of a room. //! Calculates which the read status of a room.
//! Whether all the events in the timeline have been read. //! 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); bool calculateRoomReadStatus(const std::string &room_id);
void calculateRoomReadStatus(); void calculateRoomReadStatus();
@ -212,6 +213,8 @@ public:
std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id); std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>> std::optional<std::pair<uint64_t, std::string>>
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); 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<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); std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::string_view event_id);

View file

@ -74,6 +74,9 @@ public:
void startChat(QString userid, std::optional<bool> encryptionEnabled); void startChat(QString userid, std::optional<bool> encryptionEnabled);
//! Check if the given room is currently open.
bool isRoomActive(const QString &room_id);
public slots: public slots:
bool handleMatrixUri(QString uri); bool handleMatrixUri(QString uri);
bool handleMatrixUri(const QUrl &uri); bool handleMatrixUri(const QUrl &uri);
@ -193,9 +196,6 @@ private:
void getProfileInfo(); void getProfileInfo();
void getBackupVersion(); void getBackupVersion();
//! Check if the given room is currently open.
bool isRoomActive(const QString &room_id);
using UserID = QString; using UserID = QString;
using Membership = mtx::events::StateEvent<mtx::events::state::Member>; using Membership = mtx::events::StateEvent<mtx::events::state::Member>;
using Memberships = std::map<std::string, Membership>; using Memberships = std::map<std::string, Membership>;

View file

@ -283,6 +283,14 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id));
newRoom->setDecryptDescription(ChatPage::instance()->userSettings()->decryptSidebar()); 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(), connect(newRoom.data(),
&TimelineModel::newEncryptedImage, &TimelineModel::newEncryptedImage,
MainWindow::instance()->imageProvider(), MainWindow::instance()->imageProvider(),
@ -383,7 +391,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
currentRoomPreview_->roomid() == room_id) { currentRoomPreview_->roomid() == room_id) {
currentRoom_ = models.value(room_id); currentRoom_ = models.value(room_id);
currentRoomPreview_.reset(); currentRoomPreview_.reset();
emit currentRoomChanged(); emit currentRoomChanged(room_id);
} }
for (auto p : previewsToAdd) { for (auto p : previewsToAdd) {
@ -644,7 +652,7 @@ RoomlistModel::clear()
invites.clear(); invites.clear();
roomids.clear(); roomids.clear();
currentRoom_ = nullptr; currentRoom_ = nullptr;
emit currentRoomChanged(); emit currentRoomChanged("");
endResetModel(); endResetModel();
} }
@ -743,14 +751,14 @@ RoomlistModel::setCurrentRoom(QString roomid)
if (roomid.isEmpty()) { if (roomid.isEmpty()) {
currentRoom_ = nullptr; currentRoom_ = nullptr;
currentRoomPreview_ = {}; currentRoomPreview_ = {};
emit currentRoomChanged(); emit currentRoomChanged("");
} }
nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
if (models.contains(roomid)) { if (models.contains(roomid)) {
currentRoom_ = models.value(roomid); currentRoom_ = models.value(roomid);
currentRoomPreview_.reset(); currentRoomPreview_.reset();
emit currentRoomChanged(); emit currentRoomChanged(currentRoom_->roomId());
nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
} else if (invites.contains(roomid) || previewedRooms.contains(roomid)) { } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) {
currentRoom_ = nullptr; currentRoom_ = nullptr;
@ -781,7 +789,7 @@ RoomlistModel::setCurrentRoom(QString roomid)
currentRoomPreview_->roomid_.toStdString()); currentRoomPreview_->roomid_.toStdString());
} }
emit currentRoomChanged(); emit currentRoomChanged("");
} }
} }

View file

@ -116,7 +116,7 @@ public slots:
{ {
currentRoom_ = nullptr; currentRoom_ = nullptr;
currentRoomPreview_.reset(); currentRoomPreview_.reset();
emit currentRoomChanged(); emit currentRoomChanged("");
} }
private slots: private slots:
@ -124,7 +124,7 @@ private slots:
signals: signals:
void totalUnreadMessageCountUpdated(int unreadMessages); void totalUnreadMessageCountUpdated(int unreadMessages);
void currentRoomChanged(); void currentRoomChanged(QString currentRoomId);
void fetchedPreview(QString roomid, RoomInfo info); void fetchedPreview(QString roomid, RoomInfo info);
private: private:
@ -218,7 +218,7 @@ public slots:
void updateHiddenTagsAndSpaces(); void updateHiddenTagsAndSpaces();
signals: signals:
void currentRoomChanged(); void currentRoomChanged(QString currentRoomId);
private: private:
short int calculateImportance(const QModelIndex &idx) const; short int calculateImportance(const QModelIndex &idx) const;

View file

@ -427,6 +427,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
setPaginationInProgress(false); setPaginationInProgress(false);
updateLastMessage(); updateLastMessage();
}); });
connect(&events, &EventStore::fetchedMore, this, &TimelineModel::checkAfterFetch);
connect(&events, connect(&events,
&EventStore::startDMVerification, &EventStore::startDMVerification,
this, this,
@ -977,6 +978,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
emit encryptionChanged(); emit encryptionChanged();
} }
} }
updateLastMessage(); 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> template<typename T>
void void
TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType) TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType)
@ -1550,6 +1594,9 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
event); event);
std::visit(SendMessageVisitor{this}, event); std::visit(SendMessageVisitor{this}, event);
fullyReadEventId_ = this->EventId;
emit fullyReadEventIdChanged();
} }
void void

View file

@ -189,6 +189,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QStringList widgetLinks READ widgetLinks NOTIFY widgetLinksChanged) Q_PROPERTY(QStringList widgetLinks READ widgetLinks NOTIFY widgetLinksChanged)
Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) 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(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged) Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged) Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged)
@ -325,6 +326,7 @@ public:
bool isSpace() const { return isSpace_; } bool isSpace() const { return isSpace_; }
bool isEncrypted() const { return isEncrypted_; } bool isEncrypted() const { return isEncrypted_; }
QString fullyReadEventId() const { return QString::fromStdString(fullyReadEventId_); }
crypto::Trust trustlevel() const; crypto::Trust trustlevel() const;
int roomMemberCount() const; int roomMemberCount() const;
bool isDirect() const { return roomMemberCount() <= 2; } bool isDirect() const { return roomMemberCount() <= 2; }
@ -344,6 +346,9 @@ public slots:
int currentIndex() const { return idToIndex(currentId); } int currentIndex() const { return idToIndex(currentId); }
void eventShown(); void eventShown();
void markEventsAsRead(const std::vector<QString> &event_ids); 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; QVariantMap getDump(const QString &eventId, const QString &relatedTo) const;
void updateTypingUsers(const std::vector<QString> &users) void updateTypingUsers(const std::vector<QString> &users)
{ {
@ -427,6 +432,7 @@ signals:
void updateFlowEventId(std::string event_id); void updateFlowEventId(std::string event_id);
void encryptionChanged(); void encryptionChanged();
void fullyReadEventIdChanged();
void trustlevelChanged(); void trustlevelChanged();
void roomNameChanged(); void roomNameChanged();
void roomTopicChanged(); void roomTopicChanged();
@ -480,6 +486,8 @@ private:
bool m_paginationInProgress = false; bool m_paginationInProgress = false;
bool isSpace_ = false; bool isSpace_ = false;
bool isEncrypted_ = false; bool isEncrypted_ = false;
std::string last_event_id;
std::string fullyReadEventId_;
}; };
template<class T> template<class T>
@ -497,6 +505,7 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy
msgCopy.type = eventType; msgCopy.type = eventType;
emit newMessageToSend(msgCopy); emit newMessageToSend(msgCopy);
} }
resetReply(); resetReply();
resetEdit(); resetEdit();
} }