diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
index 88ab1963..696db8de 100644
--- a/src/timeline/TimelineItem.cpp
+++ b/src/timeline/TimelineItem.cpp
@@ -42,6 +42,7 @@ StatusIndicator::StatusIndicator(QWidget *parent)
lockIcon_.addFile(":/icons/icons/ui/lock.png");
clockIcon_.addFile(":/icons/icons/ui/clock.png");
checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");
+ doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");
}
void
@@ -79,6 +80,10 @@ StatusIndicator::paintEvent(QPaintEvent *)
paintIcon(p, checkmarkIcon_);
break;
}
+ case StatusIndicatorState::Read: {
+ paintIcon(p, doubleCheckmarkIcon_);
+ break;
+ }
case StatusIndicatorState::Empty:
break;
}
@@ -302,6 +307,8 @@ TimelineItem::TimelineItem(ImageItem *image,
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
image, event, with_sender);
+ markOwnMessagesAsReceived(event.sender);
+
addSaveImageAction(image);
}
@@ -315,6 +322,8 @@ TimelineItem::TimelineItem(StickerItem *image,
{
setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);
+ markOwnMessagesAsReceived(event.sender);
+
addSaveImageAction(image);
}
@@ -328,6 +337,8 @@ TimelineItem::TimelineItem(FileItem *file,
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
file, event, with_sender);
+
+ markOwnMessagesAsReceived(event.sender);
}
TimelineItem::TimelineItem(AudioItem *audio,
@@ -340,6 +351,8 @@ TimelineItem::TimelineItem(AudioItem *audio,
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
audio, event, with_sender);
+
+ markOwnMessagesAsReceived(event.sender);
}
TimelineItem::TimelineItem(VideoItem *video,
@@ -352,6 +365,8 @@ TimelineItem::TimelineItem(VideoItem *video,
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
video, event, with_sender);
+
+ markOwnMessagesAsReceived(event.sender);
}
/*
@@ -367,6 +382,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
init();
addReplyAction();
+ markOwnMessagesAsReceived(event.sender);
+
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
@@ -413,6 +430,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote>
init();
addReplyAction();
+ markOwnMessagesAsReceived(event.sender);
+
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
@@ -455,6 +474,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
init();
addReplyAction();
+ markOwnMessagesAsReceived(event.sender);
+
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
@@ -496,6 +517,21 @@ TimelineItem::markSent()
}
void
+TimelineItem::markOwnMessagesAsReceived(const std::string &sender)
+{
+ QSettings settings;
+ if (sender == settings.value("auth/user_id").toString().toStdString())
+ statusIndicator_->setState(StatusIndicatorState::Received);
+}
+
+void
+TimelineItem::markRead()
+{
+ if (statusIndicator_->state() != StatusIndicatorState::Encrypted)
+ statusIndicator_->setState(StatusIndicatorState::Read);
+}
+
+void
TimelineItem::markReceived(bool isEncrypted)
{
isReceived_ = true;
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
index d3cab0a0..874c00df 100644
--- a/src/timeline/TimelineItem.h
+++ b/src/timeline/TimelineItem.h
@@ -50,6 +50,8 @@ enum class StatusIndicatorState
Encrypted,
//! The plaintext message was received by the server.
Received,
+ //! At least one of the participants has read the message.
+ Read,
//! The client sent the message. Not yet received.
Sent,
//! When the message is loaded from cache or backfill.
@@ -66,6 +68,7 @@ class StatusIndicator : public QWidget
public:
explicit StatusIndicator(QWidget *parent);
void setState(StatusIndicatorState state);
+ StatusIndicatorState state() const { return state_; }
protected:
void paintEvent(QPaintEvent *event) override;
@@ -76,6 +79,7 @@ private:
QIcon lockIcon_;
QIcon clockIcon_;
QIcon checkmarkIcon_;
+ QIcon doubleCheckmarkIcon_;
QColor iconColor_ = QColor("#999");
@@ -234,6 +238,7 @@ public:
QString eventId() const { return event_id_; }
void setEventId(const QString &event_id) { event_id_ = event_id; }
void markReceived(bool isEncrypted);
+ void markRead();
void markSent();
bool isReceived() { return isReceived_; };
void setRoomId(QString room_id) { room_id_ = room_id; }
@@ -252,6 +257,8 @@ protected:
void contextMenuEvent(QContextMenuEvent *event) override;
private:
+ //! If we are the sender of the message the event wil be marked as received by the server.
+ void markOwnMessagesAsReceived(const std::string &sender);
void init();
//! Add a context menu option to save the image of the timeline item.
void addSaveImageAction(ImageItem *image);
diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp
index a8c04807..074ba498 100644
--- a/src/timeline/TimelineView.cpp
+++ b/src/timeline/TimelineView.cpp
@@ -18,6 +18,7 @@
#include <QApplication>
#include <QFileInfo>
#include <QTimer>
+#include <QtConcurrent>
#include "Cache.h"
#include "ChatPage.h"
@@ -353,6 +354,27 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events:
}
void
+TimelineView::displayReadReceipts(std::vector<TimelineEvent> events)
+{
+ QtConcurrent::run(
+ [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() {
+ std::vector<QString> event_ids;
+
+ for (const auto &e : events) {
+ if (utils::event_sender(e) == local_user)
+ event_ids.emplace_back(
+ QString::fromStdString(utils::event_id(e)));
+ }
+
+ auto readEvents =
+ cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString());
+
+ if (!readEvents.empty())
+ emit markReadEvents(readEvents);
+ });
+}
+
+void
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
{
int counter = 0;
@@ -373,6 +395,8 @@ TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
lastMessageDirection_ = TimelineDirection::Bottom;
+ displayReadReceipts(events);
+
QApplication::processEvents();
}
@@ -407,6 +431,8 @@ TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events)
QApplication::processEvents();
+ displayReadReceipts(events);
+
// If this batch is the first being rendered (i.e the first and the last
// events originate from this batch), set the last sender.
if (lastSender_.isEmpty() && !items.empty()) {
@@ -499,6 +525,23 @@ TimelineView::init()
connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage);
+ connect(
+ this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) {
+ for (const auto &event : event_ids) {
+ if (eventIds_.contains(event)) {
+ auto widget = eventIds_[event];
+ if (!widget)
+ return;
+
+ auto item = qobject_cast<TimelineItem *>(widget);
+ if (!item)
+ return;
+
+ item->markRead();
+ }
+ }
+ });
+
connect(scroll_area_->verticalScrollBar(),
SIGNAL(valueChanged(int)),
this,
@@ -615,6 +658,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve
// we've already marked the widget as received.
if (!msg.widget->isReceived()) {
msg.widget->markReceived(msg.is_encrypted);
+ cache::client()->addPendingReceipt(room_id_, event_id);
pending_sent_msgs_.append(msg);
}
} else {
@@ -826,9 +870,14 @@ TimelineView::removePendingMessage(const std::string &txn_id)
}
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
if (it->txn_id == txn_id) {
- if (it->widget)
+ if (it->widget) {
it->widget->markReceived(it->is_encrypted);
+ // TODO: update when a solution for encrypted messages is available.
+ if (!it->is_encrypted)
+ cache::client()->addPendingReceipt(room_id_, it->event_id);
+ }
+
nhlog::ui()->info("[{}] received sync before message response", txn_id);
return;
}
diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h
index 7b269063..5c42415a 100644
--- a/src/timeline/TimelineView.h
+++ b/src/timeline/TimelineView.h
@@ -156,6 +156,7 @@ signals:
void messagesRetrieved(const mtx::responses::Messages &res);
void messageFailed(const std::string &txn_id);
void messageSent(const std::string &txn_id, const QString &event_id);
+ void markReadEvents(const std::vector<QString> &event_ids);
protected:
void paintEvent(QPaintEvent *event) override;
@@ -165,6 +166,9 @@ protected:
private:
using TimelineEvent = mtx::events::collections::TimelineEvents;
+ //! Mark our own widgets as read if they have more than one receipt.
+ void displayReadReceipts(std::vector<TimelineEvent> events);
+
QWidget *relativeWidget(QWidget *item, int dt) const;
DecryptionResult parseEncryptedEvent(
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 1decab35..1bbb4def 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -37,6 +37,17 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
}
void
+TimelineViewManager::updateReadReceipts(const QString &room_id,
+ const std::vector<QString> &event_ids)
+{
+ if (timelineViewExists(room_id)) {
+ auto view = views_[room_id];
+ if (view)
+ emit view->markReadEvents(event_ids);
+ }
+}
+
+void
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
{
auto view = views_[room_id];
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index f3c099c1..d23345d3 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -57,6 +57,7 @@ signals:
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
public slots:
+ void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
|