Add user directory.

This commit is contained in:
Black Hat
2019-12-25 21:20:23 +08:00
parent d25a9fb3a4
commit 282b1750ef
14 changed files with 626 additions and 12 deletions

View File

@@ -144,6 +144,7 @@ set(spectral_SRCS
src/trayicon.h
src/userlistmodel.h
src/publicroomlistmodel.h
src/userdirectorylistmodel.h
src/utils.h
src/accountlistmodel.cpp
src/controller.cpp
@@ -157,6 +158,7 @@ set(spectral_SRCS
src/trayicon.cpp
src/userlistmodel.cpp
src/publicroomlistmodel.cpp
src/userdirectorylistmodel.cpp
src/utils.cpp
src/main.cpp
)

View File

@@ -118,6 +118,36 @@ Dialog {
}
}
Control {
width: parent.width
contentItem: RowLayout {
MaterialIcon {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
color: MPalette.foreground
icon: "\ue7ff"
}
Label {
Layout.fillWidth: true
color: MPalette.foreground
text: "Start a Chat"
}
}
RippleEffect {
anchors.fill: parent
onPrimaryClicked: {
startChatDialog.createObject(ApplicationWindow.overlay, {"controller": spectralController, "connection": spectralController.connection}).open()
root.close()
}
}
}
Control {
width: parent.width

View File

@@ -3,26 +3,180 @@ import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral.Setting 0.1
import Spectral 0.1
Dialog {
property var controller
property var room
anchors.centerIn: parent
width: 360
width: 480
height: window.height - 100
id: root
title: "Invite User"
title: "Invite a User"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: ColumnLayout {
spacing: 0
contentItem: AutoTextField {
id: inviteUserDialogTextField
placeholderText: "User ID"
RowLayout {
Layout.fillWidth: true
AutoTextField {
property bool isUserID: text.match(/@(.+):(.+)/g)
Layout.fillWidth: true
id: identifierField
placeholderText: "Find a user..."
onAccepted: {
userDictListModel.search()
}
}
Button {
visible: identifierField.isUserID
text: "Add"
highlighted: true
onClicked: {
room.inviteToRoom(identifierField.text)
}
}
}
MenuSeparator {
Layout.fillWidth: true
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: userDictListView
clip: true
spacing: 4
model: UserDirectoryListModel {
id: userDictListModel
connection: root.room.connection
keyword: identifierField.text
}
delegate: Control {
property bool inRoom: room && room.containsUser(userID)
width: userDictListView.width
height: 48
id: delegate
padding: 8
contentItem: RowLayout {
spacing: 8
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name
color: MPalette.foreground
font.pixelSize: 13
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: userID
color: MPalette.lighter
font.pixelSize: 10
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
Control {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
visible: inRoom
contentItem: MaterialIcon {
icon: "\ue89e"
color: MPalette.lighter
font.pixelSize: 20
}
background: RippleEffect {
circular: true
}
}
Control {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
visible: !inRoom
contentItem: MaterialIcon {
icon: "\ue7fe"
color: MPalette.lighter
font.pixelSize: 20
}
background: RippleEffect {
circular: true
onClicked: {
room.inviteToRoom(userID)
}
}
}
}
}
ScrollBar.vertical: ScrollBar {}
Label {
anchors.centerIn: parent
visible: userDictListView.count < 1
text: "No users available"
color: MPalette.foreground
}
}
}
onAccepted: room.inviteToRoom(inviteUserDialogTextField.text)
onClosed: destroy()
}

View File

@@ -58,7 +58,7 @@ Dialog {
if (identifierField.isJoined) {
roomListForm.joinRoom(identifierField.room)
} else {
spectralController.joinRoom(connection, identifierField.text)
controller.joinRoom(connection, identifierField.text)
}
}
}
@@ -248,7 +248,7 @@ Dialog {
circular: true
onClicked: {
spectralController.joinRoom(connection, roomID)
controller.joinRoom(connection, roomID)
root.close()
}
}

View File

@@ -0,0 +1,182 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral.Setting 0.1
import Spectral 0.1
Dialog {
property var controller
property var connection
anchors.centerIn: parent
width: 480
height: window.height - 100
id: root
title: "Start a Chat"
contentItem: ColumnLayout {
spacing: 0
RowLayout {
Layout.fillWidth: true
AutoTextField {
property bool isUserID: text.match(/@(.+):(.+)/g)
Layout.fillWidth: true
id: identifierField
placeholderText: "Find a user..."
onAccepted: {
userDictListModel.search()
}
}
Button {
visible: identifierField.isUserID
text: "Chat"
highlighted: true
onClicked: {
controller.createDirectChat(connection, identifierField.text)
}
}
}
MenuSeparator {
Layout.fillWidth: true
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: userDictListView
clip: true
spacing: 4
model: UserDirectoryListModel {
id: userDictListModel
connection: root.connection
keyword: identifierField.text
}
delegate: Control {
width: userDictListView.width
height: 48
padding: 8
contentItem: RowLayout {
spacing: 8
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name
color: MPalette.foreground
font.pixelSize: 13
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: userID
color: MPalette.lighter
font.pixelSize: 10
textFormat: Text.PlainText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
Control {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
visible: directChats != null
contentItem: MaterialIcon {
icon: "\ue89e"
color: MPalette.lighter
font.pixelSize: 20
}
background: RippleEffect {
circular: true
onClicked: {
roomListForm.joinRoom(connection.room(directChats[0]))
root.close()
}
}
}
Control {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
contentItem: MaterialIcon {
icon: "\ue7f0"
color: MPalette.lighter
font.pixelSize: 20
}
background: RippleEffect {
circular: true
onClicked: {
controller.createDirectChat(connection, userID)
root.close()
}
}
}
}
}
ScrollBar.vertical: ScrollBar {}
Label {
anchors.centerIn: parent
visible: userDictListView.count < 1
text: "No users available"
color: MPalette.foreground
}
}
}
onClosed: destroy()
}

View File

@@ -12,3 +12,4 @@ AccountDetailDialog 2.0 AccountDetailDialog.qml
OpenFileDialog 2.0 OpenFileDialog.qml
OpenFolderDialog 2.0 OpenFolderDialog.qml
ImageClipboardDialog 2.0 ImageClipboardDialog.qml
StartChatDialog 2.0 StartChatDialog.qml

View File

@@ -195,7 +195,7 @@ Drawer {
color: MPalette.lighter
}
onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"controller": spectralController, "room": room}).open()
}
}
@@ -249,6 +249,12 @@ Drawer {
}
}
onRoomChanged: {
if (room == null) {
close()
}
}
Component {
id: roomSettingDialog

View File

@@ -135,6 +135,12 @@ ApplicationWindow {
JoinRoomDialog {}
}
Component {
id: startChatDialog
StartChatDialog {}
}
Component {
id: createRoomDialog

View File

@@ -58,5 +58,6 @@
<file>imports/Spectral/Component/AutoRectangle.qml</file>
<file>imports/Spectral/Component/Timeline/ReactionDelegate.qml</file>
<file>imports/Spectral/Component/Timeline/AudioDelegate.qml</file>
<file>imports/Spectral/Dialog/StartChatDialog.qml</file>
</qresource>
</RCC>

View File

@@ -20,6 +20,7 @@
#include "spectralroom.h"
#include "spectraluser.h"
#include "trayicon.h"
#include "userdirectorylistmodel.h"
#include "userlistmodel.h"
using namespace Quotient;
@@ -42,6 +43,8 @@ int main(int argc, char* argv[]) {
qmlRegisterType<UserListModel>("Spectral", 0, 1, "UserListModel");
qmlRegisterType<MessageEventModel>("Spectral", 0, 1, "MessageEventModel");
qmlRegisterType<PublicRoomListModel>("Spectral", 0, 1, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("Spectral", 0, 1,
"UserDirectoryListModel");
qmlRegisterType<EmojiModel>("Spectral", 0, 1, "EmojiModel");
qmlRegisterType<NotificationsManager>("Spectral", 0, 1,
"NotificationsManager");

View File

@@ -573,3 +573,12 @@ void SpectralRoom::toggleReaction(const QString& eventId,
postReaction(eventId, reaction);
}
}
bool SpectralRoom::containsUser(QString userID) const {
auto u = Room::user(userID);
if (!u)
return false;
return Room::memberJoinState(u) != JoinState::Leave;
}

View File

@@ -73,6 +73,8 @@ class SpectralRoom : public Room {
Qt::TextFormat format = Qt::PlainText,
bool removeReply = true) const;
Q_INVOKABLE bool containsUser(QString userID) const;
private:
QString m_cachedInput;
QSet<const Quotient::RoomEvent*> highlights;

View File

@@ -0,0 +1,156 @@
#include "userdirectorylistmodel.h"
UserDirectoryListModel::UserDirectoryListModel(QObject* parent)
: QAbstractListModel(parent) {}
void UserDirectoryListModel::setConnection(Connection* conn) {
if (m_connection == conn)
return;
beginResetModel();
m_limited = false;
attempted = false;
users.clear();
if (m_connection) {
m_connection->disconnect(this);
}
endResetModel();
m_connection = conn;
if (job) {
job->abandon();
job = nullptr;
}
emit connectionChanged();
emit limitedChanged();
}
void UserDirectoryListModel::setKeyword(const QString& value) {
if (m_keyword == value)
return;
m_keyword = value;
m_limited = false;
attempted = false;
if (job) {
job->abandon();
job = nullptr;
}
emit keywordChanged();
emit limitedChanged();
}
void UserDirectoryListModel::search(int count) {
if (count < 1)
return;
if (job) {
qDebug() << "UserDirectoryListModel: Other jobs running, ignore";
return;
}
if (attempted)
return;
job = m_connection->callApi<SearchUserDirectoryJob>(m_keyword, count);
connect(job, &BaseJob::finished, this, [=] {
attempted = true;
if (job->status() == BaseJob::Success) {
auto users = job->results();
this->beginResetModel();
this->users = users;
this->m_limited = job->limited();
this->endResetModel();
}
this->job = nullptr;
emit limitedChanged();
});
}
QVariant UserDirectoryListModel::data(const QModelIndex& index,
int role) const {
if (!index.isValid())
return QVariant();
if (index.row() >= users.count()) {
qDebug() << "UserDirectoryListModel, something's wrong: index.row() >= "
"users.count()";
return {};
}
auto user = users.at(index.row());
if (role == NameRole) {
auto displayName = user.displayName;
if (!displayName.isEmpty()) {
return displayName;
}
displayName = user.userId;
if (!displayName.isEmpty()) {
return displayName;
}
return "Unknown User";
}
if (role == AvatarRole) {
auto avatarUrl = user.avatarUrl;
if (avatarUrl.isEmpty()) {
return "";
}
return avatarUrl.remove(0, 6);
}
if (role == UserIDRole) {
return user.userId;
}
if (role == DirectChatsRole) {
if (!m_connection)
return {};
auto userObj = m_connection->user(user.userId);
auto directChats = m_connection->directChats();
if (userObj && directChats.contains(userObj)) {
auto directChatsForUser = directChats.values(userObj);
if (!directChatsForUser.isEmpty()) {
return QVariant::fromValue(directChatsForUser);
}
}
}
return {};
}
QHash<int, QByteArray> UserDirectoryListModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[AvatarRole] = "avatar";
roles[UserIDRole] = "userID";
roles[DirectChatsRole] = "directChats";
return roles;
}
int UserDirectoryListModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid())
return 0;
return users.count();
}

View File

@@ -0,0 +1,62 @@
#ifndef USERDIRECTORYLISTMODEL_H
#define USERDIRECTORYLISTMODEL_H
#include <QAbstractListModel>
#include <QObject>
#include "connection.h"
#include "csapi/users.h"
using namespace Quotient;
class UserDirectoryListModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(Connection* connection READ connection WRITE setConnection NOTIFY
connectionChanged)
Q_PROPERTY(
QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
Q_PROPERTY(bool limited READ limited NOTIFY limitedChanged)
public:
enum EventRoles {
NameRole = Qt::DisplayRole + 1,
AvatarRole,
UserIDRole,
DirectChatsRole,
};
UserDirectoryListModel(QObject* parent = nullptr);
QVariant data(const QModelIndex& index, int role = NameRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
Connection* connection() const { return m_connection; }
void setConnection(Connection* value);
QString keyword() const { return m_keyword; }
void setKeyword(const QString& value);
bool limited() const { return m_limited; }
Q_INVOKABLE void search(int count = 50);
private:
Connection* m_connection = nullptr;
QString m_keyword;
bool m_limited = false;
bool attempted = false;
QVector<SearchUserDirectoryJob::User> users;
SearchUserDirectoryJob* job = nullptr;
signals:
void connectionChanged();
void keywordChanged();
void limitedChanged();
};
#endif // USERDIRECTORYLISTMODEL_H