diff --git a/src/Cache.cpp b/src/Cache.cpp
index 4965167b..ba32cae4 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -825,8 +825,10 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
}
void
-Cache::notifyForReadReceipts(lmdb::txn &txn, const std::string &room_id)
+Cache::notifyForReadReceipts(const std::string &room_id)
{
+ auto txn = lmdb::txn::begin(env_);
+
QSettings settings;
auto local_user = settings.value("auth/user_id").toString();
@@ -839,6 +841,47 @@ Cache::notifyForReadReceipts(lmdb::txn &txn, const std::string &room_id)
if (!matches.empty())
emit newReadReceipts(QString::fromStdString(room_id), matches);
+
+ txn.commit();
+}
+
+void
+Cache::calculateRoomReadStatus()
+{
+ const auto joined_rooms = joinedRooms();
+
+ std::map<QString, bool> readStatus;
+
+ for (const auto &room : joined_rooms)
+ readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
+
+ emit roomReadStatus(readStatus);
+}
+
+bool
+Cache::calculateRoomReadStatus(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_);
+
+ // Get last event id on the room.
+ const auto last_event_id = getLastMessageInfo(txn, room_id).event_id;
+ const auto localUser = utils::localUser().toStdString();
+
+ txn.commit();
+
+ // Retrieve all read receipts for that event.
+ const auto receipts = readReceipts(last_event_id, QString::fromStdString(room_id));
+
+ if (receipts.size() == 0)
+ return true;
+
+ // Check if the local user has a read receipt for it.
+ for (auto it = receipts.cbegin(); it != receipts.cend(); it++) {
+ if (it->second == localUser)
+ return false;
+ }
+
+ return true;
}
void
@@ -880,11 +923,15 @@ Cache::saveState(const mtx::responses::Sync &res)
txn.commit();
+ std::map<QString, bool> readStatus;
+
for (const auto &room : res.rooms.join) {
- auto tmpTxn = lmdb::txn::begin(env_);
- notifyForReadReceipts(tmpTxn, room.first);
- tmpTxn.commit();
+ notifyForReadReceipts(room.first);
+ readStatus.emplace(QString::fromStdString(room.first),
+ calculateRoomReadStatus(room.first));
}
+
+ emit roomReadStatus(readStatus);
}
void
diff --git a/src/Cache.h b/src/Cache.h
index ce531056..f9a9f9c0 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -89,6 +89,7 @@ from_json(const json &j, ReadReceiptKey &key)
struct DescInfo
{
+ QString event_id;
QString username;
QString userid;
QString body;
@@ -356,7 +357,7 @@ public:
void removePendingReceipt(lmdb::txn &txn,
const std::string &room_id,
const std::string &event_id);
- void notifyForReadReceipts(lmdb::txn &txn, const std::string &room_id);
+ void notifyForReadReceipts(const std::string &room_id);
std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray image(const QString &url) const;
@@ -376,6 +377,11 @@ public:
return getRoomInfo(roomsWithStateUpdates(sync));
}
+ //! Calculates which the read status of a room.
+ //! Whether all the events in the timeline have been read.
+ bool calculateRoomReadStatus(const std::string &room_id);
+ void calculateRoomReadStatus();
+
QVector<SearchResult> searchUsers(const std::string &room_id,
const std::string &query,
std::uint8_t max_items = 5);
@@ -444,6 +450,7 @@ public:
signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
+ void roomReadStatus(const std::map<QString, bool> &status);
private:
//! Save an invited room.
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 49e37b92..6640da09 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -677,6 +677,9 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
view_manager_,
&TimelineViewManager::updateReadReceipts);
+ connect(
+ cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus);
+
const bool isInitialized = cache::client()->isInitialized();
const bool isValid = cache::client()->isFormatValid();
@@ -794,6 +797,8 @@ ChatPage::loadStateFromCache()
emit initializeEmptyViews(cache::client()->roomMessages());
emit initializeRoomList(cache::client()->roomInfo());
+ cache::client()->calculateRoomReadStatus();
+
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
emit dropToLoginPageCb(
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index 4edd7b89..a0d35b0c 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -31,9 +31,11 @@
constexpr int MaxUnreadCountDisplayed = 99;
-constexpr int Padding = 9;
-constexpr int IconSize = 44;
-constexpr int MaxHeight = IconSize + 2 * Padding;
+constexpr int Padding = 9;
+constexpr int UnreadLineWidth = 4;
+constexpr int UnreadLineOffset = 4;
+constexpr int IconSize = 44;
+constexpr int MaxHeight = IconSize + 2 * Padding;
constexpr int InviteBtnX = IconSize + 2 * Padding;
constexpr int InviteBtnY = IconSize / 2 + Padding + Padding / 3;
@@ -89,6 +91,8 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
{
init(parent);
+ QString emptyEventId;
+
// HACK
// We use fake message info with an old date to pin
// the invite events to the top.
@@ -96,7 +100,8 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
// State events in invited rooms don't contain timestamp info,
// so we can't use them for sorting.
if (roomType_ == RoomType::Invited)
- lastMsgInfo_ = {"-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
+ lastMsgInfo_ = {
+ emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
}
void
@@ -305,6 +310,15 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
p.setBrush(Qt::NoBrush);
p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt);
}
+
+ if (!isPressed_ && hasUnreadMessages_) {
+ QPen pen;
+ pen.setWidth(UnreadLineWidth);
+ pen.setColor(highlightedBackgroundColor_);
+
+ p.setPen(pen);
+ p.drawLine(0, UnreadLineOffset, 0, height() - UnreadLineOffset);
+ }
}
void
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index 95db1d75..f08e715a 100644
--- a/src/RoomInfoListItem.h
+++ b/src/RoomInfoListItem.h
@@ -121,6 +121,13 @@ public:
}
bool isInvite() { return roomType_ == RoomType::Invited; }
+ void setReadState(bool hasUnreadMessages)
+ {
+ if (hasUnreadMessages_ != hasUnreadMessages) {
+ hasUnreadMessages_ = hasUnreadMessages;
+ update();
+ }
+ }
signals:
void clicked(const QString &room_id);
@@ -164,7 +171,8 @@ private:
Menu *menu_;
QAction *leaveRoom_;
- bool isPressed_ = false;
+ bool isPressed_ = false;
+ bool hasUnreadMessages_ = true;
int unreadMsgCount_ = 0;
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 8c13a7a7..a262dc21 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -455,3 +455,16 @@ RoomList::firstRoom() const
return std::pair<QString, QSharedPointer<RoomInfoListItem>>(firstRoom->first,
firstRoom->second);
}
+
+void
+RoomList::updateReadStatus(const std::map<QString, bool> &status)
+{
+ for (const auto &room : status) {
+ if (roomExists(room.first)) {
+ auto item = rooms_.at(room.first);
+
+ if (item)
+ item->setReadState(room.second);
+ }
+ }
+}
diff --git a/src/RoomList.h b/src/RoomList.h
index 93707671..dc90d614 100644
--- a/src/RoomList.h
+++ b/src/RoomList.h
@@ -72,6 +72,7 @@ public slots:
void updateUnreadMessageCount(const QString &roomid, int count);
void updateRoomDescription(const QString &roomid, const DescInfo &info);
void closeJoinRoomDialog(bool isJoining, QString roomAlias);
+ void updateReadStatus(const std::map<QString, bool> &status);
protected:
void paintEvent(QPaintEvent *event) override;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 2bad72bf..82959b46 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -126,6 +126,7 @@ utils::getMessageDescription(const TimelineEvent &event,
info.userid = sender;
info.body = QString(" %1").arg(messageDescription<Encrypted>());
info.timestamp = utils::descriptiveTime(ts);
+ info.event_id = QString::fromStdString(msg.event_id);
info.datetime = ts;
return info;
diff --git a/src/Utils.h b/src/Utils.h
index dc688845..1d976000 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -109,6 +109,7 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin
bool isEmote = std::is_same<T, Emote>::value;
return DescInfo{
+ QString::fromStdString(msg.event_id),
isEmote ? "" : (sender == localUser ? "You" : username),
sender,
(isText || isEmote)
diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
index c2bd98d9..18598487 100644
--- a/src/timeline/TimelineItem.cpp
+++ b/src/timeline/TimelineItem.cpp
@@ -317,16 +317,23 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty,
if (formatted_body == body.trimmed().toHtmlEscaped())
formatted_body = body.toHtmlEscaped();
+ QString emptyEventId;
+
if (ty == mtx::events::MessageType::Emote) {
formatted_body = QString("<em>%1</em>").arg(formatted_body);
- descriptionMsg_ = {"",
+ descriptionMsg_ = {emptyEventId,
+ "",
userid,
QString("* %1 %2").arg(displayName).arg(body),
utils::descriptiveTime(timestamp),
timestamp};
} else {
- descriptionMsg_ = {
- "You: ", userid, body, utils::descriptiveTime(timestamp), timestamp};
+ descriptionMsg_ = {emptyEventId,
+ "You: ",
+ userid,
+ body,
+ utils::descriptiveTime(timestamp),
+ timestamp};
}
formatted_body = utils::linkifyMessage(formatted_body);
@@ -496,7 +503,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
- descriptionMsg_ = {Cache::displayName(room_id_, sender),
+ descriptionMsg_ = {event_id_,
+ Cache::displayName(room_id_, sender),
sender,
" sent a notification",
utils::descriptiveTime(timestamp),
@@ -545,7 +553,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote>
auto displayName = Cache::displayName(room_id_, sender);
formatted_body = QString("<em>%1</em>").arg(formatted_body);
- descriptionMsg_ = {"",
+ descriptionMsg_ = {event_id_,
+ "",
sender,
QString("* %1 %2").arg(displayName).arg(body),
utils::descriptiveTime(timestamp),
@@ -592,7 +601,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
auto displayName = Cache::displayName(room_id_, sender);
QSettings settings;
- descriptionMsg_ = {sender == settings.value("auth/user_id") ? "You" : displayName,
+ descriptionMsg_ = {event_id_,
+ sender == settings.value("auth/user_id") ? "You" : displayName,
sender,
QString(": %1").arg(body),
utils::descriptiveTime(timestamp),
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
index 32d586f5..b98dd148 100644
--- a/src/timeline/TimelineItem.h
+++ b/src/timeline/TimelineItem.h
@@ -319,7 +319,8 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool
auto displayName = Cache::displayName(room_id_, userid);
auto timestamp = QDateTime::currentDateTime();
- descriptionMsg_ = {"You",
+ descriptionMsg_ = {"", // No event_id up until this point.
+ "You",
userid,
QString(" %1").arg(utils::messageDescription<Widget>()),
utils::descriptiveTime(timestamp),
@@ -358,7 +359,8 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen
auto displayName = Cache::displayName(room_id_, sender);
QSettings settings;
- descriptionMsg_ = {sender == settings.value("auth/user_id") ? "You" : displayName,
+ descriptionMsg_ = {event_id_,
+ sender == settings.value("auth/user_id") ? "You" : displayName,
sender,
QString(" %1").arg(utils::messageDescription<Widget>()),
utils::descriptiveTime(timestamp),
|