add user search to invite dialog

pull/1253/head
Malte E 2 years ago
parent 6529240be8
commit b599f5c0c6
  1. 8
      CMakeLists.txt
  2. 4
      resources/qml/Root.qml
  3. 232
      resources/qml/dialogs/InviteDialog.qml
  4. 6
      src/MainWindow.cpp
  5. 5
      src/MainWindow.h
  6. 87
      src/UserDirectoryModel.cpp
  7. 55
      src/UserDirectoryModel.h
  8. 31
      src/UsersModel.cpp
  9. 4
      src/UsersModel.h

@ -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
@ -594,14 +596,14 @@ if(USE_BUNDLED_MTXCLIENT)
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG d187c63a27710fa87a44ab44d43b7cfa2023132a GIT_TAG d187c63a27710fa87a44ab44d43b7cfa2023132a
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
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)

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

@ -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,85 +91,198 @@ 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 {
ListView { ItemDelegate {
id: inviteesList visible: inviteeEntry.isValidMxid
id: del3
Layout.fillWidth: true Layout.preferredWidth: inviteDialogRoot.width/2
Layout.fillHeight: true Layout.alignment: Qt.AlignTop
model: invitees Layout.preferredHeight: layout3.implicitHeight + Nheko.paddingSmall * 2
onClicked: addInvite(inviteeEntry.text)
delegate: ItemDelegate {
id: del
hoverEnabled: true
width: ListView.view.width
height: layout.implicitHeight + Nheko.paddingSmall * 2
onClicked: TimelineManager.openGlobalUserProfile(model.mxid)
background: Rectangle { background: Rectangle {
color: del.hovered ? Nheko.colors.dark : inviteDialogRoot.color color: del3.hovered ? Nheko.colors.dark : inviteDialogRoot.color
clip: true
} }
GridLayout {
RowLayout { id: layout3
id: layout
spacing: Nheko.paddingMedium
anchors.centerIn: parent 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 { Avatar {
width: Nheko.avatarSize Layout.rowSpan: 2
height: Nheko.avatarSize Layout.preferredWidth: Nheko.avatarSize
userid: model.mxid Layout.preferredHeight: Nheko.avatarSize
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") Layout.alignment: Qt.AlignLeft
displayName: model.displayName userid: inviteeEntry.text
url: profile? profile.avatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: profile? profile.displayName : ""
enabled: false enabled: false
} }
Label {
Layout.fillWidth: true
text: profile? profile.displayName : ""
color: TimelineManager.userColor(inviteeEntry.text, Nheko.colors.window)
font.pointSize: fontMetrics.font.pointSize
}
ColumnLayout { Label {
spacing: Nheko.paddingSmall 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 { Label {
Layout.fillWidth: true
text: model.displayName 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 font.pointSize: fontMetrics.font.pointSize
} }
Label { Label {
text: model.mxid Layout.fillWidth: true
color: del.hovered ? Nheko.colors.brightText : Nheko.colors.buttonText Layout.alignment: Qt.AlignTop
text: model.userid
color: Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9 font.pointSize: fontMetrics.font.pointSize * 0.9
} }
} }
}
}
ListView {
id: inviteesList
Item { Layout.fillWidth: true
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: Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9
}
ImageButton {
image: ":/icons/icons/ui/dismiss.svg"
onClicked: invitees.removeUser(model.mxid)
} }
} CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
} }
} }
} }
} }

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

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

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

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

@ -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") {
displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); auto e = cache::client()->getAccountData(mtx::events::EventType::Direct);
userids.push_back(QString::fromStdString(m)); 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: 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();
} }

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

Loading…
Cancel
Save