diff options
author | Konstantinos Sideris <sideris.konstantin@gmail.com> | 2018-01-03 18:05:49 +0200 |
---|---|---|
committer | Konstantinos Sideris <sideris.konstantin@gmail.com> | 2018-01-03 18:06:29 +0200 |
commit | eaf05748ff1fc2b1ced8fdb329661ff20d6b7b85 (patch) | |
tree | 4d2f190f9662581f7ff8a1e95b146ba225e0a8a6 /src | |
parent | Add Alpine Linux installation instructions (#191) (diff) | |
download | nheko-eaf05748ff1fc2b1ced8fdb329661ff20d6b7b85.tar.xz |
Initial support for read receipts
Diffstat (limited to 'src')
-rw-r--r-- | src/AvatarProvider.cc | 22 | ||||
-rw-r--r-- | src/Cache.cc | 103 | ||||
-rw-r--r-- | src/ChatPage.cc | 32 | ||||
-rw-r--r-- | src/MainWindow.cc | 6 | ||||
-rw-r--r-- | src/dialogs/ReadReceipts.cc | 124 | ||||
-rw-r--r-- | src/timeline/TimelineItem.cc | 26 | ||||
-rw-r--r-- | src/ui/OverlayModal.cc | 11 |
7 files changed, 297 insertions, 27 deletions
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(); + } +} |