diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index fec3f6b4..3f0f77ed 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -117,12 +117,12 @@ ColumnLayout { palette: Nheko.colors color: labelC.color opacity: labelC.text ? 0 : 1 + focus: true onTextEdited: c.textEdited() onAccepted: c.accepted() onEditingFinished: c.editingFinished() - background: Rectangle { id: backgroundRect diff --git a/resources/qml/dialogs/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml index 55d5488b..8a12e5bc 100644 --- a/resources/qml/dialogs/RoomMembers.qml +++ b/resources/qml/dialogs/RoomMembers.qml @@ -63,6 +63,37 @@ ApplicationWindow { onClicked: TimelineManager.openInviteUsers(members.roomId) } + MatrixTextField { + id: searchBar + + Layout.fillWidth: true + placeholderText: qsTr("Search...") + onTextChanged: members.setFilterString(text) + + Component.onCompleted: forceActiveFocus() + } + + RowLayout { + spacing: Nheko.paddingMedium + + Label { + text: qsTr("Sort by: ") + color: Nheko.colors.text + } + + ComboBox { + model: ListModel { + ListElement { data: MemberList.Mxid; text: qsTr("User ID") } + ListElement { data: MemberList.DisplayName; text: qsTr("Display name") } + ListElement { data: MemberList.Powerlevel; text: qsTr("Power level") } + } + textRole: "text" + valueRole: "data" + onCurrentValueChanged: members.sortBy(currentValue) + Layout.fillWidth: true + } + } + ScrollView { palette: Nheko.colors padding: Nheko.paddingMedium @@ -172,14 +203,14 @@ ApplicationWindow { width: parent.width visible: (members.numUsersLoaded < members.memberCount) && members.loadingMoreMembers // use the default height if it's visible, otherwise no height at all - height: membersLoadingSpinner.height + height: membersLoadingSpinner.implicitHeight anchors.margins: Nheko.paddingMedium Spinner { id: membersLoadingSpinner anchors.centerIn: parent - height: visible ? 35 : 0 + implicitHeight: parent.visible ? 35 : 0 } } diff --git a/src/MemberList.cpp b/src/MemberList.cpp index fb4ae76b..f5154da4 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -6,15 +6,20 @@ #include "MemberList.h" #include "Cache.h" +#include "Cache_p.h" #include "ChatPage.h" #include "Config.h" #include "Logging.h" #include "Utils.h" #include "timeline/TimelineViewManager.h" -MemberList::MemberList(const QString &room_id, QObject *parent) +MemberListBackend::MemberListBackend(const QString &room_id, QObject *parent) : QAbstractListModel{parent} , room_id_{room_id} + , powerLevels_{cache::client() + ->getStateEvent(room_id_.toStdString()) + .value_or(mtx::events::StateEvent{}) + .content} { try { info_ = cache::singleRoomInfo(room_id_.toStdString()); @@ -23,7 +28,8 @@ MemberList::MemberList(const QString &room_id, QObject *parent) } try { - auto members = cache::getMembers(room_id_.toStdString()); + // HACK: due to QTBUG-1020169, we'll load a big chunk to speed things up + auto members = cache::getMembers(room_id_.toStdString(), 0, -1); addUsers(members); numUsersLoaded_ = members.size(); } catch (const lmdb::error &e) { @@ -32,7 +38,7 @@ MemberList::MemberList(const QString &room_id, QObject *parent) } void -MemberList::addUsers(const std::vector &members) +MemberListBackend::addUsers(const std::vector &members) { beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); @@ -46,7 +52,7 @@ MemberList::addUsers(const std::vector &members) } QHash -MemberList::roleNames() const +MemberListBackend::roleNames() const { return { {Mxid, "mxid"}, @@ -57,7 +63,7 @@ MemberList::roleNames() const } QVariant -MemberList::data(const QModelIndex &index, int role) const +MemberListBackend::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) return {}; @@ -80,13 +86,16 @@ MemberList::data(const QModelIndex &index, int role) const else return stat->user_verified; } + case Powerlevel: + return static_cast( + powerLevels_.user_level(m_memberList[index.row()].first.user_id.toStdString())); default: return {}; } } bool -MemberList::canFetchMore(const QModelIndex &) const +MemberListBackend::canFetchMore(const QModelIndex &) const { const size_t numMembers = rowCount(); if (numMembers > 1 && numMembers < info_.member_count) @@ -96,7 +105,7 @@ MemberList::canFetchMore(const QModelIndex &) const } void -MemberList::fetchMore(const QModelIndex &) +MemberListBackend::fetchMore(const QModelIndex &) { loadingMoreMembers_ = true; emit loadingMoreMembersChanged(); @@ -109,3 +118,49 @@ MemberList::fetchMore(const QModelIndex &) loadingMoreMembers_ = false; emit loadingMoreMembersChanged(); } + +MemberList::MemberList(const QString &room_id, QObject *parent) + : QSortFilterProxyModel{parent} + , m_model{room_id, this} +{ + connect(&m_model, &MemberListBackend::roomNameChanged, this, &MemberList::roomNameChanged); + connect( + &m_model, &MemberListBackend::memberCountChanged, this, &MemberList::memberCountChanged); + connect(&m_model, &MemberListBackend::avatarUrlChanged, this, &MemberList::avatarUrlChanged); + connect(&m_model, &MemberListBackend::roomIdChanged, this, &MemberList::roomIdChanged); + connect(&m_model, + &MemberListBackend::numUsersLoadedChanged, + this, + &MemberList::numUsersLoadedChanged); + connect(&m_model, + &MemberListBackend::loadingMoreMembersChanged, + this, + &MemberList::loadingMoreMembersChanged); + + setSourceModel(&m_model); + setSortRole(MemberSortRoles::Mxid); + sort(0, Qt::AscendingOrder); + setDynamicSortFilter(true); + setFilterCaseSensitivity(Qt::CaseInsensitive); +} + +void +MemberList::setFilterString(const QString &text) +{ + setFilterRegExp(QRegExp::escape(text)); +} + +void +MemberList::sortBy(const MemberSortRoles role) +{ + setSortRole(role); + // Unfortunately, Qt doesn't provide a "setSortOrder" function. + sort(0, role == MemberSortRoles::Powerlevel ? Qt::DescendingOrder : Qt::AscendingOrder); +} + +bool +MemberList::filterAcceptsRow(int source_row, const QModelIndex &) const +{ + return m_model.m_memberList[source_row].first.user_id.contains(filterRegExp()) || + m_model.m_memberList[source_row].first.display_name.contains(filterRegExp()); +} diff --git a/src/MemberList.h b/src/MemberList.h index be345d41..2f90e5e8 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -6,10 +6,13 @@ #pragma once #include +#include + +#include #include "CacheStructs.h" -class MemberList : public QAbstractListModel +class MemberListBackend : public QAbstractListModel { Q_OBJECT @@ -27,8 +30,10 @@ public: DisplayName, AvatarUrl, Trustlevel, + Powerlevel, }; - MemberList(const QString &room_id, QObject *parent = nullptr); + + MemberListBackend(const QString &room_id, QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override @@ -66,4 +71,56 @@ private: RoomInfo info_; int numUsersLoaded_{0}; bool loadingMoreMembers_{false}; + + mtx::events::state::PowerLevels powerLevels_; + + friend class MemberList; +}; + +class MemberList : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) + Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) + +public: + enum MemberSortRoles + { + Mxid = MemberListBackend::Roles::Mxid, + DisplayName = MemberListBackend::Roles::DisplayName, + Powerlevel = MemberListBackend::Roles::Powerlevel, + }; + Q_ENUM(MemberSortRoles) + + MemberList(const QString &room_id, QObject *parent = nullptr); + + QString roomName() const { return m_model.roomName(); } + int memberCount() const { return m_model.memberCount(); } + QString avatarUrl() const { return m_model.avatarUrl(); } + QString roomId() const { return m_model.roomId(); } + int numUsersLoaded() const { return m_model.numUsersLoaded(); } + bool loadingMoreMembers() const { return m_model.loadingMoreMembers(); } + +signals: + void roomNameChanged(); + void memberCountChanged(); + void avatarUrlChanged(); + void roomIdChanged(); + void numUsersLoadedChanged(); + void loadingMoreMembersChanged(); + +public slots: + void setFilterString(const QString &text); + void sortBy(const MemberSortRoles role); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +private: + MemberListBackend m_model; };