mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 12:38:48 +03:00
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:
parent
02adcfdc38
commit
8071b192b8
11 changed files with 172 additions and 20 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -108,6 +108,8 @@ Page {
|
|||
timelineRoot: timelineView
|
||||
windowTarget: roomWindowW
|
||||
}
|
||||
|
||||
onActiveChanged: { room.lastReadIdOnWindowFocus(); }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue