diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6b26b2e5..84f52766 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -285,7 +285,6 @@ set(SRC_FILES
src/dialogs/JoinRoom.cpp
src/dialogs/LeaveRoom.cpp
src/dialogs/Logout.cpp
- src/dialogs/MemberList.cpp
src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp
src/dialogs/ReadReceipts.cpp
@@ -351,6 +350,7 @@ set(SRC_FILES
src/LoginPage.cpp
src/MainWindow.cpp
src/MatrixClient.cpp
+ src/MemberList.cpp
src/MxcImageProvider.cpp
src/Olm.cpp
src/RegisterPage.cpp
@@ -496,7 +496,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/JoinRoom.h
src/dialogs/LeaveRoom.h
src/dialogs/Logout.h
- src/dialogs/MemberList.h
src/dialogs/PreviewUploadOverlay.h
src/dialogs/RawMessage.h
src/dialogs/ReCaptcha.h
@@ -557,6 +556,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/InviteeItem.h
src/LoginPage.h
src/MainWindow.h
+ src/MemberList.h
src/MxcImageProvider.h
src/RegisterPage.h
src/SSOHandler.h
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml
new file mode 100644
index 00000000..4406c1b0
--- /dev/null
+++ b/resources/qml/RoomMembers.qml
@@ -0,0 +1,111 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
+import im.nheko 1.0
+
+ApplicationWindow {
+ id: roomMembersRoot
+
+ property string roomName: Rooms.currentRoom.roomName
+ property MemberList members
+
+ title: qsTr("Members of ") + roomName
+ x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+ y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+ height: 650
+ width: 420
+ minimumHeight: 420
+
+ Shortcut {
+ sequence: StandardKey.Cancel
+ onActivated: roomMembersRoot.close()
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 10
+
+ Avatar {
+ id: roomAvatar
+
+ width: 130
+ height: width
+ displayName: members.roomName
+ Layout.alignment: Qt.AlignHCenter
+ url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
+ onClicked: TimelineManager.timeline.openRoomSettings(members.roomId)
+ }
+
+ Label {
+ font.pixelSize: 24
+ text: members.memberCount + (members.memberCount === 1 ? qsTr(" person in ") : qsTr(" people in ")) + roomName
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ ScrollView {
+ clip: false
+ palette: colors
+ padding: 10
+ ScrollBar.horizontal.visible: false
+ Layout.fillHeight: true
+ Layout.minimumHeight: 200
+ Layout.fillWidth: true
+
+ ListView {
+ id: memberList
+
+ clip: true
+ spacing: 8
+ boundsBehavior: Flickable.StopAtBounds
+ model: members
+
+ ScrollHelper {
+ flickable: parent
+ anchors.fill: parent
+ enabled: !Settings.mobileMode
+ }
+
+ delegate: RowLayout {
+ spacing: 10
+
+ Avatar {
+ width: avatarSize
+ height: avatarSize
+ userid: model.mxid
+ url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+ displayName: model.displayName
+ onClicked: TimelineManager.timeline.openUserProfile(model.mxid)
+ }
+
+ ColumnLayout {
+ spacing: 5
+
+ Label {
+ text: model.displayName
+ color: TimelineManager.userColor(model ? model.mxid : "", colors.window)
+ font.pointSize: 12
+ }
+
+ Label {
+ text: model.mxid
+ color: colors.buttonText
+ font.pointSize: 10
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+ }
+ }
+
+ footer: DialogButtonBox {
+ standardButtons: DialogButtonBox.Ok
+ onAccepted: roomMembersRoot.close()
+ }
+}
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 58aba0c7..50c2447c 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -116,7 +116,7 @@ Rectangle {
Platform.MenuItem {
text: qsTr("Members")
- onTriggered: TimelineManager.openMemberListDialog(room.roomId())
+ onTriggered: Rooms.currentRoom.openRoomMembers(room.roomId())
}
Platform.MenuItem {
diff --git a/resources/res.qrc b/resources/res.qrc
index e9479e57..da5288c8 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -185,6 +185,7 @@
qml/components/AdaptiveLayout.qml
qml/components/AdaptiveLayoutElement.qml
qml/components/FlatButton.qml
+ qml/RoomMembers.qml
media/ring.ogg
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ed337ca4..36bada83 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -21,6 +21,7 @@
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "MemberList.h"
#include "RegisterPage.h"
#include "TrayIcon.h"
#include "UserSettingsPage.h"
@@ -36,7 +37,6 @@
#include "dialogs/JoinRoom.h"
#include "dialogs/LeaveRoom.h"
#include "dialogs/Logout.h"
-#include "dialogs/MemberList.h"
#include "dialogs/ReadReceipts.h"
MainWindow *MainWindow::instance_ = nullptr;
@@ -310,14 +310,6 @@ MainWindow::hasActiveUser()
settings.contains(prefix + "auth/user_id");
}
-void
-MainWindow::openMemberListDialog(const QString &room_id)
-{
- auto dialog = new dialogs::MemberList(room_id, this);
-
- showDialog(dialog);
-}
-
void
MainWindow::openLeaveRoomDialog(const QString &room_id)
{
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 3571f079..6d62545c 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -65,7 +65,6 @@ public:
std::function callback);
void openJoinRoomDialog(std::function callback);
void openLogoutDialog();
- void openMemberListDialog(const QString &room_id);
void openReadReceiptsDialog(const QString &event_id);
void hideOverlay();
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
new file mode 100644
index 00000000..62488277
--- /dev/null
+++ b/src/MemberList.cpp
@@ -0,0 +1,91 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "MemberList.h"
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Config.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "timeline/TimelineViewManager.h"
+#include "ui/Avatar.h"
+
+MemberList::MemberList(const QString &room_id, QWidget *parent)
+ : QAbstractListModel{parent}
+ , room_id_{room_id}
+{
+ try {
+ info_ = cache::singleRoomInfo(room_id_.toStdString());
+ } catch (const lmdb::error &) {
+ nhlog::db()->warn("failed to retrieve room info from cache: {}",
+ room_id_.toStdString());
+ }
+
+ try {
+ addUsers(cache::getMembers(room_id_.toStdString()));
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
+ }
+}
+
+void
+MemberList::addUsers(const std::vector &members)
+{
+ beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1);
+
+ for (const auto &member : members)
+ m_memberList.push_back(
+ {member,
+ ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl(
+ member.user_id)});
+
+ endInsertRows();
+}
+
+QHash
+MemberList::roleNames() const
+{
+ return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}};
+}
+
+QVariant
+MemberList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0)
+ return {};
+
+ switch (role) {
+ case Mxid:
+ return m_memberList[index.row()].first.user_id;
+ case DisplayName:
+ return m_memberList[index.row()].first.display_name;
+ case AvatarUrl:
+ return m_memberList[index.row()].second;
+ default:
+ return {};
+ }
+}
+
+bool MemberList::canFetchMore(const QModelIndex &) const
+{
+ const size_t numMembers = rowCount();
+ return (numMembers > 1 && numMembers < info_.member_count);
+}
+
+void
+MemberList::fetchMore(const QModelIndex &)
+{
+ addUsers(cache::getMembers(room_id_.toStdString(), rowCount()));
+}
diff --git a/src/MemberList.h b/src/MemberList.h
new file mode 100644
index 00000000..dbe69f4b
--- /dev/null
+++ b/src/MemberList.h
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "CacheStructs.h"
+#include
+
+class MemberList : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+ Q_PROPERTY(size_t memberCount READ memberCount NOTIFY memberCountChanged)
+ Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+ Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged)
+
+public:
+ enum Roles
+ {
+ Mxid,
+ DisplayName,
+ AvatarUrl,
+ };
+ MemberList(const QString &room_id, QWidget *parent = nullptr);
+
+ QHash roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ Q_UNUSED(parent)
+ return static_cast(m_memberList.size());
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ QString roomName() const { return QString::fromStdString(info_.name); }
+ size_t memberCount() const { return info_.member_count; }
+ QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); }
+ QString roomId() const { return room_id_; }
+
+signals:
+ void roomNameChanged();
+ void memberCountChanged();
+ void avatarUrlChanged();
+ void roomIdChanged();
+
+public slots:
+ void addUsers(const std::vector &users);
+
+protected:
+ bool canFetchMore(const QModelIndex &) const;
+ void fetchMore(const QModelIndex &);
+
+private:
+ QVector> m_memberList;
+ QString room_id_;
+ RoomInfo info_;
+};
diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
deleted file mode 100644
index 21eb72b0..00000000
--- a/src/dialogs/MemberList.cpp
+++ /dev/null
@@ -1,146 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "dialogs/MemberList.h"
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-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, 44);
- avatar_->setLetter(utils::firstChar(member.display_name));
-
- avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id);
-
- QFont nameFont;
- nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-
- userId_ = new QLabel(member.user_id, this);
- userName_ = new QLabel(member.display_name, this);
- userName_->setFont(nameFont);
-
- textLayout_->addWidget(userName_);
- textLayout_->addWidget(userId_);
-
- topLayout_->addWidget(avatar_);
- topLayout_->addLayout(textLayout_, 1);
-}
-
-void
-MemberItem::paintEvent(QPaintEvent *)
-{
- QStyleOption opt;
- opt.init(this);
- QPainter p(this);
- style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-MemberList::MemberList(const QString &room_id, QWidget *parent)
- : QFrame(parent)
- , room_id_{room_id}
-{
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- list_ = new QListWidget;
- list_->setFrameStyle(QFrame::NoFrame);
- list_->setSelectionMode(QAbstractItemView::NoSelection);
- list_->setSpacing(5);
-
- QFont largeFont;
- largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
- setMinimumHeight(list_->sizeHint().height() * 2);
- setMinimumWidth(std::max(list_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
- QFontMetrics(largeFont).averageCharWidth() * 30 -
- 2 * conf::modals::WIDGET_MARGIN));
-
- QFont font;
- font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
- topLabel_ = new QLabel(tr("Room members"), this);
- topLabel_->setAlignment(Qt::AlignCenter);
- topLabel_->setFont(font);
-
- auto okBtn = new QPushButton(tr("OK"), this);
-
- auto buttonLayout = new QHBoxLayout();
- buttonLayout->setSpacing(15);
- buttonLayout->addStretch(1);
- buttonLayout->addWidget(okBtn);
-
- layout->addWidget(topLabel_);
- layout->addWidget(list_);
- layout->addLayout(buttonLayout);
-
- list_->clear();
-
- connect(list_->verticalScrollBar(), &QAbstractSlider::valueChanged, this, [this](int pos) {
- if (pos != list_->verticalScrollBar()->maximum())
- return;
-
- 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) {
- nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
- }
-
- auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
- connect(closeShortcut, &QShortcut::activated, this, &MemberList::close);
- connect(okBtn, &QPushButton::clicked, this, &MemberList::close);
-}
-
-void
-MemberList::addUsers(const std::vector &members)
-{
- 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);
- }
-}
diff --git a/src/dialogs/MemberList.h b/src/dialogs/MemberList.h
deleted file mode 100644
index b822eec8..00000000
--- a/src/dialogs/MemberList.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include
-#include
-
-class Avatar;
-class QPushButton;
-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);
-
-protected:
- void paintEvent(QPaintEvent *) override;
-
-private:
- QHBoxLayout *topLayout_;
- QVBoxLayout *textLayout_;
-
- Avatar *avatar_;
-
- QLabel *userName_;
- QLabel *userId_;
-};
-
-class MemberList : public QFrame
-{
- Q_OBJECT
-public:
- MemberList(const QString &room_id, QWidget *parent = nullptr);
-
-public slots:
- void addUsers(const std::vector &users);
-
-private:
- QString room_id_;
- QLabel *topLabel_;
- QListWidget *list_;
-};
-} // dialogs
diff --git a/src/main.cpp b/src/main.cpp
index 29e93d49..376fc4f5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -205,6 +205,9 @@ main(int argc, char *argv[])
parser.process(app);
+ // make sure that size_t properties will work
+ qRegisterMetaType("size_t");
+
app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"}));
http::init();
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 7b3f0729..48d69493 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -25,6 +25,7 @@
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "MemberList.h"
#include "MxcImageProvider.h"
#include "Olm.h"
#include "TimelineViewManager.h"
@@ -1061,9 +1062,16 @@ TimelineModel::openUserProfile(QString userid)
}
void
-TimelineModel::openRoomSettings()
+TimelineModel::openRoomMembers()
{
- RoomSettings *settings = new RoomSettings(roomId(), this);
+ MemberList *memberList = new MemberList(roomId());
+ emit openRoomMembersDialog(memberList);
+}
+
+void
+TimelineModel::openRoomSettings(QString room_id)
+{
+ RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this);
connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
openRoomSettingsDialog(settings);
}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3c80ade8..feb7b5f5 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -17,6 +17,7 @@
#include "CacheStructs.h"
#include "EventStore.h"
#include "InputBar.h"
+#include "MemberList.h"
#include "Permissions.h"
#include "ui/RoomSettings.h"
#include "ui/UserProfile.h"
@@ -235,7 +236,8 @@ public:
Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void openUserProfile(QString userid);
- Q_INVOKABLE void openRoomSettings();
+ Q_INVOKABLE void openRoomMembers();
+ Q_INVOKABLE void openRoomSettings(QString room_id = QString());
Q_INVOKABLE void editAction(QString id);
Q_INVOKABLE void replyAction(QString id);
Q_INVOKABLE void readReceiptsAction(QString id) const;
@@ -352,6 +354,7 @@ signals:
void lastMessageChanged();
void notificationsChanged();
+ void openRoomMembersDialog(MemberList *members);
void openRoomSettingsDialog(RoomSettings *settings);
void newMessageToSend(mtx::events::collections::TimelineEvents event);
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3e69f92b..011ff61c 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -174,6 +174,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"UserProfileModel",
"UserProfile needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType(
+ "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side");
qmlRegisterUncreatableType(
"im.nheko",
1,
@@ -428,11 +430,6 @@ TimelineViewManager::openInviteUsersDialog()
[this](const QStringList &invitees) { emit inviteUsers(invitees); });
}
void
-TimelineViewManager::openMemberListDialog(QString roomid) const
-{
- MainWindow::instance()->openMemberListDialog(roomid);
-}
-void
TimelineViewManager::openLeaveRoomDialog(QString roomid) const
{
MainWindow::instance()->openLeaveRoomDialog(roomid);
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 15b4f523..39d0d31c 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -66,7 +66,6 @@ public:
Q_INVOKABLE void focusMessageInput();
Q_INVOKABLE void openInviteUsersDialog();
- Q_INVOKABLE void openMemberListDialog(QString roomid) const;
Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);