diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3085cc76..c669262b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -146,6 +146,7 @@ set(SRC_FILES
src/dialogs/JoinRoom.cc
src/dialogs/LeaveRoom.cc
src/dialogs/Logout.cc
+ src/dialogs/ReadReceipts.cc
# Emoji
src/emoji/Category.cc
@@ -226,6 +227,7 @@ qt5_wrap_cpp(MOC_HEADERS
include/dialogs/JoinRoom.h
include/dialogs/LeaveRoom.h
include/dialogs/Logout.h
+ include/dialogs/ReadReceipts.h
# Emoji
include/emoji/Category.h
diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h
index 906f2593..44bf1ad2 100644
--- a/include/AvatarProvider.h
+++ b/include/AvatarProvider.h
@@ -36,7 +36,7 @@ class AvatarProvider : public QObject
public:
static void init(QSharedPointer<MatrixClient> client);
- static void resolve(const QString &userId, TimelineItem *item);
+ static void resolve(const QString &userId, std::function<void(QImage)> callback);
static void setAvatarUrl(const QString &userId, const QUrl &url);
static void clear();
@@ -48,5 +48,5 @@ private:
using UserID = QString;
static QMap<UserID, AvatarData> avatars_;
- static QMap<UserID, QList<TimelineItem *>> toBeResolved_;
+ static QMap<UserID, QList<std::function<void(QImage)>>> toBeResolved_;
};
diff --git a/include/Cache.h b/include/Cache.h
index 1f6c59f0..ae58e418 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -18,11 +18,52 @@
#pragma once
#include <QDir>
+#include <json.hpp>
#include <lmdb++.h>
#include <mtx/responses.hpp>
class RoomState;
+//! Used to uniquely identify a list of read receipts.
+struct ReadReceiptKey
+{
+ std::string event_id;
+ std::string room_id;
+};
+
+inline void
+to_json(json &j, const ReadReceiptKey &key)
+{
+ j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
+}
+
+inline void
+from_json(const json &j, ReadReceiptKey &key)
+{
+ key.event_id = j.at("event_id").get<std::string>();
+ key.room_id = j.at("room_id").get<std::string>();
+}
+
+//! Decribes a read receipt stored in cache.
+struct ReadReceiptValue
+{
+ std::string user_id;
+ uint64_t ts;
+};
+
+inline void
+to_json(json &j, const ReadReceiptValue &value)
+{
+ j = json{{"user_id", value.user_id}, {"ts", value.ts}};
+}
+
+inline void
+from_json(const json &j, ReadReceiptValue &value)
+{
+ value.user_id = j.at("user_id").get<std::string>();
+ value.ts = j.at("ts").get<uint64_t>();
+}
+
class Cache
{
public:
@@ -48,6 +89,19 @@ public:
bool isFormatValid();
void setCurrentFormat();
+ //! Adds a user to the read list for the given event.
+ //!
+ //! There should be only one user id present in a receipt list per room.
+ //! The user id should be removed from any other lists.
+ using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
+ void updateReadReceipt(const std::string &room_id, const Receipts &receipts);
+
+ //! Retrieve all the read receipts for the given event id and room.
+ //!
+ //! Returns a map of user ids and the time of the read receipt in milliseconds.
+ using UserReceipts = std::multimap<uint64_t, std::string>;
+ UserReceipts readReceipts(const QString &event_id, const QString &room_id);
+
QByteArray image(const QString &url) const;
void saveImage(const QString &url, const QByteArray &data);
@@ -60,6 +114,7 @@ private:
lmdb::dbi roomDb_;
lmdb::dbi invitesDb_;
lmdb::dbi imagesDb_;
+ lmdb::dbi readReceiptsDb_;
bool isMounted_;
diff --git a/include/ChatPage.h b/include/ChatPage.h
index 24fc6a25..584424c0 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -42,6 +42,10 @@ class TypingDisplay;
class UserInfoWidget;
class UserSettings;
+namespace dialogs {
+class ReadReceipts;
+}
+
constexpr int CONSENSUS_TIMEOUT = 1000;
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
constexpr int TYPING_REFRESH_TIMEOUT = 10000;
@@ -59,6 +63,9 @@ public:
// Initialize all the components of the UI.
void bootstrap(QString userid, QString homeserver, QString token);
void showQuickSwitcher();
+ void showReadReceipts(const QString &event_id);
+
+ static ChatPage *instance() { return instance_; }
signals:
void contentLoaded();
@@ -84,6 +91,8 @@ private slots:
void removeInvite(const QString &room_id);
private:
+ static ChatPage *instance_;
+
using UserID = QString;
using RoomStates = QMap<UserID, RoomState>;
using Membership = mtx::events::StateEvent<mtx::events::state::Member>;
@@ -150,6 +159,9 @@ private:
QSharedPointer<QuickSwitcher> quickSwitcher_;
QSharedPointer<OverlayModal> quickSwitcherModal_;
+ QSharedPointer<dialogs::ReadReceipts> receiptsDialog_;
+ QSharedPointer<OverlayModal> receiptsModal_;
+
// Matrix Client API provider.
QSharedPointer<MatrixClient> client_;
diff --git a/include/Config.h b/include/Config.h
index 7d35094e..5492e5fb 100644
--- a/include/Config.h
+++ b/include/Config.h
@@ -15,6 +15,10 @@ static constexpr int emojiSize = 14;
static constexpr int headerFontSize = 21;
static constexpr int typingNotificationFontSize = 11;
+namespace receipts {
+static constexpr int font = 12;
+}
+
namespace dialogs {
static constexpr int labelSize = 15;
}
diff --git a/include/MainWindow.h b/include/MainWindow.h
index 2d047b51..d7c5e41d 100644
--- a/include/MainWindow.h
+++ b/include/MainWindow.h
@@ -42,7 +42,7 @@ public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
- static MainWindow *instance();
+ static MainWindow *instance() { return instance_; };
void saveCurrentWindowSize();
protected:
diff --git a/include/dialogs/ReadReceipts.h b/include/dialogs/ReadReceipts.h
new file mode 100644
index 00000000..42a9e1b7
--- /dev/null
+++ b/include/dialogs/ReadReceipts.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <QDateTime>
+#include <QFrame>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QVBoxLayout>
+
+class Avatar;
+
+namespace dialogs {
+
+class ReceiptItem : public QWidget
+{
+ Q_OBJECT
+
+public:
+ ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp);
+
+private:
+ QString dateFormat(const QDateTime &then) const;
+
+ QHBoxLayout *topLayout_;
+ QVBoxLayout *textLayout_;
+
+ Avatar *avatar_;
+
+ QLabel *userName_;
+ QLabel *timestamp_;
+};
+
+class ReadReceipts : public QFrame
+{
+ Q_OBJECT
+public:
+ explicit ReadReceipts(QWidget *parent = nullptr);
+
+public slots:
+ void addUsers(const std::multimap<uint64_t, std::string> &users);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+
+private:
+ QLabel *topLabel_;
+
+ QListWidget *userList_;
+};
+} // dialogs
diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h
index f1498d1b..78fb95c9 100644
--- a/include/timeline/TimelineItem.h
+++ b/include/timeline/TimelineItem.h
@@ -87,6 +87,7 @@ public:
protected:
void paintEvent(QPaintEvent *event) override;
+ void contextMenuEvent(QContextMenuEvent *event) override;
private:
void init();
@@ -116,6 +117,9 @@ private:
DescInfo descriptionMsg_;
+ QMenu *receiptsMenu_;
+ QAction *showReadReceipts_;
+
QHBoxLayout *topLayout_;
QVBoxLayout *sideLayout_; // Avatar or Timestamp
QVBoxLayout *mainLayout_; // Header & Message body
@@ -156,7 +160,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget,
setupAvatarLayout(displayName);
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(userid, this);
+ AvatarProvider::resolve(userid, [=](const QImage &img) { setUserAvatar(img); });
} else {
setupSimpleLayout();
}
@@ -199,7 +203,7 @@ TimelineItem::setupWidgetLayout(Widget *widget,
mainLayout_->addLayout(headerLayout_);
- AvatarProvider::resolve(sender, this);
+ AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); });
} else {
setupSimpleLayout();
}
diff --git a/include/ui/OverlayModal.h b/include/ui/OverlayModal.h
index 167ba71a..5f6b6eee 100644
--- a/include/ui/OverlayModal.h
+++ b/include/ui/OverlayModal.h
@@ -18,6 +18,7 @@
#pragma once
#include <QGraphicsOpacityEffect>
+#include <QKeyEvent>
#include <QPaintEvent>
#include <QPropertyAnimation>
@@ -37,6 +38,7 @@ public:
protected:
void paintEvent(QPaintEvent *event) override;
+ void keyPressEvent(QKeyEvent *event) override;
private:
int duration_;
diff --git a/libs/matrix-structs b/libs/matrix-structs
-Subproject acb732474665343174209f0518c33a7ca0eb504
+Subproject 3555ec1cfc3865e0ae47c0fa53c9ea00f1e7cb3
diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss
index 970a1d03..a78fb612 100644
--- a/resources/styles/nheko-dark.qss
+++ b/resources/styles/nheko-dark.qss
@@ -84,6 +84,7 @@ dialogs--Logout,
dialogs--LeaveRoom,
dialogs--CreateRoom,
dialogs--InviteUsers,
+dialogs--ReadReceipts,
dialogs--JoinRoom {
background-color: #383c4a;
color: #caccd1;
diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss
index 6bdb6aa6..ce86e212 100644
--- a/resources/styles/nheko.qss
+++ b/resources/styles/nheko.qss
@@ -86,6 +86,7 @@ dialogs--Logout,
dialogs--LeaveRoom,
dialogs--CreateRoom,
dialogs--InviteUsers,
+dialogs--ReadReceipts,
dialogs--JoinRoom {
background-color: white;
color: #333;
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();
+ }
+}
|