diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc
index 334f72c3..b8ea9e20 100644
--- a/src/AvatarProvider.cc
+++ b/src/AvatarProvider.cc
@@ -18,12 +18,10 @@
#include "AvatarProvider.h"
#include "MatrixClient.h"
-#include "timeline/TimelineItem.h"
-
QSharedPointer<MatrixClient> AvatarProvider::client_;
QMap<QString, AvatarData> AvatarProvider::avatars_;
-QMap<QString, QList<TimelineItem *>> AvatarProvider::toBeResolved_;
+QMap<QString, QList<std::function<void(QImage)>>> AvatarProvider::toBeResolved_;
void
AvatarProvider::init(QSharedPointer<MatrixClient> client)
@@ -37,11 +35,11 @@ void
AvatarProvider::updateAvatar(const QString &uid, const QImage &img)
{
if (toBeResolved_.contains(uid)) {
- auto items = toBeResolved_[uid];
+ auto callbacks = toBeResolved_[uid];
// Update all the timeline items with the resolved avatar.
- for (const auto item : items)
- item->setUserAvatar(img);
+ for (const auto callback : callbacks)
+ callback(img);
toBeResolved_.remove(uid);
}
@@ -53,7 +51,7 @@ AvatarProvider::updateAvatar(const QString &uid, const QImage &img)
}
void
-AvatarProvider::resolve(const QString &userId, TimelineItem *item)
+AvatarProvider::resolve(const QString &userId, std::function<void(QImage)> callback)
{
if (!avatars_.contains(userId))
return;
@@ -61,7 +59,7 @@ AvatarProvider::resolve(const QString &userId, TimelineItem *item)
auto img = avatars_[userId].img;
if (!img.isNull()) {
- item->setUserAvatar(img);
+ callback(img);
return;
}
@@ -69,12 +67,12 @@ AvatarProvider::resolve(const QString &userId, TimelineItem *item)
if (!toBeResolved_.contains(userId)) {
client_->fetchUserAvatar(userId, avatars_[userId].url);
- QList<TimelineItem *> timelineItems;
- timelineItems.push_back(item);
+ QList<std::function<void(QImage)>> items;
+ items.push_back(callback);
- toBeResolved_.insert(userId, timelineItems);
+ toBeResolved_.insert(userId, items);
} else {
- toBeResolved_[userId].push_back(item);
+ toBeResolved_[userId].push_back(callback);
}
}
diff --git a/src/Cache.cc b/src/Cache.cc
index 06e45f13..e4e700b2 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -36,6 +36,7 @@ Cache::Cache(const QString &userId)
, roomDb_{0}
, invitesDb_{0}
, imagesDb_{0}
+ , readReceiptsDb_{0}
, isMounted_{false}
, userId_{userId}
{}
@@ -89,11 +90,12 @@ Cache::setup()
env_.open(statePath.toStdString().c_str());
}
- auto txn = lmdb::txn::begin(env_);
- stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE);
- roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE);
- invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE);
- imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE);
+ auto txn = lmdb::txn::begin(env_);
+ stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE);
+ roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE);
+ invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE);
+ imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE);
+ readReceiptsDb_ = lmdb::dbi::open(txn, "read_receipts", MDB_CREATE);
txn.commit();
@@ -449,3 +451,94 @@ Cache::setInvites(const std::map<std::string, mtx::responses::InvitedRoom> &invi
unmount();
}
}
+
+std::multimap<uint64_t, std::string>
+Cache::readReceipts(const QString &event_id, const QString &room_id)
+{
+ std::multimap<uint64_t, std::string> receipts;
+
+ ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
+ nlohmann::json json_key = receipt_key;
+
+ try {
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto key = json_key.dump();
+
+ lmdb::val value;
+
+ bool res =
+ lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value);
+
+ txn.commit();
+
+ if (res) {
+ auto json_response = json::parse(std::string(value.data(), value.size()));
+ auto values = json_response.get<std::vector<ReadReceiptValue>>();
+
+ for (auto v : values)
+ receipts.emplace(v.ts, v.user_id);
+ }
+
+ } catch (const lmdb::error &e) {
+ qCritical() << "readReceipts:" << e.what();
+ }
+
+ return receipts;
+}
+
+using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
+void
+Cache::updateReadReceipt(const std::string &room_id, const Receipts &receipts)
+{
+ for (auto receipt : receipts) {
+ const auto event_id = receipt.first;
+ auto event_receipts = receipt.second;
+
+ ReadReceiptKey receipt_key{event_id, room_id};
+ nlohmann::json json_key = receipt_key;
+
+ try {
+ auto read_txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ const auto key = json_key.dump();
+
+ lmdb::val prev_value;
+
+ bool exists = lmdb::dbi_get(
+ read_txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value);
+
+ read_txn.commit();
+
+ std::vector<ReadReceiptValue> saved_receipts;
+
+ // If an entry for the event id already exists, we would
+ // merge the existing receipts with the new ones.
+ if (exists) {
+ auto json_value =
+ json::parse(std::string(prev_value.data(), prev_value.size()));
+
+ // Retrieve the saved receipts.
+ saved_receipts = json_value.get<std::vector<ReadReceiptValue>>();
+ }
+
+ // Append the new ones.
+ for (auto event_receipt : event_receipts)
+ saved_receipts.push_back(
+ ReadReceiptValue{event_receipt.first, event_receipt.second});
+
+ // Save back the merged (or only the new) receipts.
+ nlohmann::json json_updated_value = saved_receipts;
+ std::string merged_receipts = json_updated_value.dump();
+
+ auto txn = lmdb::txn::begin(env_);
+
+ lmdb::dbi_put(txn,
+ readReceiptsDb_,
+ lmdb::val(key.data(), key.size()),
+ lmdb::val(merged_receipts.data(), merged_receipts.size()));
+
+ txn.commit();
+ } catch (const lmdb::error &e) {
+ qCritical() << "updateReadReceipts:" << e.what();
+ }
+ }
+}
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 071fef71..3958e2c2 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -39,11 +39,14 @@
#include "UserInfoWidget.h"
#include "UserSettingsPage.h"
+#include "dialogs/ReadReceipts.h"
#include "timeline/TimelineViewManager.h"
constexpr int MAX_INITIAL_SYNC_FAILURES = 5;
constexpr int SYNC_RETRY_TIMEOUT = 10000;
+ChatPage *ChatPage::instance_ = nullptr;
+
ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
QSharedPointer<UserSettings> userSettings,
QWidget *parent)
@@ -302,6 +305,8 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
});
AvatarProvider::init(client);
+
+ instance_ = this;
}
void
@@ -734,6 +739,12 @@ ChatPage::updateJoinedRooms(const std::map<std::string, mtx::responses::JoinedRo
updateTypingUsers(roomid, it->second.ephemeral.typing);
+ if (it->second.ephemeral.receipts.size() > 0)
+ QtConcurrent::run(cache_.data(),
+ &Cache::updateReadReceipt,
+ it->first,
+ it->second.ephemeral.receipts);
+
const auto newStateEvents = it->second.state;
const auto newTimelineEvents = it->second.timeline;
@@ -809,4 +820,25 @@ ChatPage::generateMembershipDifference(
return stateDiff;
}
+void
+ChatPage::showReadReceipts(const QString &event_id)
+{
+ if (receiptsDialog_.isNull()) {
+ receiptsDialog_ = QSharedPointer<dialogs::ReadReceipts>(
+ new dialogs::ReadReceipts(this),
+ [=](dialogs::ReadReceipts *dialog) { dialog->deleteLater(); });
+ }
+
+ if (receiptsModal_.isNull()) {
+ receiptsModal_ = QSharedPointer<OverlayModal>(
+ new OverlayModal(MainWindow::instance(), receiptsDialog_.data()),
+ [=](OverlayModal *modal) { modal->deleteLater(); });
+ receiptsModal_->setDuration(0);
+ receiptsModal_->setColor(QColor(30, 30, 30, 170));
+ }
+
+ receiptsDialog_->addUsers(cache_->readReceipts(event_id, current_room_));
+ receiptsModal_->fadeIn();
+}
+
ChatPage::~ChatPage() {}
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index 39f58dac..f2b7005b 100644
--- a/src/MainWindow.cc
+++ b/src/MainWindow.cc
@@ -281,10 +281,4 @@ MainWindow::hasActiveUser()
settings.contains("auth/user_id");
}
-MainWindow *
-MainWindow::instance()
-{
- return instance_;
-}
-
MainWindow::~MainWindow() {}
diff --git a/src/dialogs/ReadReceipts.cc b/src/dialogs/ReadReceipts.cc
new file mode 100644
index 00000000..ae28969f
--- /dev/null
+++ b/src/dialogs/ReadReceipts.cc
@@ -0,0 +1,124 @@
+#include <QDebug>
+#include <QIcon>
+#include <QListWidgetItem>
+#include <QPainter>
+#include <QStyleOption>
+#include <QTimer>
+#include <QVBoxLayout>
+
+#include "Config.h"
+
+#include "Avatar.h"
+#include "AvatarProvider.h"
+#include "dialogs/ReadReceipts.h"
+#include "timeline/TimelineViewManager.h"
+
+using namespace dialogs;
+
+ReceiptItem::ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp)
+ : QWidget(parent)
+{
+ topLayout_ = new QHBoxLayout(this);
+ topLayout_->setMargin(0);
+
+ textLayout_ = new QVBoxLayout;
+ textLayout_->setMargin(0);
+ textLayout_->setSpacing(5);
+
+ QFont font;
+ font.setPixelSize(conf::receipts::font);
+
+ auto displayName = TimelineViewManager::displayName(user_id);
+
+ avatar_ = new Avatar(this);
+ avatar_->setSize(40);
+ avatar_->setLetter(QChar(displayName[0]));
+
+ // If it's a matrix id we use the second letter.
+ if (displayName.size() > 1 && displayName.at(0) == '@')
+ avatar_->setLetter(QChar(displayName.at(1)));
+
+ userName_ = new QLabel(displayName, this);
+ userName_->setFont(font);
+
+ timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this);
+ timestamp_->setFont(font);
+
+ textLayout_->addWidget(userName_);
+ textLayout_->addWidget(timestamp_);
+
+ topLayout_->addWidget(avatar_);
+ topLayout_->addLayout(textLayout_, 1);
+
+ AvatarProvider::resolve(user_id, [=](const QImage &img) { avatar_->setImage(img); });
+}
+
+QString
+ReceiptItem::dateFormat(const QDateTime &then) const
+{
+ auto now = QDateTime::currentDateTime();
+ auto days = then.daysTo(now);
+
+ if (days == 0)
+ return QString("Today %1").arg(then.toString("HH:mm"));
+ else if (days < 2)
+ return QString("Yesterday %1").arg(then.toString("HH::mm"));
+ else if (days < 365)
+ return then.toString("dd/MM HH:mm");
+
+ return then.toString("dd/MM/yy");
+}
+
+ReadReceipts::ReadReceipts(QWidget *parent)
+ : QFrame(parent)
+{
+ setMaximumSize(400, 350);
+
+ auto layout = new QVBoxLayout(this);
+ layout->setSpacing(30);
+ layout->setMargin(20);
+
+ userList_ = new QListWidget;
+ userList_->setFrameStyle(QFrame::NoFrame);
+ userList_->setSelectionMode(QAbstractItemView::NoSelection);
+ userList_->setAttribute(Qt::WA_MacShowFocusRect, 0);
+ userList_->setSpacing(5);
+
+ QFont font;
+ font.setPixelSize(conf::headerFontSize);
+
+ topLabel_ = new QLabel(tr("Read receipts"), this);
+ topLabel_->setAlignment(Qt::AlignCenter);
+ topLabel_->setFont(font);
+
+ layout->addWidget(topLabel_);
+ layout->addWidget(userList_);
+}
+
+void
+ReadReceipts::addUsers(const std::multimap<uint64_t, std::string> &receipts)
+{
+ // We want to remove any previous items that have been set.
+ userList_->clear();
+
+ for (auto receipt : receipts) {
+ auto user =
+ new ReceiptItem(this, QString::fromStdString(receipt.second), receipt.first);
+ auto item = new QListWidgetItem(userList_);
+
+ item->setSizeHint(user->minimumSizeHint());
+ item->setFlags(Qt::NoItemFlags);
+ item->setTextAlignment(Qt::AlignCenter);
+
+ userList_->setItemWidget(item, user);
+ }
+}
+
+void
+ReadReceipts::paintEvent(QPaintEvent *)
+{
+ QStyleOption opt;
+ opt.init(this);
+ QPainter p(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+}
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index a42edbb7..202b331d 100644
--- a/src/timeline/TimelineItem.cc
+++ b/src/timeline/TimelineItem.cc
@@ -15,10 +15,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include <QContextMenuEvent>
#include <QFontDatabase>
+#include <QMenu>
#include <QTextEdit>
#include "Avatar.h"
+#include "ChatPage.h"
#include "Config.h"
#include "timeline/TimelineItem.h"
@@ -39,6 +42,14 @@ TimelineItem::init()
QFontMetrics fm(font_);
+ receiptsMenu_ = new QMenu(this);
+ showReadReceipts_ = new QAction("Read receipts", this);
+ receiptsMenu_->addAction(showReadReceipts_);
+ connect(showReadReceipts_, &QAction::triggered, this, [=]() {
+ if (!event_id_.isEmpty())
+ ChatPage::instance()->showReadReceipts(event_id_);
+ });
+
topLayout_ = new QHBoxLayout(this);
sideLayout_ = new QVBoxLayout;
mainLayout_ = new QVBoxLayout;
@@ -88,7 +99,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty,
setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(userid, this);
+ AvatarProvider::resolve(userid, [=](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
@@ -213,7 +224,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(sender, this);
+ AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
@@ -252,7 +263,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote>
setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(sender, this);
+ AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(emoteMsg);
setupSimpleLayout();
@@ -297,7 +308,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(sender, this);
+ AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
@@ -472,6 +483,13 @@ TimelineItem::descriptiveTime(const QDateTime &then)
TimelineItem::~TimelineItem() {}
void
+TimelineItem::contextMenuEvent(QContextMenuEvent *event)
+{
+ if (receiptsMenu_)
+ receiptsMenu_->exec(event->globalPos());
+}
+
+void
TimelineItem::paintEvent(QPaintEvent *)
{
QStyleOption opt;
diff --git a/src/ui/OverlayModal.cc b/src/ui/OverlayModal.cc
index 4fb57175..290d28e5 100644
--- a/src/ui/OverlayModal.cc
+++ b/src/ui/OverlayModal.cc
@@ -47,6 +47,8 @@ OverlayModal::OverlayModal(QWidget *parent, QWidget *content)
if (animation_->direction() == QAbstractAnimation::Forward)
this->close();
});
+
+ content->setFocus();
}
void
@@ -72,3 +74,12 @@ OverlayModal::fadeOut()
animation_->setDirection(QAbstractAnimation::Forward);
animation_->start();
}
+
+void
+OverlayModal::keyPressEvent(QKeyEvent *event)
+{
+ if (event->key() == Qt::Key_Escape) {
+ event->accept();
+ fadeOut();
+ }
+}
|