add user search to invite dialog

This commit is contained in:
Malte E 2022-12-13 13:28:57 +08:00
parent 6529240be8
commit b599f5c0c6
9 changed files with 365 additions and 67 deletions

View file

@ -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)

View file

@ -33,6 +33,10 @@ Pane {
id: publicRooms
}
UserDirectoryModel {
id: userDirectory
}
//Timer {
// onTriggered: gc()
// interval: 1000

View file

@ -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
}
}
}
}

View file

@ -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<DeviceInfo>)
Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
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<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
qRegisterMetaType<mtx::responses::PublicRoom>();
qRegisterMetaType<mtx::responses::User>();
qRegisterMetaType<mtx::responses::Profile>();
qRegisterMetaType<CombinedImagePackModel *>();
qRegisterMetaType<RoomSettingsAllowedRoomsModel *>();
@ -154,7 +157,9 @@ MainWindow::registerQmlTypes()
qRegisterMetaType<std::vector<DeviceInfo>>();
qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
qRegisterMetaType<std::vector<mtx::responses::User>>();
qRegisterMetaType<mtx::responses::User>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
1,
@ -184,6 +189,7 @@ MainWindow::registerQmlTypes()
qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
qmlRegisterType<UserDirectoryModel>("im.nheko", 1, 0, "UserDirectoryModel");
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");

View file

@ -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);

View file

@ -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<int, QByteArray>
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<mtx::responses::User> results)
{
results_ = results;
if (results_.empty()) {
nhlog::net()->error("mtxclient helper thread yielded empty chunk!");
return;
}
beginInsertRows(QModelIndex(), 0, static_cast<int>(results_.size()) - 1);
endInsertRows();
}

55
src/UserDirectoryModel.h Normal file
View file

@ -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 <QAbstractListModel>
#include <QString>
#include <string>
#include <vector>
#include <mtx/responses/users.hpp>
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<int, QByteArray> 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<int>(results_.size());
}
private:
std::vector<mtx::responses::User> results_;
std::string userSearchString_;
bool searchingUsers_{false};
signals:
void searchingUsersChanged();
void fetchedSearchResults(std::vector<mtx::responses::User> results);
public slots:
void setSearchString(const QString &f);
bool searchingUsers() const { return searchingUsers_; }
private slots:
void displaySearchResults(std::vector<mtx::responses::User> results);
};

View file

@ -8,6 +8,7 @@
#include <QUrl>
#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<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(
&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();
}

View file

@ -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<std::string> roomMembers_;
std::vector<QString> avatarUrls;
std::vector<QString> displayNames;
std::vector<QString> userids;
};