diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3722787cf..df4dc0e84 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
)
diff --git a/imports/Spectral/Dialog/AccountDetailDialog.qml b/imports/Spectral/Dialog/AccountDetailDialog.qml
index 2846ec6fd..75b598b8a 100644
--- a/imports/Spectral/Dialog/AccountDetailDialog.qml
+++ b/imports/Spectral/Dialog/AccountDetailDialog.qml
@@ -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
diff --git a/imports/Spectral/Dialog/InviteUserDialog.qml b/imports/Spectral/Dialog/InviteUserDialog.qml
index f56858ad0..b14e4da2d 100644
--- a/imports/Spectral/Dialog/InviteUserDialog.qml
+++ b/imports/Spectral/Dialog/InviteUserDialog.qml
@@ -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()
}
diff --git a/imports/Spectral/Dialog/JoinRoomDialog.qml b/imports/Spectral/Dialog/JoinRoomDialog.qml
index 76d7a1d00..c8df13dbd 100644
--- a/imports/Spectral/Dialog/JoinRoomDialog.qml
+++ b/imports/Spectral/Dialog/JoinRoomDialog.qml
@@ -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()
}
}
diff --git a/imports/Spectral/Dialog/StartChatDialog.qml b/imports/Spectral/Dialog/StartChatDialog.qml
new file mode 100644
index 000000000..273f7cd12
--- /dev/null
+++ b/imports/Spectral/Dialog/StartChatDialog.qml
@@ -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()
+}
diff --git a/imports/Spectral/Dialog/qmldir b/imports/Spectral/Dialog/qmldir
index 51a130a89..70ca5b9e6 100644
--- a/imports/Spectral/Dialog/qmldir
+++ b/imports/Spectral/Dialog/qmldir
@@ -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
diff --git a/imports/Spectral/Panel/RoomDrawer.qml b/imports/Spectral/Panel/RoomDrawer.qml
index 5e9e1a930..48561703e 100644
--- a/imports/Spectral/Panel/RoomDrawer.qml
+++ b/imports/Spectral/Panel/RoomDrawer.qml
@@ -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
diff --git a/qml/main.qml b/qml/main.qml
index 74e30214a..11a55e4e0 100644
--- a/qml/main.qml
+++ b/qml/main.qml
@@ -135,6 +135,12 @@ ApplicationWindow {
JoinRoomDialog {}
}
+ Component {
+ id: startChatDialog
+
+ StartChatDialog {}
+ }
+
Component {
id: createRoomDialog
diff --git a/res.qrc b/res.qrc
index b4c8ae782..9bb99b5b2 100644
--- a/res.qrc
+++ b/res.qrc
@@ -58,5 +58,6 @@
imports/Spectral/Component/AutoRectangle.qml
imports/Spectral/Component/Timeline/ReactionDelegate.qml
imports/Spectral/Component/Timeline/AudioDelegate.qml
+ imports/Spectral/Dialog/StartChatDialog.qml
diff --git a/src/main.cpp b/src/main.cpp
index f7676fdcc..babe5d8d0 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -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("Spectral", 0, 1, "UserListModel");
qmlRegisterType("Spectral", 0, 1, "MessageEventModel");
qmlRegisterType("Spectral", 0, 1, "PublicRoomListModel");
+ qmlRegisterType("Spectral", 0, 1,
+ "UserDirectoryListModel");
qmlRegisterType("Spectral", 0, 1, "EmojiModel");
qmlRegisterType("Spectral", 0, 1,
"NotificationsManager");
diff --git a/src/spectralroom.cpp b/src/spectralroom.cpp
index 110752bcb..40ed9b92c 100644
--- a/src/spectralroom.cpp
+++ b/src/spectralroom.cpp
@@ -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;
+}
diff --git a/src/spectralroom.h b/src/spectralroom.h
index a051091e9..8fd0bf8cb 100644
--- a/src/spectralroom.h
+++ b/src/spectralroom.h
@@ -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 highlights;
diff --git a/src/userdirectorylistmodel.cpp b/src/userdirectorylistmodel.cpp
new file mode 100644
index 000000000..b109f2bb1
--- /dev/null
+++ b/src/userdirectorylistmodel.cpp
@@ -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(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 UserDirectoryListModel::roleNames() const {
+ QHash 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();
+}
diff --git a/src/userdirectorylistmodel.h b/src/userdirectorylistmodel.h
new file mode 100644
index 000000000..be76b4902
--- /dev/null
+++ b/src/userdirectorylistmodel.h
@@ -0,0 +1,62 @@
+#ifndef USERDIRECTORYLISTMODEL_H
+#define USERDIRECTORYLISTMODEL_H
+
+#include
+#include
+
+#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 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 users;
+
+ SearchUserDirectoryJob* job = nullptr;
+
+ signals:
+ void connectionChanged();
+ void keywordChanged();
+ void limitedChanged();
+};
+
+#endif // USERDIRECTORYLISTMODEL_H