diff --git a/matrique.pro b/matrique.pro index d1ed1dd..58f8420 100644 --- a/matrique.pro +++ b/matrique.pro @@ -26,7 +26,8 @@ SOURCES += src/main.cpp \ src/messageeventmodel.cpp \ src/imageproviderconnection.cpp \ src/emojimodel.cpp \ - src/matriqueroom.cpp + src/matriqueroom.cpp \ + src/userlistmodel.cpp RESOURCES += \ res.qrc @@ -86,4 +87,5 @@ HEADERS += \ src/messageeventmodel.h \ src/imageproviderconnection.h \ src/emojimodel.h \ - src/matriqueroom.h + src/matriqueroom.h \ + src/userlistmodel.h diff --git a/qml/Login.qml b/qml/Login.qml index 19f6342..811955b 100644 --- a/qml/Login.qml +++ b/qml/Login.qml @@ -6,7 +6,7 @@ import QtQuick.Controls.Material 2.2 import Qt.labs.settings 1.0 import Matrique.Settings 0.1 -import "qrc:/qml/component" +import "component" Page { property var controller diff --git a/qml/Room.qml b/qml/Room.qml index 9272548..5e72175 100644 --- a/qml/Room.qml +++ b/qml/Room.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts 1.3 import Matrique 0.1 import Matrique.Settings 0.1 -import "qrc:/qml/form" +import "form" Page { property alias connection: roomListModel.connection diff --git a/qml/component/ImageStatus.qml b/qml/component/ImageStatus.qml index 7fbfada..137bb06 100644 --- a/qml/component/ImageStatus.qml +++ b/qml/component/ImageStatus.qml @@ -56,7 +56,9 @@ Item { function getInitials(text) { if (!text) return "N" - return text.toUpperCase().replace(/[^a-zA-Z- ]/g, "").match(/\b\w/g); + var initial = text.toUpperCase().replace(/[^a-zA-Z- ]/g, "").match(/\b\w/g); + if (!initial) return "N" + return initial } function stringToColor(str) { diff --git a/qml/form/RoomForm.qml b/qml/form/RoomForm.qml index c482b3d..6e93353 100644 --- a/qml/form/RoomForm.qml +++ b/qml/form/RoomForm.qml @@ -7,7 +7,7 @@ import QtGraphicalEffects 1.0 import Matrique 0.1 import Matrique.Settings 0.1 -import "qrc:/qml/component" +import "../component" import "qrc:/js/md.js" as Markdown Item { @@ -15,6 +15,12 @@ Item { id: item + UserListModel { + id: userListModel + + room: currentRoom + } + Drawer { id: roomDrawer @@ -30,34 +36,34 @@ Item { onClicked: roomDrawer.close() } - Column { + ColumnLayout { anchors.fill: parent anchors.margins: 32 spacing: 16 ImageStatus { - width: 64 - height: 64 - anchors.horizontalCenter: parent.horizontalCenter + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + Layout.alignment: Qt.AlignHCenter source: currentRoom && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : null displayText: currentRoom ? currentRoom.displayName : "" } Label { - width: parent.width + Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: currentRoom && currentRoom.id ? currentRoom.id : "" } Label { - width: parent.width + Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: currentRoom && currentRoom.canonicalAlias ? currentRoom.canonicalAlias : "No Canonical Alias" } RowLayout { - width: parent.width + Layout.fillWidth: true TextField { id: roomNameField @@ -67,7 +73,7 @@ Item { ItemDelegate { Layout.preferredWidth: height - Layout.fillHeight: true + Layout.preferredHeight: parent.height contentItem: MaterialIcon { icon: "\ue5ca" } @@ -76,7 +82,7 @@ Item { } RowLayout { - width: parent.width + Layout.fillWidth: true TextField { id: roomTopicField @@ -87,13 +93,51 @@ Item { ItemDelegate { Layout.preferredWidth: height - Layout.fillHeight: true + Layout.preferredHeight: parent.height contentItem: MaterialIcon { icon: "\ue5ca" } onClicked: currentRoom.setTopic(roomTopicField.text) } } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + + boundsBehavior: Flickable.DragOverBounds + + delegate: ItemDelegate { + width: parent.width + height: 48 + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 16 + + ImageStatus { + Layout.preferredWidth: height + Layout.fillHeight: true + + source: avatar != "" ? "image://mxc/" + avatar : "" + displayText: name + } + + Label { + Layout.fillWidth: true + + text: name + } + } + } + + model: userListModel + + ScrollBar.vertical: ScrollBar {} + } } } @@ -119,49 +163,49 @@ Item { color: MSettings.darkTheme ? "#242424" : "#eaeaea" - MouseArea { + ItemDelegate { anchors.fill: parent onClicked: roomDrawer.open() - } - RowLayout { - anchors.fill: parent - anchors.margins: 16 + RowLayout { + anchors.fill: parent + anchors.margins: 16 - spacing: 16 + spacing: 16 - ImageStatus { - Layout.preferredWidth: height - Layout.fillHeight: true - source: currentRoom && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : null - displayText: currentRoom ? currentRoom.displayName : "" - } - - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - - visible: parent.width > 80 - - Label { - Layout.fillWidth: true + ImageStatus { + Layout.preferredWidth: height Layout.fillHeight: true - - text: currentRoom ? currentRoom.displayName : "" - font.pointSize: 16 - elide: Text.ElideRight - wrapMode: Text.NoWrap + source: currentRoom && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : null + displayText: currentRoom ? currentRoom.displayName : "" } - Label { + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter - text: currentRoom ? currentRoom.topic : "" - elide: Text.ElideRight - wrapMode: Text.NoWrap + visible: parent.width > 80 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: currentRoom ? currentRoom.displayName : "" + font.pointSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: currentRoom ? currentRoom.topic : "" + elide: Text.ElideRight + wrapMode: Text.NoWrap + } } } } diff --git a/qml/form/RoomListForm.qml b/qml/form/RoomListForm.qml index 668fa89..10738bd 100644 --- a/qml/form/RoomListForm.qml +++ b/qml/form/RoomListForm.qml @@ -8,7 +8,7 @@ import Matrique 0.1 import SortFilterProxyModel 0.2 import Matrique.Settings 0.1 -import "qrc:/qml/component" +import "../component" Item { property alias listModel: roomListProxyModel.sourceModel @@ -115,11 +115,13 @@ Item { ScrollBar.vertical: ScrollBar { id: scrollBar } delegate: Rectangle { + readonly property bool highlighted: currentRoom === enteredRoom + id: swipeDelegate width: parent.width height: 80 - color: currentRoom === enteredRoom ? Material.background : "transparent" + color: highlighted ? Material.background : "transparent" AutoMouseArea { anchors.fill: parent @@ -137,7 +139,7 @@ Item { width: 4 height: parent.height color: Qt.tint(Material.accent, "#20FFFFFF") - visible: unreadCount > 0 + visible: unreadCount > 0 || highlighted } RowLayout { diff --git a/src/main.cpp b/src/main.cpp index a900ac3..fccb6b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "messageeventmodel.h" #include "room.h" #include "roomlistmodel.h" +#include "userlistmodel.h" #include "csapi/joining.h" #include "csapi/leaving.h" @@ -36,6 +37,7 @@ int main(int argc, char *argv[]) { qmlRegisterType("Matrique", 0, 1, "Controller"); qmlRegisterType("Matrique", 0, 1, "RoomListModel"); + qmlRegisterType("Matrique", 0, 1, "UserListModel"); qmlRegisterType("Matrique", 0, 1, "MessageEventModel"); qmlRegisterType("Matrique", 0, 1, "EmojiModel"); qmlRegisterUncreatableType("Matrique", 0, 1, diff --git a/src/userlistmodel.cpp b/src/userlistmodel.cpp new file mode 100644 index 0000000..7157196 --- /dev/null +++ b/src/userlistmodel.cpp @@ -0,0 +1,129 @@ +#include "userlistmodel.h" + +#include +#include +#include + +#include +#include +#include + +UserListModel::UserListModel(QObject* parent) + : QAbstractListModel(parent), m_currentRoom(nullptr) {} + +void UserListModel::setRoom(QMatrixClient::Room* room) { + if (m_currentRoom == room) return; + + using namespace QMatrixClient; + beginResetModel(); + if (m_currentRoom) { + m_currentRoom->connection()->disconnect(this); + m_currentRoom->disconnect(this); + for (User* user : m_users) user->disconnect(this); + m_users.clear(); + } + m_currentRoom = room; + if (m_currentRoom) { + connect(m_currentRoom, &Room::userAdded, this, &UserListModel::userAdded); + connect(m_currentRoom, &Room::userRemoved, this, + &UserListModel::userRemoved); + connect(m_currentRoom, &Room::memberAboutToRename, this, + &UserListModel::userRemoved); + connect(m_currentRoom, &Room::memberRenamed, this, + &UserListModel::userAdded); + { + QElapsedTimer et; + et.start(); + m_users = m_currentRoom->users(); + std::sort(m_users.begin(), m_users.end(), room->memberSorter()); + qDebug() << "Sorting" << m_users.size() << "user(s) in" + << m_currentRoom->displayName() << "took" << et; + } + for (User* user : m_users) { + connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged); + } + connect(m_currentRoom->connection(), &Connection::loggedOut, this, + [=] { setRoom(nullptr); }); + qDebug() << m_users.count() << "user(s) in the room"; + } + endResetModel(); + emit roomChanged(); +} + +QMatrixClient::User* UserListModel::userAt(QModelIndex index) { + if (index.row() < 0 || index.row() >= m_users.size()) return nullptr; + return m_users.at(index.row()); +} + +QVariant UserListModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) return QVariant(); + + if (index.row() >= m_users.count()) { + qDebug() + << "UserListModel, something's wrong: index.row() >= m_users.count()"; + return QVariant(); + } + auto user = m_users.at(index.row()); + if (role == NameRole) { + return user->displayname(m_currentRoom); + } + if (role == AvatarRole) { + return user->avatarUrl(m_currentRoom); + } + + return QVariant(); +} + +int UserListModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) return 0; + + return m_users.count(); +} + +void UserListModel::userAdded(QMatrixClient::User* user) { + auto pos = findUserPos(user); + beginInsertRows(QModelIndex(), pos, pos); + m_users.insert(pos, user); + endInsertRows(); + connect(user, &QMatrixClient::User::avatarChanged, this, + &UserListModel::avatarChanged); +} + +void UserListModel::userRemoved(QMatrixClient::User* user) { + auto pos = findUserPos(user); + if (pos != m_users.size()) { + beginRemoveRows(QModelIndex(), pos, pos); + m_users.removeAt(pos); + endRemoveRows(); + user->disconnect(this); + } else + qWarning() << "Trying to remove a room member not in the user list"; +} + +void UserListModel::refresh(QMatrixClient::User* user, QVector roles) { + auto pos = findUserPos(user); + if (pos != m_users.size()) + emit dataChanged(index(pos), index(pos), roles); + else + qWarning() << "Trying to access a room member not in the user list"; +} + +void UserListModel::avatarChanged(QMatrixClient::User* user, + const QMatrixClient::Room* context) { + if (context == m_currentRoom) refresh(user, {Qt::DecorationRole}); +} + +int UserListModel::findUserPos(User* user) const { + return findUserPos(m_currentRoom->roomMembername(user)); +} + +int UserListModel::findUserPos(const QString& username) const { + return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username); +} + +QHash UserListModel::roleNames() const { + QHash roles; + roles[NameRole] = "name"; + roles[AvatarRole] = "avatar"; + return roles; +} diff --git a/src/userlistmodel.h b/src/userlistmodel.h new file mode 100644 index 0000000..d09adb7 --- /dev/null +++ b/src/userlistmodel.h @@ -0,0 +1,56 @@ +#ifndef USERLISTMODEL_H +#define USERLISTMODEL_H + +#include "room.h" + +#include +#include + +namespace QMatrixClient { +class Connection; +class Room; +class User; +} // namespace QMatrixClient + +class UserListModel : public QAbstractListModel { + Q_OBJECT + Q_PROPERTY( + QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged) + public: + enum EventRoles { + NameRole = Qt::UserRole + 1, + AvatarRole + }; + + using User = QMatrixClient::User; + + UserListModel(QObject* parent = nullptr); + + QMatrixClient::Room* room() { return m_currentRoom; } + void setRoom(QMatrixClient::Room* room); + User* userAt(QModelIndex index); + + QVariant data(const QModelIndex& index, + int role = NameRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + QHash roleNames() const override; + + signals: + void roomChanged(); + + private slots: + void userAdded(User* user); + void userRemoved(User* user); + void refresh(User* user, QVector roles = {}); + void avatarChanged(User* user, const QMatrixClient::Room* context); + + private: + QMatrixClient::Room* m_currentRoom; + QList m_users; + + int findUserPos(User* user) const; + int findUserPos(const QString& username) const; +}; + +#endif // USERLISTMODEL_H