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/SingleImagePackModel.h
src/TrayIcon.cpp src/TrayIcon.cpp
src/TrayIcon.h src/TrayIcon.h
src/UserDirectoryModel.cpp
src/UserDirectoryModel.h
src/UserSettingsPage.cpp src/UserSettingsPage.cpp
src/UserSettingsPage.h src/UserSettingsPage.h
src/UsersModel.cpp src/UsersModel.cpp
@ -601,7 +603,7 @@ if(USE_BUNDLED_MTXCLIENT)
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(MatrixClient) FetchContent_MakeAvailable(MatrixClient)
else() else()
find_package(MatrixClient 0.8.1 REQUIRED) find_package(MatrixClient 0.8.3 REQUIRED)
endif() endif()
if (VOIP) if (VOIP)

View file

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

View file

@ -15,17 +15,26 @@ ApplicationWindow {
property string roomId property string roomId
property string plainRoomName property string plainRoomName
property InviteesModel invitees property InviteesModel invitees
property var friendsCompleter
property var profile
minimumWidth: 500
function addInvite() { Component.onCompleted: {
if (inviteeEntry.isValidMxid) { friendsCompleter = TimelineManager.completerFor("user", "friends")
invitees.addUser(inviteeEntry.text);
inviteeEntry.clear();
} }
function addInvite(mxid) {
if (mxid.match("@.+?:.{3,}")) {
invitees.addUser(mxid);
if (mxid == inviteeEntry.text)
inviteeEntry.clear();
} else
console.log("invalid mxid: " + mxid)
} }
function cleanUpAndClose() { function cleanUpAndClose() {
if (inviteeEntry.isValidMxid) if (inviteeEntry.isValidMxid)
addInvite(); addInvite(inviteeEntry.text);
invitees.accept(); invitees.accept();
close(); close();
@ -72,7 +81,7 @@ ApplicationWindow {
Layout.fillWidth: true Layout.fillWidth: true
onAccepted: { onAccepted: {
if (isValidMxid) if (isValidMxid)
addInvite(); addInvite(text);
} }
Component.onCompleted: forceActiveFocus() Component.onCompleted: forceActiveFocus()
@ -82,22 +91,136 @@ ApplicationWindow {
cleanUpAndClose(); 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 { CheckBox {
text: qsTr("Add") id: searchOnServer
enabled: inviteeEntry.isValidMxid text: qsTr("Search on Server")
onClicked: addInvite() checked: false
onClicked: userSearch.model.setSearchString(inviteeEntry.text)
} }
} }
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: del3.hovered ? Nheko.colors.dark : inviteDialogRoot.color
clip: true
}
GridLayout {
id: layout3
anchors.centerIn: parent
width: del3.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: 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
}
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.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 { ListView {
id: inviteesList id: inviteesList
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
model: invitees model: invitees
clip: true
delegate: ItemDelegate { delegate: ItemDelegate {
id: del id: del
@ -109,49 +232,47 @@ ApplicationWindow {
background: Rectangle { background: Rectangle {
color: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color color: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color
} }
GridLayout {
RowLayout {
id: layout id: layout
spacing: Nheko.paddingMedium
anchors.centerIn: parent anchors.centerIn: parent
width: del.width - Nheko.paddingSmall * 2 width: del.width - Nheko.paddingSmall * 2
rows: 2
columns: 3
rowSpacing: Nheko.paddingSmall
columnSpacing: Nheko.paddingMedium
Avatar { Avatar {
width: Nheko.avatarSize Layout.rowSpan: 2
height: Nheko.avatarSize Layout.preferredWidth: Nheko.avatarSize
Layout.preferredHeight: Nheko.avatarSize
Layout.alignment: Qt.AlignLeft
userid: model.mxid userid: model.mxid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: model.displayName displayName: model.displayName
enabled: false enabled: false
} }
ColumnLayout {
spacing: Nheko.paddingSmall
Label { Label {
Layout.fillWidth: true
text: model.displayName text: model.displayName
color: TimelineManager.userColor(model ? model.mxid : "", del.background.color) color: TimelineManager.userColor(model.mxid, Nheko.colors.window)
font.pointSize: fontMetrics.font.pointSize font.pointSize: fontMetrics.font.pointSize
} }
Label {
text: model.mxid
color: del.hovered ? Nheko.colors.brightText : Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9
}
}
Item {
Layout.fillWidth: true
}
ImageButton { ImageButton {
Layout.rowSpan: 2
id: removeButton
image: ":/icons/icons/ui/dismiss.svg" image: ":/icons/icons/ui/dismiss.svg"
onClicked: invitees.removeUser(model.mxid) onClicked: invitees.removeUser(model.mxid)
} }
Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
text: model.mxid
color: Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9
}
} }
CursorShape { CursorShape {
@ -162,6 +283,7 @@ ApplicationWindow {
} }
} }
}
} }

View file

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

View file

@ -57,7 +57,10 @@ public:
void showChatPage(); void showChatPage();
#ifdef NHEKO_DBUS_SYS #ifdef NHEKO_DBUS_SYS
bool dbusAvailable() const { return dbusAvailable_; } bool dbusAvailable() const
{
return dbusAvailable_;
}
#endif #endif
Q_INVOKABLE void addPerRoomWindow(const QString &room, QWindow *window); 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 <QUrl>
#include "Cache.h" #include "Cache.h"
#include "Cache_p.h"
#include "CompletionModelRoles.h" #include "CompletionModelRoles.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
@ -15,10 +16,29 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, room_id(roomId) , room_id(roomId)
{ {
roomMembers_ = cache::roomMembers(roomId); // obviously, "friends" isn't a room, but I felt this was the least invasive way
for (const auto &m : roomMembers_) { 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))); displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m)));
userids.push_back(QString::fromStdString(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: case CompletionModel::SearchRole2:
return userids[index.row()]; return userids[index.row()];
case Roles::AvatarUrl: case Roles::AvatarUrl:
return cache::avatarUrl(QString::fromStdString(room_id), return avatarUrls[index.row()];
QString::fromStdString(roomMembers_[index.row()]));
case Roles::UserID: case Roles::UserID:
return userids[index.row()].toHtmlEscaped(); return userids[index.row()].toHtmlEscaped();
} }

View file

@ -22,13 +22,13 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override int rowCount(const QModelIndex &parent = QModelIndex()) const override
{ {
(void)parent; (void)parent;
return (int)roomMembers_.size(); return (int)userids.size();
} }
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
private: private:
std::string room_id; std::string room_id;
std::vector<std::string> roomMembers_; std::vector<QString> avatarUrls;
std::vector<QString> displayNames; std::vector<QString> displayNames;
std::vector<QString> userids; std::vector<QString> userids;
}; };