Begin adding space child management

createSpaces
Loren Burkholder 3 years ago
parent d3c6bcd43c
commit cf09a6aa2f
  1. 2
      CMakeLists.txt
  2. 17
      resources/qml/Root.qml
  3. 12
      resources/qml/dialogs/RoomSettings.qml
  4. 188
      resources/qml/dialogs/SpaceChildren.qml
  5. 1
      resources/res.qrc
  6. 39
      src/ChatPage.cpp
  7. 3
      src/ChatPage.h
  8. 13
      src/MainWindow.cpp
  9. 106
      src/SpaceChildrenModel.cpp
  10. 79
      src/SpaceChildrenModel.h
  11. 10
      src/timeline/TimelineViewManager.cpp
  12. 3
      src/timeline/TimelineViewManager.h
  13. 21
      src/ui/NhekoGlobalObject.cpp
  14. 10
      src/ui/NhekoGlobalObject.h
  15. 13
      src/ui/RoomSettings.cpp
  16. 5
      src/ui/RoomSettings.h

@ -375,6 +375,7 @@ set(SRC_FILES
src/SSOHandler.cpp
src/CombinedImagePackModel.cpp
src/SingleImagePackModel.cpp
src/SpaceChildrenModel.cpp
src/ImagePackListModel.cpp
src/TrayIcon.cpp
src/UserSettingsPage.cpp
@ -570,6 +571,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/RoomsModel.h
src/SSOHandler.h
src/SingleImagePackModel.h
src/SpaceChildrenModel.h
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h

@ -59,6 +59,14 @@ Pane {
}
Component {
id: spaceChildrenComponent
SpaceChildren {
}
}
Component {
id: roomMembersComponent
@ -265,6 +273,15 @@ Pane {
destroyOnClose(roomSettings);
}
function onOpenSpaceChildrenDialog(spaceChildren, nonChildren) {
var spaceChildrenDialog = spaceChildrenComponent.createObject(timelineRoot, {
"spaceChildren": spaceChildren,
"nonChildren": nonChildren
});
spaceChildrenDialog.show();
destroyOnClose(spaceChildrenDialog);
}
function onOpenInviteUsersDialog(invitees) {
var dialog = inviteDialog.createObject(timelineRoot, {
"roomId": Rooms.currentRoom.roomId,

@ -362,6 +362,18 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight
}
Item {
Layout.fillWidth: true
visible: roomSettings.isSpace
}
Button {
visible: roomSettings.isSpace
text: qsTr("Manage space children")
Layout.alignment: Qt.AlignRight
onClicked: TimelineManager.openSpaceChildren(roomSettings.roomId)
}
Item {
// for adding extra space between sections
Layout.fillWidth: true

@ -0,0 +1,188 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick 2.15
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
ApplicationWindow {
id: spaceChildrenDialog
property SpaceChildrenModel spaceChildren
property NonSpaceChildrenModel nonChildren
minimumWidth: 340
minimumHeight: 450
width: 450
height: 680
palette: Nheko.colors
color: Nheko.colors.window
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
title: qsTr("Space children")
Shortcut {
sequence: StandardKey.Cancel
onActivated: spaceChildrenDialog.close()
}
ScrollHelper {
flickable: flickable
anchors.fill: flickable
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
spacing: Nheko.paddingMedium
Label {
color: Nheko.colors.text
horizontalAlignment: Label.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("Children of %1").arg(spaceChildren.space.roomName)
wrapMode: Text.Wrap
font.pointSize: fontMetrics.font.pointSize * 1.5
}
ListView {
id: childrenList
Layout.fillWidth: true
Layout.fillHeight: true
model: spaceChildren
spacing: Nheko.paddingMedium
clip: true
ScrollHelper {
flickable: parent
anchors.fill: parent
}
delegate: RowLayout {
id: childDel
required property string id
required property string roomName
required property string avatarUrl
required property string alias
spacing: Nheko.paddingMedium
width: ListView.view.width
Avatar {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Nheko.paddingMedium
width: Nheko.avatarSize
height: Nheko.avatarSize
url: childDel.avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: childDel.id
displayName: childDel.roomName
}
ColumnLayout {
spacing: Nheko.paddingMedium
Label {
font.bold: true
text: childDel.roomName
}
Label {
text: childDel.alias
visible: childDel.alias
color: Nheko.inactiveColors.text
}
}
Item { Layout.fillWidth: true }
ImageButton {
image: ":/icons/icons/ui/delete.svg"
onClicked: Nheko.removeRoomFromSpace(childDel.id, spaceChildren.space.roomId)
}
}
}
Label {
color: Nheko.colors.text
horizontalAlignment: Label.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("Add rooms to %1").arg(spaceChildren.space.roomName)
wrapMode: Text.Wrap
font.pointSize: fontMetrics.font.pointSize * 1.5
}
ListView {
id: nonChildrenList
Layout.fillWidth: true
Layout.fillHeight: true
model: nonChildren
spacing: Nheko.paddingMedium
clip: true
ScrollHelper {
flickable: parent
anchors.fill: parent
}
delegate: RowLayout {
id: nonChildDel
required property string id
required property string roomName
required property string avatarUrl
required property string alias
spacing: Nheko.paddingMedium
width: ListView.view.width
Avatar {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Nheko.paddingMedium
width: Nheko.avatarSize
height: Nheko.avatarSize
url: nonChildDel.avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: nonChildDel.id
displayName: nonChildDel.roomName
}
ColumnLayout {
spacing: Nheko.paddingMedium
Label {
font.bold: true
text: nonChildDel.roomName
}
Label {
text: nonChildDel.alias
visible: nonChildDel.alias
color: Nheko.inactiveColors.text
}
}
Item { Layout.fillWidth: true }
ImageButton {
image: ":/icons/icons/ui/add-square-button.svg"
onClicked: Nheko.addRoomToSpace(nonChildDel.id, spaceChildren.space.roomId)
}
}
}
}
footer: DialogButtonBox {
standardButtons: DialogButtonBox.Ok
onAccepted: close()
}
}

@ -177,6 +177,7 @@
<file>qml/voip/PlaceCall.qml</file>
<file>qml/voip/ScreenShare.qml</file>
<file>qml/voip/VideoCall.qml</file>
<file>qml/dialogs/SpaceChildren.qml</file>
</qresource>
<qresource prefix="/media">
<file>media/ring.ogg</file>

@ -747,6 +747,45 @@ ChatPage::joinRoomVia(const std::string &room_id,
reason.toStdString());
}
void
ChatPage::addRoomToSpace(const QString &roomId, const QString &spaceId)
{
// Make sure that the user isn't trying to create an infinite loop. Granted, this is not
// perfect, since you could have a loop of the form "a => b => a => ...", but this is sufficient
// for now.
if (roomId == spaceId)
return;
mtx::events::state::space::Child child;
child.via = {roomId.splitRef(QStringLiteral(":")).last().toString().toStdString()};
http::client()->send_state_event(
roomId.toStdString(),
child,
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit showNotification(tr("Failed to add room to space: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
}
});
}
void
ChatPage::removeRoomFromSpace(const QString &roomId, const QString &spaceId)
{
auto childEvent = cache::client()->getStateEvent<mtx::events::state::space::Child>(
spaceId.toStdString(), "m.space.child");
if (childEvent.has_value())
http::client()->redact_event(
roomId.toStdString(),
childEvent->event_id,
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit showNotification(tr("Failed to remove room from space: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
}
});
}
void
ChatPage::createRoom(const mtx::requests::CreateRoom &req)
{

@ -94,6 +94,9 @@ public slots:
bool promptForConfirmation = true,
const QString &reason = "");
void addRoomToSpace(const QString &roomId, const QString &spaceId);
void removeRoomFromSpace(const QString &roomId, const QString &spaceId);
void inviteUser(QString userid, QString reason);
void kickUser(QString userid, QString reason);
void banUser(QString userid, QString reason);

@ -34,6 +34,7 @@
#include "RoomDirectoryModel.h"
#include "RoomsModel.h"
#include "SingleImagePackModel.h"
#include "SpaceChildrenModel.h"
#include "TrayIcon.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
@ -212,6 +213,18 @@ MainWindow::registerQmlTypes()
0,
"SingleImagePackModel",
QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<SpaceChildrenModel>(
"im.nheko",
1,
0,
"SpaceChildrenModel",
"SpaceChildrenModel needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<NonSpaceChildrenModel>(
"im.nheko",
1,
0,
"NonSpaceChildrenModel",
"NonSpaceChildrenModel needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<InviteesModel>(
"im.nheko",
1,

@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SpaceChildrenModel.h"
#include "Cache.h"
#include "Cache_p.h"
SpaceChildrenModel::SpaceChildrenModel(QSharedPointer<TimelineModel> space, QObject *parent)
: QAbstractListModel{parent}
, m_space{space}
{
auto joinedRooms = cache::joinedRooms();
for (const auto &child : cache::client()->getChildRoomIds(m_space->roomId().toStdString())) {
m_childIds.push_back(QString::fromStdString(child));
if (std::find(std::begin(joinedRooms), std::end(joinedRooms), child) !=
std::end(joinedRooms))
m_childInfos.push_back(cache::singleRoomInfo(child));
else // TODO: replace with code to fetch a non-joined room
m_childInfos.push_back(cache::singleRoomInfo(child));
}
}
QHash<int, QByteArray>
SpaceChildrenModel::roleNames() const
{
return {{Roles::Id, "id"},
{Roles::RoomName, "roomName"},
{Roles::AvatarUrl, "avatarUrl"},
{Roles::Alias, "alias"}};
}
QVariant
SpaceChildrenModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= (int)m_childIds.size() || index.row() < 0)
return {};
switch (role) {
case Roles::Id:
return m_childIds[index.row()];
case Roles::RoomName:
return QString::fromStdString(m_childInfos[index.row()].name);
case Roles::AvatarUrl:
return QString::fromStdString(m_childInfos[index.row()].avatar_url);
case Roles::Alias: {
auto aliases = cache::client()->getRoomAliases(m_childIds[index.row()].toStdString());
if (aliases.has_value())
return QString::fromStdString(aliases->alias);
else
return {};
}
default:
return {};
}
}
NonSpaceChildrenModel::NonSpaceChildrenModel(QSharedPointer<TimelineModel> space, QObject *parent)
: QAbstractListModel{parent}
, m_space{space}
{
auto children = cache::client()->getChildRoomIds(m_space->roomId().toStdString());
for (const auto &room : cache::joinedRooms()) {
if (room == space->roomId().toStdString() ||
std::find(std::begin(children), std::end(children), room) == std::end(children)) {
m_roomIds.push_back(QString::fromStdString(room));
m_roomInfos.push_back(cache::singleRoomInfo(room));
}
}
}
QHash<int, QByteArray>
NonSpaceChildrenModel::roleNames() const
{
return {{Roles::Id, "id"},
{Roles::RoomName, "roomName"},
{Roles::AvatarUrl, "avatarUrl"},
{Roles::Alias, "alias"}};
}
QVariant
NonSpaceChildrenModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= (int)m_roomIds.size() || index.row() < 0)
return {};
switch (role) {
case Roles::Id:
return m_roomIds[index.row()];
case Roles::RoomName:
return QString::fromStdString(m_roomInfos[index.row()].name);
case Roles::AvatarUrl:
return QString::fromStdString(m_roomInfos[index.row()].avatar_url);
case Roles::Alias: {
auto aliases = cache::client()->getRoomAliases(m_roomIds[index.row()].toStdString());
if (aliases.has_value())
return QString::fromStdString(aliases->alias);
else
return {};
}
default:
return {};
}
}

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef SPACECHILDRENMODEL_H
#define SPACECHILDRENMODEL_H
#include <QAbstractListModel>
#include <QSharedPointer>
#include "timeline/TimelineModel.h"
class SpaceChildrenModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(TimelineModel *space READ space CONSTANT)
public:
enum Roles
{
Id,
RoomName,
AvatarUrl,
Alias,
};
explicit SpaceChildrenModel(QSharedPointer<TimelineModel> space, QObject *parent = nullptr);
TimelineModel *space() const { return m_space.data(); }
int rowCount(const QModelIndex &parent) const override
{
Q_UNUSED(parent);
return m_childIds.size();
}
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
QSharedPointer<TimelineModel> m_space;
QStringList m_childIds;
QList<RoomInfo> m_childInfos;
};
class NonSpaceChildrenModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(TimelineModel *space READ space CONSTANT)
public:
enum Roles
{
Id,
RoomName,
AvatarUrl,
Alias,
};
explicit NonSpaceChildrenModel(QSharedPointer<TimelineModel> space, QObject *parent = nullptr);
TimelineModel *space() const { return m_space.data(); }
int rowCount(const QModelIndex &parent) const override
{
Q_UNUSED(parent);
return m_roomIds.size();
}
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
QSharedPointer<TimelineModel> m_space;
QStringList m_roomIds;
QList<RoomInfo> m_roomInfos;
};
#endif // SPACECHILDRENMODEL_H

@ -180,6 +180,16 @@ TimelineViewManager::openRoomSettings(QString room_id)
emit openRoomSettingsDialog(settings);
}
void
TimelineViewManager::openSpaceChildren(const QString &spaceId)
{
auto children = new SpaceChildrenModel{rooms_->getRoomById(spaceId)};
auto nonChildren = new NonSpaceChildrenModel{rooms_->getRoomById(spaceId)};
QQmlEngine::setObjectOwnership(children, QQmlEngine::JavaScriptOwnership);
QQmlEngine::setObjectOwnership(nonChildren, QQmlEngine::JavaScriptOwnership);
emit openSpaceChildrenDialog(children, nonChildren);
}
void
TimelineViewManager::openInviteUsers(QString roomId)
{

@ -17,6 +17,7 @@
#include "Cache.h"
#include "JdenticonProvider.h"
#include "Logging.h"
#include "SpaceChildrenModel.h"
#include "TimelineModel.h"
#include "Utils.h"
#include "emoji/EmojiModel.h"
@ -65,6 +66,7 @@ public:
Q_INVOKABLE void openRoomMembers(TimelineModel *room);
Q_INVOKABLE void openRoomSettings(QString room_id);
Q_INVOKABLE void openSpaceChildren(const QString &spaceId);
Q_INVOKABLE void openInviteUsers(QString roomId);
Q_INVOKABLE void openGlobalUserProfile(QString userId);
Q_INVOKABLE UserProfile *getGlobalUserProfile(QString userId);
@ -86,6 +88,7 @@ signals:
void focusInput();
void openRoomMembersDialog(MemberList *members, TimelineModel *room);
void openRoomSettingsDialog(RoomSettings *settings);
void openSpaceChildrenDialog(SpaceChildrenModel *children, NonSpaceChildrenModel *nonChildren);
void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile);
void showImagePackSettings(TimelineModel *room, ImagePackListModel *packlist);

@ -128,7 +128,12 @@ Nheko::logout() const
}
void
Nheko::createRoom(QString name, QString topic, QString aliasLocalpart, bool isEncrypted, bool isSpace, int preset)
Nheko::createRoom(QString name,
QString topic,
QString aliasLocalpart,
bool isEncrypted,
bool isSpace,
int preset)
{
mtx::requests::CreateRoom req;
@ -156,9 +161,21 @@ Nheko::createRoom(QString name, QString topic, QString aliasLocalpart, bool isEn
}
if (isSpace) {
req.creation_content = mtx::events::state::Create{};
req.creation_content = mtx::events::state::Create{};
req.creation_content->type = "m.space";
}
emit ChatPage::instance()->createRoom(req);
}
void
Nheko::addRoomToSpace(const QString &roomId, const QString &spaceId)
{
ChatPage::instance()->addRoomToSpace(roomId, spaceId);
}
void
Nheko::removeRoomFromSpace(const QString &roomId, const QString &spaceId)
{
ChatPage::instance()->removeRoomFromSpace(roomId, spaceId);
}

@ -52,8 +52,14 @@ public:
Q_INVOKABLE void setStatusMessage(QString msg) const;
Q_INVOKABLE void showUserSettingsPage() const;
Q_INVOKABLE void logout() const;
Q_INVOKABLE void
createRoom(QString name, QString topic, QString aliasLocalpart, bool isEncrypted, bool isSpace, int preset);
Q_INVOKABLE void createRoom(QString name,
QString topic,
QString aliasLocalpart,
bool isEncrypted,
bool isSpace,
int preset);
Q_INVOKABLE void addRoomToSpace(const QString &roomId, const QString &spaceId);
Q_INVOKABLE void removeRoomFromSpace(const QString &roomId, const QString &spaceId);
public slots:
void updateUserProfile();

@ -243,6 +243,19 @@ RoomSettings::canChangeAvatar() const
return false;
}
bool
RoomSettings::canSetSpaceChild() const
{
try {
return cache::hasEnoughPowerLevel(
{EventType::SpaceChild}, roomid_.toStdString(), utils::localUser().toStdString());
} catch (const lmdb::error &e) {
nhlog::db()->warn("lmdb error: {}", e.what());
}
return false;
}
bool
RoomSettings::isEncryptionEnabled() const
{

@ -37,6 +37,7 @@ class RoomSettings : public QObject
Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY roomNameChanged)
Q_PROPERTY(QString plainRoomTopic READ plainRoomTopic NOTIFY roomTopicChanged)
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged)
Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(int memberCount READ memberCount CONSTANT)
Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged)
Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged)
@ -45,6 +46,7 @@ class RoomSettings : public QObject
Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
Q_PROPERTY(bool canChangeName READ canChangeName CONSTANT)
Q_PROPERTY(bool canChangeTopic READ canChangeTopic CONSTANT)
Q_PROPERTY(bool canSetSpaceChild READ canSetSpaceChild CONSTANT)
Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
Q_PROPERTY(bool supportsKnocking READ supportsKnocking CONSTANT)
Q_PROPERTY(bool supportsRestricted READ supportsRestricted CONSTANT)
@ -59,6 +61,7 @@ public:
QString plainRoomTopic() const;
QString roomVersion() const;
QString roomAvatarUrl();
bool isSpace() const { return info_.is_space; }
int memberCount() const;
int notifications();
int accessJoinRules();
@ -71,6 +74,8 @@ public:
bool canChangeTopic() const;
//! Whether the user has enough power level to send m.room.avatar event.
bool canChangeAvatar() const;
//! Whether the user has enough power level to add children to a space.
bool canSetSpaceChild() const;
bool isEncryptionEnabled() const;
bool supportsKnocking() const;
bool supportsRestricted() const;

Loading…
Cancel
Save