diff --git a/CMakeLists.txt b/CMakeLists.txt index e1d835f6..e85c586b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ set(SRC_FILES src/dialogs/PreviewUploadOverlay.cc src/dialogs/InviteUsers.cc src/dialogs/JoinRoom.cc + src/dialogs/MemberList.cpp src/dialogs/LeaveRoom.cc src/dialogs/Logout.cc src/dialogs/ReadReceipts.cc @@ -219,6 +220,7 @@ qt5_wrap_cpp(MOC_HEADERS include/dialogs/PreviewUploadOverlay.h include/dialogs/InviteUsers.h include/dialogs/JoinRoom.h + include/dialogs/MemberList.hpp include/dialogs/LeaveRoom.h include/dialogs/Logout.h include/dialogs/ReadReceipts.h diff --git a/include/Cache.h b/include/Cache.h index db5dba00..e0b68549 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -24,6 +24,13 @@ #include #include +struct RoomMember +{ + QString user_id; + QString display_name; + QImage avatar; +}; + struct SearchResult { QString user_id; @@ -32,6 +39,7 @@ struct SearchResult Q_DECLARE_METATYPE(SearchResult) Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(RoomMember); //! Used to uniquely identify a list of read receipts. struct ReadReceiptKey @@ -164,6 +172,11 @@ public: lmdb::dbi &membersdb, const QString &room_id); + //! Retrieve member info from a room. + std::vector getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + void saveState(const mtx::responses::Sync &res); bool isInitialized() const; diff --git a/include/MainWindow.h b/include/MainWindow.h index 08d7e53e..1ffbe850 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -49,6 +49,7 @@ class InviteUsers; class JoinRoom; class LeaveRoom; class Logout; +class MemberList; class ReCaptcha; class RoomSettings; } @@ -70,6 +71,7 @@ public: void openJoinRoomDialog(std::function callback); void openLogoutDialog(std::function callback); void openRoomSettings(const QString &room_id = ""); + void openMemberListDialog(const QString &room_id = ""); protected: void closeEvent(QCloseEvent *event); @@ -153,4 +155,8 @@ private: QSharedPointer roomSettingsModal_; //! Room settings dialog. QSharedPointer roomSettingsDialog_; + //! Member list modal. + QSharedPointer memberListModal_; + //! Member list dialog. + QSharedPointer memberListDialog_; }; diff --git a/include/TopRoomBar.h b/include/TopRoomBar.h index 37a0b61b..54b89a59 100644 --- a/include/TopRoomBar.h +++ b/include/TopRoomBar.h @@ -67,6 +67,7 @@ private: Menu *menu_; QAction *leaveRoom_; + QAction *roomMembers_; QAction *roomSettings_; QAction *inviteUsers_; diff --git a/include/dialogs/MemberList.hpp b/include/dialogs/MemberList.hpp new file mode 100644 index 00000000..09a56aab --- /dev/null +++ b/include/dialogs/MemberList.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +class Avatar; +class Cache; +class FlatButton; +class QHBoxLayout; +class QLabel; +class QVBoxLayout; + +struct RoomMember; + +template +class QSharedPointer; + +namespace dialogs { + +class MemberItem : public QWidget +{ + Q_OBJECT + +public: + MemberItem(const RoomMember &member, QWidget *parent); + +private: + QHBoxLayout *topLayout_; + QVBoxLayout *textLayout_; + + Avatar *avatar_; + + QLabel *userName_; + QLabel *userId_; +}; + +class MemberList : public QFrame +{ + Q_OBJECT +public: + MemberList(const QString &room_id, QSharedPointer cache, QWidget *parent = nullptr); + +public slots: + void addUsers(const std::vector &users); + +protected: + void paintEvent(QPaintEvent *event) override; + void moveButtonToBottom(); + +private: + QString room_id_; + QLabel *topLabel_; + QListWidget *list_; + QSharedPointer cache_; + FlatButton *moreBtn_; +}; +} // dialogs diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 89582348..e0a6fe20 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -140,6 +140,7 @@ dialogs--RoomSettings, dialogs--InviteUsers, dialogs--ReadReceipts, dialogs--JoinRoom, +dialogs--MemberList, dialogs--PreviewUploadOverlay, dialogs--CreateRoom > QLineEdit, dialogs--InviteUsers > QLineEdit, diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 6538a780..2a746e37 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -141,6 +141,7 @@ dialogs--CreateRoom, dialogs--RoomSettings, dialogs--InviteUsers, dialogs--ReadReceipts, +dialogs--MemberList, dialogs--JoinRoom, dialogs--PreviewUploadOverlay, QListWidget { diff --git a/src/Cache.cc b/src/Cache.cc index 60181afc..92c86322 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -1045,6 +1045,48 @@ Cache::searchUsers(const std::string &room_id, const std::string &query, std::ui return results; } +std::vector +Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto db = getMembersDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, db); + + std::size_t currentIndex = 0; + + const auto endIndex = std::min(startIndex + len, db.size(txn)); + + std::vector members; + + std::string user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + if (currentIndex < startIndex) { + currentIndex += 1; + continue; + } + + if (currentIndex >= endIndex) + break; + + try { + MemberInfo tmp = json::parse(user_data); + members.emplace_back( + RoomMember{QString::fromStdString(user_id), + QString::fromStdString(tmp.name), + QImage::fromData(image(txn, tmp.avatar_url))}); + } catch (const json::exception &e) { + qWarning() << e.what(); + } + + currentIndex += 1; + } + + cursor.close(); + txn.commit(); + + return members; +} + QHash Cache::DisplayNames; QHash Cache::AvatarUrls; diff --git a/src/MainWindow.cc b/src/MainWindow.cc index 66f956a5..c2353a11 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -41,6 +41,7 @@ #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" +#include "dialogs/MemberList.hpp" #include "dialogs/RoomSettings.hpp" MainWindow *MainWindow::instance_ = nullptr; @@ -268,8 +269,6 @@ MainWindow::openRoomSettings(const QString &room_id) { const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; - qDebug() << "room settings" << roomToSearch; - roomSettingsDialog_ = QSharedPointer( new dialogs::RoomSettings(roomToSearch, chat_page_->cache(), this)); @@ -279,11 +278,24 @@ MainWindow::openRoomSettings(const QString &room_id) roomSettingsModal_ = QSharedPointer(new OverlayModal(this, roomSettingsDialog_.data())); - roomSettingsModal_->setColor(QColor(30, 30, 30, 170)); roomSettingsModal_->show(); } +void +MainWindow::openMemberListDialog(const QString &room_id) +{ + const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; + + memberListDialog_ = QSharedPointer( + new dialogs::MemberList(roomToSearch, chat_page_->cache(), this)); + + memberListModal_ = + QSharedPointer(new OverlayModal(this, memberListDialog_.data())); + + memberListModal_->show(); +} + void MainWindow::openLeaveRoomDialog(const QString &room_id) { @@ -325,6 +337,7 @@ MainWindow::showOverlayProgressBar() progressModal_ = QSharedPointer(new OverlayModal(this, spinner_.data()), [](OverlayModal *modal) { modal->deleteLater(); }); + progressModal_->setColor(QColor(30, 30, 30)); progressModal_->setDismissible(false); progressModal_->show(); } @@ -377,7 +390,6 @@ MainWindow::openJoinRoomDialog(std::function callb if (joinRoomModal_.isNull()) { joinRoomModal_ = QSharedPointer( new OverlayModal(MainWindow::instance(), joinRoomDialog_.data())); - joinRoomModal_->setColor(QColor(30, 30, 30, 170)); } joinRoomModal_->show(); @@ -406,7 +418,6 @@ MainWindow::openCreateRoomDialog( if (createRoomModal_.isNull()) { createRoomModal_ = QSharedPointer( new OverlayModal(MainWindow::instance(), createRoomDialog_.data())); - createRoomModal_->setColor(QColor(30, 30, 30, 170)); } createRoomModal_->show(); @@ -431,7 +442,6 @@ MainWindow::openLogoutDialog(std::function callback) if (logoutModal_.isNull()) { logoutModal_ = QSharedPointer( new OverlayModal(MainWindow::instance(), logoutDialog_.data())); - logoutModal_->setColor(QColor(30, 30, 30, 170)); } logoutModal_->show(); diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc index beca1d51..7338cfb1 100644 --- a/src/TopRoomBar.cc +++ b/src/TopRoomBar.cc @@ -90,6 +90,11 @@ TopRoomBar::TopRoomBar(QWidget *parent) [this](const QStringList &invitees) { emit inviteUsers(invitees); }); }); + roomMembers_ = new QAction(tr("Members"), this); + connect(roomMembers_, &QAction::triggered, this, []() { + MainWindow::instance()->openMemberListDialog(); + }); + leaveRoom_ = new QAction(tr("Leave room"), this); connect(leaveRoom_, &QAction::triggered, this, []() { MainWindow::instance()->openLeaveRoomDialog(); @@ -101,6 +106,7 @@ TopRoomBar::TopRoomBar(QWidget *parent) }); menu_->addAction(inviteUsers_); + menu_->addAction(roomMembers_); menu_->addAction(leaveRoom_); menu_->addAction(roomSettings_); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp new file mode 100644 index 00000000..26f526dd --- /dev/null +++ b/src/dialogs/MemberList.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include + +#include "Config.h" +#include "FlatButton.h" +#include "Utils.h" + +#include "Avatar.h" +#include "Cache.h" +#include "dialogs/MemberList.hpp" + +using namespace dialogs; + +MemberItem::MemberItem(const RoomMember &member, QWidget *parent) + : QWidget(parent) +{ + topLayout_ = new QHBoxLayout(this); + topLayout_->setMargin(0); + + textLayout_ = new QVBoxLayout; + textLayout_->setMargin(0); + textLayout_->setSpacing(0); + + avatar_ = new Avatar(this); + avatar_->setSize(44); + avatar_->setLetter(utils::firstChar(member.display_name)); + + if (!member.avatar.isNull()) + avatar_->setImage(member.avatar); + + QFont nameFont, idFont; + nameFont.setWeight(65); + nameFont.setPixelSize(conf::receipts::font + 1); + idFont.setWeight(50); + idFont.setPixelSize(conf::receipts::font); + + userName_ = new QLabel(member.display_name, this); + userName_->setFont(nameFont); + + userId_ = new QLabel(member.user_id, this); + userId_->setFont(idFont); + + textLayout_->addWidget(userName_); + textLayout_->addWidget(userId_); + + topLayout_->addWidget(avatar_); + topLayout_->addLayout(textLayout_, 1); +} + +MemberList::MemberList(const QString &room_id, QSharedPointer cache, QWidget *parent) + : QFrame(parent) + , room_id_{room_id} + , cache_{cache} +{ + setMaximumSize(420, 380); + setAttribute(Qt::WA_DeleteOnClose, true); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(30); + layout->setMargin(20); + + list_ = new QListWidget; + list_->setFrameStyle(QFrame::NoFrame); + list_->setSelectionMode(QAbstractItemView::NoSelection); + list_->setAttribute(Qt::WA_MacShowFocusRect, 0); + list_->setSpacing(5); + + QFont font; + font.setPixelSize(conf::headerFontSize); + + topLabel_ = new QLabel(tr("Room members"), this); + topLabel_->setAlignment(Qt::AlignCenter); + topLabel_->setFont(font); + + layout->addWidget(topLabel_); + layout->addWidget(list_); + + list_->clear(); + + // Add button at the bottom. + moreBtn_ = new FlatButton(tr("SHOW MORE"), this); + auto item = new QListWidgetItem; + item->setSizeHint(moreBtn_->minimumSizeHint()); + item->setFlags(Qt::NoItemFlags); + item->setTextAlignment(Qt::AlignCenter); + list_->insertItem(0, item); + list_->setItemWidget(item, moreBtn_); + + connect(moreBtn_, &FlatButton::clicked, this, [this]() { + const size_t numMembers = list_->count() - 1; + + if (numMembers > 0) + addUsers(cache_->getMembers(room_id_.toStdString(), numMembers)); + }); + + try { + addUsers(cache_->getMembers(room_id_.toStdString())); + } catch (const lmdb::error &e) { + qCritical() << e.what(); + } +} + +void +MemberList::moveButtonToBottom() +{ + auto item = new QListWidgetItem(list_); + item->setSizeHint(moreBtn_->minimumSizeHint()); + item->setFlags(Qt::NoItemFlags); + item->setTextAlignment(Qt::AlignCenter); + list_->setItemWidget(item, moreBtn_); + list_->addItem(item); +} + +void +MemberList::addUsers(const std::vector &members) +{ + if (members.size() == 0) { + moreBtn_->hide(); + } else { + moreBtn_->show(); + } + + for (const auto &member : members) { + auto user = new MemberItem(member, this); + auto item = new QListWidgetItem; + + item->setSizeHint(user->minimumSizeHint()); + item->setFlags(Qt::NoItemFlags); + item->setTextAlignment(Qt::AlignCenter); + + list_->insertItem(list_->count() - 1, item); + list_->setItemWidget(item, user); + } +} + +void +MemberList::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 8fcbc82f..b2c87c94 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -49,18 +49,20 @@ RoomSettings::RoomSettings(const QString &room_id, QSharedPointer cache, auto notifOptionLayout_ = new QHBoxLayout; notifOptionLayout_->setMargin(5); - auto themeLabel_ = new QLabel(tr("Notifications"), this); - auto notifCombo = new QComboBox(this); - notifCombo->addItem("Nothing"); - notifCombo->addItem("Mentions only"); - notifCombo->addItem("All messages"); - themeLabel_->setStyleSheet("font-size: 15px;"); + auto notifLabel = new QLabel(tr("Notifications"), this); + auto notifCombo = new QComboBox(this); + notifCombo->setDisabled(true); + notifCombo->addItem(tr("Muted")); + notifCombo->addItem(tr("Mentions only")); + notifCombo->addItem(tr("All messages")); + notifLabel->setStyleSheet("font-size: 15px;"); - notifOptionLayout_->addWidget(themeLabel_); + notifOptionLayout_->addWidget(notifLabel); notifOptionLayout_->addWidget(notifCombo, 0, Qt::AlignBottom | Qt::AlignRight); layout->addWidget(new TopSection(info_, avatarImg_, this)); layout->addLayout(notifOptionLayout_); + layout->addLayout(notifOptionLayout_); layout->addLayout(btnLayout); connect(cancelBtn_, &FlatButton::clicked, this, &RoomSettings::closing); diff --git a/src/ui/OverlayModal.cc b/src/ui/OverlayModal.cc index 4b0747a1..6aa16b07 100644 --- a/src/ui/OverlayModal.cc +++ b/src/ui/OverlayModal.cc @@ -23,7 +23,7 @@ OverlayModal::OverlayModal(QWidget *parent, QWidget *content) : OverlayWidget(parent) , content_{content} - , color_{QColor(55, 55, 55)} + , color_{QColor(30, 30, 30, 170)} { auto layout = new QVBoxLayout(); layout->addWidget(content);