Initial support for read receipts

This commit is contained in:
Konstantinos Sideris 2018-01-03 18:05:49 +02:00
parent 44ee1b549d
commit eaf05748ff
19 changed files with 434 additions and 33 deletions

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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() {}

View file

@ -281,10 +281,4 @@ MainWindow::hasActiveUser()
settings.contains("auth/user_id");
}
MainWindow *
MainWindow::instance()
{
return instance_;
}
MainWindow::~MainWindow() {}

124
src/dialogs/ReadReceipts.cc Normal file
View file

@ -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);
}

View file

@ -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();
@ -471,6 +482,13 @@ TimelineItem::descriptiveTime(const QDateTime &then)
TimelineItem::~TimelineItem() {}
void
TimelineItem::contextMenuEvent(QContextMenuEvent *event)
{
if (receiptsMenu_)
receiptsMenu_->exec(event->globalPos());
}
void
TimelineItem::paintEvent(QPaintEvent *)
{

View file

@ -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();
}
}