Initial support for read receipts
This commit is contained in:
parent
44ee1b549d
commit
eaf05748ff
@ -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
|
||||
|
@ -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_;
|
||||
};
|
||||
|
@ -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_;
|
||||
|
||||
|
@ -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_;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ public:
|
||||
explicit MainWindow(QWidget *parent = 0);
|
||||
~MainWindow();
|
||||
|
||||
static MainWindow *instance();
|
||||
static MainWindow *instance() { return instance_; };
|
||||
void saveCurrentWindowSize();
|
||||
|
||||
protected:
|
||||
|
50
include/dialogs/ReadReceipts.h
Normal file
50
include/dialogs/ReadReceipts.h
Normal file
@ -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
|
@ -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();
|
||||
}
|
||||
|
@ -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_;
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit acb732474665343174209f0518c33a7ca0eb504a
|
||||
Subproject commit 3555ec1cfc3865e0ae47c0fa53c9ea00f1e7cb36
|
@ -84,6 +84,7 @@ dialogs--Logout,
|
||||
dialogs--LeaveRoom,
|
||||
dialogs--CreateRoom,
|
||||
dialogs--InviteUsers,
|
||||
dialogs--ReadReceipts,
|
||||
dialogs--JoinRoom {
|
||||
background-color: #383c4a;
|
||||
color: #caccd1;
|
||||
|
@ -86,6 +86,7 @@ dialogs--Logout,
|
||||
dialogs--LeaveRoom,
|
||||
dialogs--CreateRoom,
|
||||
dialogs--InviteUsers,
|
||||
dialogs--ReadReceipts,
|
||||
dialogs--JoinRoom {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
103
src/Cache.cc
103
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {}
|
||||
|
@ -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
124
src/dialogs/ReadReceipts.cc
Normal 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);
|
||||
}
|
@ -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 *)
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user