summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-01-03 18:05:49 +0200
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-01-03 18:06:29 +0200
commiteaf05748ff1fc2b1ced8fdb329661ff20d6b7b85 (patch)
tree4d2f190f9662581f7ff8a1e95b146ba225e0a8a6 /src
parentAdd Alpine Linux installation instructions (#191) (diff)
downloadnheko-eaf05748ff1fc2b1ced8fdb329661ff20d6b7b85.tar.xz
Initial support for read receipts
Diffstat (limited to 'src')
-rw-r--r--src/AvatarProvider.cc22
-rw-r--r--src/Cache.cc103
-rw-r--r--src/ChatPage.cc32
-rw-r--r--src/MainWindow.cc6
-rw-r--r--src/dialogs/ReadReceipts.cc124
-rw-r--r--src/timeline/TimelineItem.cc26
-rw-r--r--src/ui/OverlayModal.cc11
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();
+        }
+}