Mark own read messages with a double checkmark (#377)

This commit is contained in:
Konstantinos Sideris 2018-07-17 23:50:18 +03:00
parent 40facd116e
commit e4dedbcaba
12 changed files with 248 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

View File

@ -1,5 +1,7 @@
<RCC> <RCC>
<qresource prefix="/icons"> <qresource prefix="/icons">
<file>icons/ui/double-tick-indicator.png</file>
<file>icons/ui/double-tick-indicator@2x.png</file>
<file>icons/ui/lock.png</file> <file>icons/ui/lock.png</file>
<file>icons/ui/lock@2x.png</file> <file>icons/ui/lock@2x.png</file>
<file>icons/ui/clock.png</file> <file>icons/ui/clock.png</file>

View File

@ -649,6 +649,70 @@ Cache::setCurrentFormat()
txn.commit(); txn.commit();
} }
std::vector<QString>
Cache::pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id)
{
auto db = getPendingReceiptsDb(txn);
std::string key, unused;
std::vector<QString> pending;
auto cursor = lmdb::cursor::open(txn, db);
while (cursor.get(key, unused, MDB_NEXT)) {
ReadReceiptKey receipt;
try {
receipt = json::parse(key);
} catch (const nlohmann::json::exception &e) {
nhlog::db()->warn("pendingReceiptsEvents: {}", e.what());
continue;
}
if (receipt.room_id == room_id)
pending.emplace_back(QString::fromStdString(receipt.event_id));
}
cursor.close();
return pending;
}
void
Cache::removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id)
{
auto db = getPendingReceiptsDb(txn);
ReadReceiptKey receipt_key{event_id, room_id};
auto key = json(receipt_key).dump();
try {
lmdb::dbi_del(txn, db, lmdb::val(key.data(), key.size()), nullptr);
} catch (const lmdb::error &e) {
nhlog::db()->critical("removePendingReceipt: {}", e.what());
}
}
void
Cache::addPendingReceipt(const QString &room_id, const QString &event_id)
{
auto txn = lmdb::txn::begin(env_);
auto db = getPendingReceiptsDb(txn);
ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
auto key = json(receipt_key).dump();
std::string empty;
try {
lmdb::dbi_put(txn,
db,
lmdb::val(key.data(), key.size()),
lmdb::val(empty.data(), empty.size()));
} catch (const lmdb::error &e) {
nhlog::db()->critical("addPendingReceipt: {}", e.what());
}
txn.commit();
}
CachedReceipts CachedReceipts
Cache::readReceipts(const QString &event_id, const QString &room_id) Cache::readReceipts(const QString &event_id, const QString &room_id)
{ {
@ -684,6 +748,30 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
return receipts; return receipts;
} }
std::vector<QString>
Cache::filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user)
{
std::vector<QString> read_events;
for (const auto &event : event_ids) {
auto receipts = readReceipts(event, room_id);
if (receipts.size() == 0)
continue;
if (receipts.size() == 1) {
if (receipts.begin()->second == excluded_user)
continue;
}
read_events.emplace_back(event);
}
return read_events;
}
void void
Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
{ {
@ -733,6 +821,23 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
} }
} }
void
Cache::notifyForReadReceipts(lmdb::txn &txn, const std::string &room_id)
{
QSettings settings;
auto local_user = settings.value("auth/user_id").toString();
auto matches = filterReadEvents(QString::fromStdString(room_id),
pendingReceiptsEvents(txn, room_id),
local_user.toStdString());
for (const auto &m : matches)
removePendingReceipt(txn, room_id, m.toStdString());
if (!matches.empty())
emit newReadReceipts(QString::fromStdString(room_id), matches);
}
void void
Cache::saveState(const mtx::responses::Sync &res) Cache::saveState(const mtx::responses::Sync &res)
{ {
@ -771,6 +876,12 @@ Cache::saveState(const mtx::responses::Sync &res)
removeLeftRooms(txn, res.rooms.leave); removeLeftRooms(txn, res.rooms.leave);
txn.commit(); txn.commit();
for (const auto &room : res.rooms.join) {
auto txn = lmdb::txn::begin(env_);
notifyForReadReceipts(txn, room.first);
txn.commit();
}
} }
void void

View File

@ -347,6 +347,18 @@ public:
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
UserReceipts readReceipts(const QString &event_id, const QString &room_id); UserReceipts readReceipts(const QString &event_id, const QString &room_id);
//! Filter the events that have at least one read receipt.
std::vector<QString> filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user);
//! Add event for which we are expecting some read receipts.
void addPendingReceipt(const QString &room_id, const QString &event_id);
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);
std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray image(const QString &url) const; QByteArray image(const QString &url) const;
QByteArray image(lmdb::txn &txn, const std::string &url) const; QByteArray image(lmdb::txn &txn, const std::string &url) const;
QByteArray image(const std::string &url) const QByteArray image(const std::string &url) const
@ -421,6 +433,9 @@ public:
OlmSessionStorage session_storage; OlmSessionStorage session_storage;
signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
private: private:
//! Save an invited room. //! Save an invited room.
void saveInvite(lmdb::txn &txn, void saveInvite(lmdb::txn &txn,
@ -582,6 +597,11 @@ private:
} }
} }
lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
}
lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id)
{ {
auto db = auto db =

View File

@ -685,6 +685,11 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
try { try {
cache::init(userid); cache::init(userid);
connect(cache::client(),
&Cache::newReadReceipts,
view_manager_,
&TimelineViewManager::updateReadReceipts);
const bool isInitialized = cache::client()->isInitialized(); const bool isInitialized = cache::client()->isInitialized();
const bool isValid = cache::client()->isFormatValid(); const bool isValid = cache::client()->isFormatValid();
@ -700,6 +705,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
loadStateFromCache(); loadStateFromCache();
return; return;
} }
} catch (const lmdb::error &e) { } catch (const lmdb::error &e) {
nhlog::db()->critical("failure during boot: {}", e.what()); nhlog::db()->critical("failure during boot: {}", e.what());
cache::client()->deleteData(); cache::client()->deleteData();

View File

@ -42,6 +42,7 @@ StatusIndicator::StatusIndicator(QWidget *parent)
lockIcon_.addFile(":/icons/icons/ui/lock.png"); lockIcon_.addFile(":/icons/icons/ui/lock.png");
clockIcon_.addFile(":/icons/icons/ui/clock.png"); clockIcon_.addFile(":/icons/icons/ui/clock.png");
checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");
doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");
} }
void void
@ -79,6 +80,10 @@ StatusIndicator::paintEvent(QPaintEvent *)
paintIcon(p, checkmarkIcon_); paintIcon(p, checkmarkIcon_);
break; break;
} }
case StatusIndicatorState::Read: {
paintIcon(p, doubleCheckmarkIcon_);
break;
}
case StatusIndicatorState::Empty: case StatusIndicatorState::Empty:
break; break;
} }
@ -302,6 +307,8 @@ TimelineItem::TimelineItem(ImageItem *image,
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>( setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
image, event, with_sender); image, event, with_sender);
markOwnMessagesAsReceived(event.sender);
addSaveImageAction(image); addSaveImageAction(image);
} }
@ -315,6 +322,8 @@ TimelineItem::TimelineItem(StickerItem *image,
{ {
setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender); setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);
markOwnMessagesAsReceived(event.sender);
addSaveImageAction(image); addSaveImageAction(image);
} }
@ -328,6 +337,8 @@ TimelineItem::TimelineItem(FileItem *file,
{ {
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>( setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
file, event, with_sender); file, event, with_sender);
markOwnMessagesAsReceived(event.sender);
} }
TimelineItem::TimelineItem(AudioItem *audio, TimelineItem::TimelineItem(AudioItem *audio,
@ -340,6 +351,8 @@ TimelineItem::TimelineItem(AudioItem *audio,
{ {
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>( setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
audio, event, with_sender); audio, event, with_sender);
markOwnMessagesAsReceived(event.sender);
} }
TimelineItem::TimelineItem(VideoItem *video, TimelineItem::TimelineItem(VideoItem *video,
@ -352,6 +365,8 @@ TimelineItem::TimelineItem(VideoItem *video,
{ {
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>( setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
video, event, with_sender); video, event, with_sender);
markOwnMessagesAsReceived(event.sender);
} }
/* /*
@ -367,6 +382,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
init(); init();
addReplyAction(); addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id); event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender); const auto sender = QString::fromStdString(event.sender);
const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
@ -413,6 +430,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote>
init(); init();
addReplyAction(); addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id); event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender); const auto sender = QString::fromStdString(event.sender);
@ -455,6 +474,8 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
init(); init();
addReplyAction(); addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id); event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender); const auto sender = QString::fromStdString(event.sender);
@ -495,6 +516,21 @@ TimelineItem::markSent()
statusIndicator_->setState(StatusIndicatorState::Sent); statusIndicator_->setState(StatusIndicatorState::Sent);
} }
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 void
TimelineItem::markReceived(bool isEncrypted) TimelineItem::markReceived(bool isEncrypted)
{ {

View File

@ -50,6 +50,8 @@ enum class StatusIndicatorState
Encrypted, Encrypted,
//! The plaintext message was received by the server. //! The plaintext message was received by the server.
Received, Received,
//! At least one of the participants has read the message.
Read,
//! The client sent the message. Not yet received. //! The client sent the message. Not yet received.
Sent, Sent,
//! When the message is loaded from cache or backfill. //! When the message is loaded from cache or backfill.
@ -66,6 +68,7 @@ class StatusIndicator : public QWidget
public: public:
explicit StatusIndicator(QWidget *parent); explicit StatusIndicator(QWidget *parent);
void setState(StatusIndicatorState state); void setState(StatusIndicatorState state);
StatusIndicatorState state() const { return state_; }
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
@ -76,6 +79,7 @@ private:
QIcon lockIcon_; QIcon lockIcon_;
QIcon clockIcon_; QIcon clockIcon_;
QIcon checkmarkIcon_; QIcon checkmarkIcon_;
QIcon doubleCheckmarkIcon_;
QColor iconColor_ = QColor("#999"); QColor iconColor_ = QColor("#999");
@ -234,6 +238,7 @@ public:
QString eventId() const { return event_id_; } QString eventId() const { return event_id_; }
void setEventId(const QString &event_id) { event_id_ = event_id; } void setEventId(const QString &event_id) { event_id_ = event_id; }
void markReceived(bool isEncrypted); void markReceived(bool isEncrypted);
void markRead();
void markSent(); void markSent();
bool isReceived() { return isReceived_; }; bool isReceived() { return isReceived_; };
void setRoomId(QString room_id) { room_id_ = room_id; } void setRoomId(QString room_id) { room_id_ = room_id; }
@ -252,6 +257,8 @@ protected:
void contextMenuEvent(QContextMenuEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override;
private: 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(); void init();
//! Add a context menu option to save the image of the timeline item. //! Add a context menu option to save the image of the timeline item.
void addSaveImageAction(ImageItem *image); void addSaveImageAction(ImageItem *image);

View File

@ -18,6 +18,7 @@
#include <QApplication> #include <QApplication>
#include <QFileInfo> #include <QFileInfo>
#include <QTimer> #include <QTimer>
#include <QtConcurrent>
#include "Cache.h" #include "Cache.h"
#include "ChatPage.h" #include "ChatPage.h"
@ -352,6 +353,27 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events:
return {dummy, false}; return {dummy, false};
} }
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 void
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events) TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
{ {
@ -373,6 +395,8 @@ TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
lastMessageDirection_ = TimelineDirection::Bottom; lastMessageDirection_ = TimelineDirection::Bottom;
displayReadReceipts(events);
QApplication::processEvents(); QApplication::processEvents();
} }
@ -407,6 +431,8 @@ TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events)
QApplication::processEvents(); QApplication::processEvents();
displayReadReceipts(events);
// If this batch is the first being rendered (i.e the first and the last // If this batch is the first being rendered (i.e the first and the last
// events originate from this batch), set the last sender. // events originate from this batch), set the last sender.
if (lastSender_.isEmpty() && !items.empty()) { if (lastSender_.isEmpty() && !items.empty()) {
@ -499,6 +525,23 @@ TimelineView::init()
connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage);
connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); 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(), connect(scroll_area_->verticalScrollBar(),
SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)),
this, this,
@ -615,6 +658,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve
// we've already marked the widget as received. // we've already marked the widget as received.
if (!msg.widget->isReceived()) { if (!msg.widget->isReceived()) {
msg.widget->markReceived(msg.is_encrypted); msg.widget->markReceived(msg.is_encrypted);
cache::client()->addPendingReceipt(room_id_, event_id);
pending_sent_msgs_.append(msg); pending_sent_msgs_.append(msg);
} }
} else { } else {
@ -826,9 +870,14 @@ TimelineView::removePendingMessage(const std::string &txn_id)
} }
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
if (it->txn_id == txn_id) { if (it->txn_id == txn_id) {
if (it->widget) if (it->widget) {
it->widget->markReceived(it->is_encrypted); 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); nhlog::ui()->info("[{}] received sync before message response", txn_id);
return; return;
} }

View File

@ -156,6 +156,7 @@ signals:
void messagesRetrieved(const mtx::responses::Messages &res); void messagesRetrieved(const mtx::responses::Messages &res);
void messageFailed(const std::string &txn_id); void messageFailed(const std::string &txn_id);
void messageSent(const std::string &txn_id, const QString &event_id); void messageSent(const std::string &txn_id, const QString &event_id);
void markReadEvents(const std::vector<QString> &event_ids);
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
@ -165,6 +166,9 @@ protected:
private: private:
using TimelineEvent = mtx::events::collections::TimelineEvents; 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; QWidget *relativeWidget(QWidget *item, int dt) const;
DecryptionResult parseEncryptedEvent( DecryptionResult parseEncryptedEvent(

View File

@ -36,6 +36,17 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
setStyleSheet("border: none;"); setStyleSheet("border: none;");
} }
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 void
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
{ {

View File

@ -57,6 +57,7 @@ signals:
void updateRoomsLastMessage(const QString &user, const DescInfo &info); void updateRoomsLastMessage(const QString &user, const DescInfo &info);
public slots: 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 removeTimelineEvent(const QString &room_id, const QString &event_id);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);