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
timestamp: wrapper.timestamp
status: wrapper.status
index: wrapper.index
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
y: section.visible && section.active ? section.y + section.height : 0

View file

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

View file

@ -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
}
}

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
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)
{

View file

@ -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

View file

@ -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);

View file

@ -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>;

View file

@ -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("");
}
}

View file

@ -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;

View file

@ -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

View file

@ -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();
}