mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-10-30 09:30:47 +03:00
Mark own read messages with a double checkmark (#377)
This commit is contained in:
parent
40facd116e
commit
e4dedbcaba
12 changed files with 248 additions and 1 deletions
BIN
resources/icons/ui/double-tick-indicator.png
Normal file
BIN
resources/icons/ui/double-tick-indicator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 577 B |
BIN
resources/icons/ui/double-tick-indicator@2x.png
Normal file
BIN
resources/icons/ui/double-tick-indicator@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 700 B |
|
@ -1,5 +1,7 @@
|
|||
<RCC>
|
||||
<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@2x.png</file>
|
||||
<file>icons/ui/clock.png</file>
|
||||
|
|
111
src/Cache.cpp
111
src/Cache.cpp
|
@ -649,6 +649,70 @@ Cache::setCurrentFormat()
|
|||
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
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
Cache::saveState(const mtx::responses::Sync &res)
|
||||
{
|
||||
|
@ -771,6 +876,12 @@ Cache::saveState(const mtx::responses::Sync &res)
|
|||
removeLeftRooms(txn, res.rooms.leave);
|
||||
|
||||
txn.commit();
|
||||
|
||||
for (const auto &room : res.rooms.join) {
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
notifyForReadReceipts(txn, room.first);
|
||||
txn.commit();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
|
20
src/Cache.h
20
src/Cache.h
|
@ -347,6 +347,18 @@ public:
|
|||
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
|
||||
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(lmdb::txn &txn, const std::string &url) const;
|
||||
QByteArray image(const std::string &url) const
|
||||
|
@ -421,6 +433,9 @@ public:
|
|||
|
||||
OlmSessionStorage session_storage;
|
||||
|
||||
signals:
|
||||
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
|
||||
|
||||
private:
|
||||
//! Save an invited room.
|
||||
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)
|
||||
{
|
||||
auto db =
|
||||
|
|
|
@ -685,6 +685,11 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
|
|||
try {
|
||||
cache::init(userid);
|
||||
|
||||
connect(cache::client(),
|
||||
&Cache::newReadReceipts,
|
||||
view_manager_,
|
||||
&TimelineViewManager::updateReadReceipts);
|
||||
|
||||
const bool isInitialized = cache::client()->isInitialized();
|
||||
const bool isValid = cache::client()->isFormatValid();
|
||||
|
||||
|
@ -700,6 +705,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
|
|||
loadStateFromCache();
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->critical("failure during boot: {}", e.what());
|
||||
cache::client()->deleteData();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -495,6 +516,21 @@ TimelineItem::markSent()
|
|||
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
|
||||
TimelineItem::markReceived(bool isEncrypted)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <QApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "ChatPage.h"
|
||||
|
@ -352,6 +353,27 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events:
|
|||
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
|
||||
TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -36,6 +36,17 @@ TimelineViewManager::TimelineViewManager(QWidget *parent)
|
|||
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
|
||||
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue