From b599f5c0c6d1f7a876b8aad935890c8d53951568 Mon Sep 17 00:00:00 2001 From: Malte E Date: Tue, 13 Dec 2022 13:28:57 +0800 Subject: [PATCH] add user search to invite dialog --- CMakeLists.txt | 8 +- resources/qml/Root.qml | 4 + resources/qml/dialogs/InviteDialog.qml | 232 +++++++++++++++++++------ src/MainWindow.cpp | 6 + src/MainWindow.h | 5 +- src/UserDirectoryModel.cpp | 87 ++++++++++ src/UserDirectoryModel.h | 55 ++++++ src/UsersModel.cpp | 31 +++- src/UsersModel.h | 4 +- 9 files changed, 365 insertions(+), 67 deletions(-) create mode 100644 src/UserDirectoryModel.cpp create mode 100644 src/UserDirectoryModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8658b228..ebdae1a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -493,6 +493,8 @@ set(SRC_FILES src/SingleImagePackModel.h src/TrayIcon.cpp src/TrayIcon.h + src/UserDirectoryModel.cpp + src/UserDirectoryModel.h src/UserSettingsPage.cpp src/UserSettingsPage.h src/UsersModel.cpp @@ -594,14 +596,14 @@ if(USE_BUNDLED_MTXCLIENT) include(FetchContent) FetchContent_Declare( MatrixClient - GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG d187c63a27710fa87a44ab44d43b7cfa2023132a + GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git + GIT_TAG d187c63a27710fa87a44ab44d43b7cfa2023132a ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") FetchContent_MakeAvailable(MatrixClient) else() - find_package(MatrixClient 0.8.1 REQUIRED) + find_package(MatrixClient 0.8.3 REQUIRED) endif() if (VOIP) diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 1f079213..6fd0bfaa 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -33,6 +33,10 @@ Pane { id: publicRooms } + UserDirectoryModel { + id: userDirectory + } + //Timer { // onTriggered: gc() // interval: 1000 diff --git a/resources/qml/dialogs/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml index e7dd4e3a..3105576d 100644 --- a/resources/qml/dialogs/InviteDialog.qml +++ b/resources/qml/dialogs/InviteDialog.qml @@ -15,17 +15,26 @@ ApplicationWindow { property string roomId property string plainRoomName property InviteesModel invitees + property var friendsCompleter + property var profile + minimumWidth: 500 - function addInvite() { - if (inviteeEntry.isValidMxid) { - invitees.addUser(inviteeEntry.text); - inviteeEntry.clear(); - } + Component.onCompleted: { + friendsCompleter = TimelineManager.completerFor("user", "friends") + } + + function addInvite(mxid) { + if (mxid.match("@.+?:.{3,}")) { + invitees.addUser(mxid); + if (mxid == inviteeEntry.text) + inviteeEntry.clear(); + } else + console.log("invalid mxid: " + mxid) } function cleanUpAndClose() { if (inviteeEntry.isValidMxid) - addInvite(); + addInvite(inviteeEntry.text); invitees.accept(); close(); @@ -72,7 +81,7 @@ ApplicationWindow { Layout.fillWidth: true onAccepted: { if (isValidMxid) - addInvite(); + addInvite(text); } Component.onCompleted: forceActiveFocus() @@ -82,85 +91,198 @@ ApplicationWindow { cleanUpAndClose(); } + onTextChanged: { + searchTimer.restart() + if(isValidMxid) { + profile = TimelineManager.getGlobalUserProfile(text); + } else + profile = null; + } + Timer { + id: searchTimer + + interval: 350 + onTriggered: { + userSearch.model.setSearchString(parent.text) + } + } } - Button { - text: qsTr("Add") - enabled: inviteeEntry.isValidMxid - onClicked: addInvite() + CheckBox { + id: searchOnServer + text: qsTr("Search on Server") + checked: false + onClicked: userSearch.model.setSearchString(inviteeEntry.text) } } - - ListView { - id: inviteesList - - Layout.fillWidth: true - Layout.fillHeight: true - model: invitees - - delegate: ItemDelegate { - id: del - - hoverEnabled: true - width: ListView.view.width - height: layout.implicitHeight + Nheko.paddingSmall * 2 - onClicked: TimelineManager.openGlobalUserProfile(model.mxid) + RowLayout { + ItemDelegate { + visible: inviteeEntry.isValidMxid + id: del3 + Layout.preferredWidth: inviteDialogRoot.width/2 + Layout.alignment: Qt.AlignTop + Layout.preferredHeight: layout3.implicitHeight + Nheko.paddingSmall * 2 + onClicked: addInvite(inviteeEntry.text) background: Rectangle { - color: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color + color: del3.hovered ? Nheko.colors.dark : inviteDialogRoot.color + clip: true } - - RowLayout { - id: layout - - spacing: Nheko.paddingMedium + GridLayout { + id: layout3 anchors.centerIn: parent - width: del.width - Nheko.paddingSmall * 2 + width: del3.width - Nheko.paddingSmall * 2 + rows: 2 + columns: 2 + rowSpacing: Nheko.paddingSmall + columnSpacing: Nheko.paddingMedium Avatar { - width: Nheko.avatarSize - height: Nheko.avatarSize - userid: model.mxid - url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - displayName: model.displayName + Layout.rowSpan: 2 + Layout.preferredWidth: Nheko.avatarSize + Layout.preferredHeight: Nheko.avatarSize + Layout.alignment: Qt.AlignLeft + userid: inviteeEntry.text + url: profile? profile.avatarUrl.replace("mxc://", "image://MxcImage/") : "" + displayName: profile? profile.displayName : "" enabled: false } + Label { + Layout.fillWidth: true + text: profile? profile.displayName : "" + color: TimelineManager.userColor(inviteeEntry.text, Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + } - ColumnLayout { - spacing: Nheko.paddingSmall + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + text: inviteeEntry.text + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + } + } + ListView { + visible: !inviteeEntry.isValidMxid + id: userSearch + model: searchOnServer.checked? userDirectory : friendsCompleter + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + delegate: ItemDelegate { + id: del2 + width: ListView.view.width + height: layout2.implicitHeight + Nheko.paddingSmall * 2 + onClicked: addInvite(model.userid) + background: Rectangle { + color: del2.hovered ? Nheko.colors.dark : inviteDialogRoot.color + } + GridLayout { + id: layout2 + anchors.centerIn: parent + width: del2.width - Nheko.paddingSmall * 2 + rows: 2 + columns: 2 + rowSpacing: Nheko.paddingSmall + columnSpacing: Nheko.paddingMedium + Avatar { + Layout.rowSpan: 2 + Layout.preferredWidth: Nheko.avatarSize + Layout.preferredHeight: Nheko.avatarSize + Layout.alignment: Qt.AlignLeft + userid: model.userid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + enabled: false + } Label { + Layout.fillWidth: true text: model.displayName - color: TimelineManager.userColor(model ? model.mxid : "", del.background.color) + color: TimelineManager.userColor(model.userid, Nheko.colors.window) font.pointSize: fontMetrics.font.pointSize } Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + text: model.userid + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + } + } + } + ListView { + id: inviteesList + + Layout.fillWidth: true + Layout.fillHeight: true + model: invitees + clip: true + + delegate: ItemDelegate { + id: del + + hoverEnabled: true + width: ListView.view.width + height: layout.implicitHeight + Nheko.paddingSmall * 2 + onClicked: TimelineManager.openGlobalUserProfile(model.mxid) + background: Rectangle { + color: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color + } + GridLayout { + id: layout + anchors.centerIn: parent + width: del.width - Nheko.paddingSmall * 2 + rows: 2 + columns: 3 + rowSpacing: Nheko.paddingSmall + columnSpacing: Nheko.paddingMedium + + Avatar { + Layout.rowSpan: 2 + Layout.preferredWidth: Nheko.avatarSize + Layout.preferredHeight: Nheko.avatarSize + Layout.alignment: Qt.AlignLeft + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + enabled: false + } + Label { + Layout.fillWidth: true + text: model.displayName + color: TimelineManager.userColor(model.mxid, Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + } + + ImageButton { + Layout.rowSpan: 2 + id: removeButton + image: ":/icons/icons/ui/dismiss.svg" + onClicked: invitees.removeUser(model.mxid) + } + + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop text: model.mxid - color: del.hovered ? Nheko.colors.brightText : Nheko.colors.buttonText + color: Nheko.colors.buttonText font.pointSize: fontMetrics.font.pointSize * 0.9 } } - Item { - Layout.fillWidth: true + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor } - ImageButton { - image: ":/icons/icons/ui/dismiss.svg" - onClicked: invitees.removeUser(model.mxid) - } - - } - - CursorShape { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor } } - } } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 1d743844..ef6897b4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -37,6 +37,7 @@ #include "RoomsModel.h" #include "SingleImagePackModel.h" #include "TrayIcon.h" +#include "UserDirectoryModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" #include "Utils.h" @@ -69,6 +70,7 @@ Q_DECLARE_METATYPE(std::vector) Q_DECLARE_METATYPE(std::vector) Q_DECLARE_METATYPE(mtx::responses::PublicRoom) Q_DECLARE_METATYPE(mtx::responses::Profile) +Q_DECLARE_METATYPE(mtx::responses::User) MainWindow *MainWindow::instance_ = nullptr; @@ -147,6 +149,7 @@ MainWindow::registerQmlTypes() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); @@ -154,7 +157,9 @@ MainWindow::registerQmlTypes() qRegisterMetaType>(); qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType(); qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", 1, @@ -184,6 +189,7 @@ MainWindow::registerQmlTypes() qmlRegisterType("im.nheko", 1, 0, "MxcAnimatedImage"); qmlRegisterType("im.nheko", 1, 0, "MxcMedia"); qmlRegisterType("im.nheko", 1, 0, "RoomDirectoryModel"); + qmlRegisterType("im.nheko", 1, 0, "UserDirectoryModel"); qmlRegisterType("im.nheko", 1, 0, "Login"); qmlRegisterType("im.nheko", 1, 0, "Registration"); qmlRegisterType("im.nheko", 1, 0, "HiddenEvents"); diff --git a/src/MainWindow.h b/src/MainWindow.h index f567c93e..60dbdb14 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -57,7 +57,10 @@ public: void showChatPage(); #ifdef NHEKO_DBUS_SYS - bool dbusAvailable() const { return dbusAvailable_; } + bool dbusAvailable() const + { + return dbusAvailable_; + } #endif Q_INVOKABLE void addPerRoomWindow(const QString &room, QWindow *window); diff --git a/src/UserDirectoryModel.cpp b/src/UserDirectoryModel.cpp new file mode 100644 index 00000000..5a014704 --- /dev/null +++ b/src/UserDirectoryModel.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "UserDirectoryModel.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "mtx/responses/users.hpp" + +UserDirectoryModel::UserDirectoryModel(QObject *parent) + : QAbstractListModel{parent} +{ + connect(this, + &UserDirectoryModel::fetchedSearchResults, + this, + &UserDirectoryModel::displaySearchResults, + Qt::QueuedConnection); +} + +QHash +UserDirectoryModel::roleNames() const +{ + return { + {Roles::DisplayName, "displayName"}, + {Roles::Mxid, "userid"}, + {Roles::AvatarUrl, "avatarUrl"}, + }; +} + +void +UserDirectoryModel::setSearchString(const QString &f) +{ + userSearchString_ = f.toStdString(); + nhlog::ui()->debug("Received user directory query: {}", userSearchString_); + beginResetModel(); + results_.clear(); + endResetModel(); + searchingUsers_ = true; + emit searchingUsersChanged(); + http::client()->search_user_directory( + userSearchString_, + [this](const mtx::responses::Users &res, mtx::http::RequestErr err) { + searchingUsers_ = false; + emit searchingUsersChanged(); + + if (err) { + nhlog::net()->error("Failed to retrieve users from mtxclient - {} - {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error, + err->parse_error); + } else { + emit fetchedSearchResults(res.results); + } + }, + -1); +} + +QVariant +UserDirectoryModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)results_.size() || index.row() < 0) + return {}; + switch (role) { + case Roles::DisplayName: + return QString::fromStdString(results_[index.row()].display_name); + case Roles::Mxid: + return QString::fromStdString(results_[index.row()].user_id); + case Roles::AvatarUrl: + return QString::fromStdString(results_[index.row()].avatar_url); + } + return {}; +} + +void +UserDirectoryModel::displaySearchResults(std::vector results) +{ + results_ = results; + if (results_.empty()) { + nhlog::net()->error("mtxclient helper thread yielded empty chunk!"); + return; + } + beginInsertRows(QModelIndex(), 0, static_cast(results_.size()) - 1); + endInsertRows(); +} diff --git a/src/UserDirectoryModel.h b/src/UserDirectoryModel.h new file mode 100644 index 00000000..da1e390b --- /dev/null +++ b/src/UserDirectoryModel.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +class UserDirectoryModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool searchingUsers READ searchingUsers NOTIFY searchingUsersChanged) + +public: + explicit UserDirectoryModel(QObject *parent = nullptr); + + enum Roles + { + DisplayName, + Mxid, + AvatarUrl, + }; + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role) const override; + + inline int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return static_cast(results_.size()); + } + +private: + std::vector results_; + std::string userSearchString_; + bool searchingUsers_{false}; + +signals: + void searchingUsersChanged(); + void fetchedSearchResults(std::vector results); + +public slots: + void setSearchString(const QString &f); + bool searchingUsers() const { return searchingUsers_; } + +private slots: + void displaySearchResults(std::vector results); +}; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index 5d7dd5b7..0dc5d866 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -8,6 +8,7 @@ #include #include "Cache.h" +#include "Cache_p.h" #include "CompletionModelRoles.h" #include "UserSettingsPage.h" @@ -15,10 +16,29 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent) : QAbstractListModel(parent) , room_id(roomId) { - roomMembers_ = cache::roomMembers(roomId); - for (const auto &m : roomMembers_) { - displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); - userids.push_back(QString::fromStdString(m)); + // obviously, "friends" isn't a room, but I felt this was the least invasive way + if (roomId == "friends") { + auto e = cache::client()->getAccountData(mtx::events::EventType::Direct); + if (e) { + if (auto event = + std::get_if>( + &e.value())) { + for (const auto &[userId, roomIds] : event->content.user_to_rooms) { + displayNames.push_back( + QString::fromStdString(cache::displayName(roomIds[0], userId))); + userids.push_back(QString::fromStdString(userId)); + avatarUrls.push_back(cache::avatarUrl(QString::fromStdString(roomIds[0]), + QString::fromStdString(userId))); + } + } + } + } else { + for (const auto &m : cache::roomMembers(roomId)) { + displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); + userids.push_back(QString::fromStdString(m)); + avatarUrls.push_back( + cache::avatarUrl(QString::fromStdString(room_id), QString::fromStdString(m))); + } } } @@ -58,8 +78,7 @@ UsersModel::data(const QModelIndex &index, int role) const case CompletionModel::SearchRole2: return userids[index.row()]; case Roles::AvatarUrl: - return cache::avatarUrl(QString::fromStdString(room_id), - QString::fromStdString(roomMembers_[index.row()])); + return avatarUrls[index.row()]; case Roles::UserID: return userids[index.row()].toHtmlEscaped(); } diff --git a/src/UsersModel.h b/src/UsersModel.h index e6d21845..61d5ee0f 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -22,13 +22,13 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { (void)parent; - return (int)roomMembers_.size(); + return (int)userids.size(); } QVariant data(const QModelIndex &index, int role) const override; private: std::string room_id; - std::vector roomMembers_; + std::vector avatarUrls; std::vector displayNames; std::vector userids; };