Room Settings Parents
Add the ability to manage parent rooms from a child, this includes: - viewing parents - adding a new parent - removing an existing one Follows the rules from the matrix spec https://spec.matrix.org/v1.7/client-server-api/#mspaceparent-relationships
This commit is contained in:
@@ -117,6 +117,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
|||||||
});
|
});
|
||||||
connect(this, &Room::changed, this, [this] {
|
connect(this, &Room::changed, this, [this] {
|
||||||
Q_EMIT canEncryptRoomChanged();
|
Q_EMIT canEncryptRoomChanged();
|
||||||
|
Q_EMIT parentIdsChanged();
|
||||||
});
|
});
|
||||||
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
|
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
|
||||||
connect(this, &Room::changed, this, [this]() {
|
connect(this, &Room::changed, this, [this]() {
|
||||||
@@ -1099,6 +1100,80 @@ void NeoChatRoom::clearInvitationNotification()
|
|||||||
NotificationsManager::instance().clearInvitationNotification(id());
|
NotificationsManager::instance().clearInvitationNotification(id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NeoChatRoom::hasParent() const
|
||||||
|
{
|
||||||
|
return currentState().eventsOfType("m.space.parent"_ls).size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QString> NeoChatRoom::parentIds() const
|
||||||
|
{
|
||||||
|
auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
|
||||||
|
QVector<QString> parentIds;
|
||||||
|
for (const auto &parentEvent : parentEvents) {
|
||||||
|
if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart<QJsonArray>("via"_ls).isEmpty()) {
|
||||||
|
parentIds += parentEvent->stateKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NeoChatRoom::isCanonicalParent(const QString &parentId) const
|
||||||
|
{
|
||||||
|
if (auto parentEvent = currentState().get("m.space.parent"_ls, parentId)) {
|
||||||
|
return parentEvent->contentPart<bool>("canonical"_ls);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NeoChatRoom::canModifyParent(const QString &parentId) const
|
||||||
|
{
|
||||||
|
if (!canSendState("m.space.parent"_ls)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If we can't peek the parent we assume that we neither have permission nor is
|
||||||
|
// there an existing space child event for this room.
|
||||||
|
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
|
||||||
|
if (!parent->isSpace()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If the user is allowed to set space child events in the parent they are
|
||||||
|
// allowed to set the space as a parent (even if a space child event doesn't
|
||||||
|
// exist).
|
||||||
|
if (parent->canSendState("m.space.child"_ls)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If the parent has a space child event the user can set as a parent (even
|
||||||
|
// if they don't have permission to set space child events in that parent).
|
||||||
|
if (parent->currentState().contains("m.space.child"_ls, id())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::addParent(const QString &parentId)
|
||||||
|
{
|
||||||
|
if (!canModifyParent(parentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
|
||||||
|
setState("m.space.parent"_ls, parentId, QJsonObject{{"canonical"_ls, true}, {"via"_ls, QJsonArray{connection()->domain()}}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::removeParent(const QString &parentId)
|
||||||
|
{
|
||||||
|
if (!canModifyParent(parentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentState().contains("m.space.parent"_ls, parentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
|
||||||
|
setState("m.space.parent"_ls, parentId, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool NeoChatRoom::isSpace()
|
bool NeoChatRoom::isSpace()
|
||||||
{
|
{
|
||||||
const auto creationEvent = this->creation();
|
const auto creationEvent = this->creation();
|
||||||
|
|||||||
@@ -126,6 +126,13 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(Quotient::User *directChatRemoteUser READ directChatRemoteUser CONSTANT)
|
Q_PROPERTY(Quotient::User *directChatRemoteUser READ directChatRemoteUser CONSTANT)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The Matrix IDs of this room's parents.
|
||||||
|
*
|
||||||
|
* Empty if no parent space is set.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QVector<QString> parentIds READ parentIds NOTIFY parentIdsChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief If the room is a space.
|
* @brief If the room is a space.
|
||||||
*/
|
*/
|
||||||
@@ -587,10 +594,62 @@ public:
|
|||||||
|
|
||||||
Quotient::User *directChatRemoteUser() const;
|
Quotient::User *directChatRemoteUser() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this room has one or more parent spaces set.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE bool hasParent() const;
|
||||||
|
|
||||||
|
QVector<QString> parentIds() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the given parent is the canonical parent of the room.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE bool isCanonicalParent(const QString &parentId) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the local user has permission to set the given space as a parent.
|
||||||
|
*
|
||||||
|
* @note This follows the rules determined in the Matrix spec
|
||||||
|
* https://spec.matrix.org/v1.7/client-server-api/#mspaceparent-relationships
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE bool canModifyParent(const QString &parentId) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add the given room as a parent.
|
||||||
|
*
|
||||||
|
* Will fail if the user doesn't have the required privileges (see
|
||||||
|
* canModifyParent()).
|
||||||
|
*
|
||||||
|
* @sa canModifyParent()
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void addParent(const QString &parentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove the given room as a parent.
|
||||||
|
*
|
||||||
|
* Will fail if the user doesn't have the required privileges (see
|
||||||
|
* canModifyParent()).
|
||||||
|
*
|
||||||
|
* @sa canModifyParent()
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void removeParent(const QString &parentId);
|
||||||
|
|
||||||
[[nodiscard]] bool isSpace();
|
[[nodiscard]] bool isSpace();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add the given room as a child.
|
||||||
|
*
|
||||||
|
* Will fail if the user doesn't have the required privileges or this room is
|
||||||
|
* not a space.
|
||||||
|
*/
|
||||||
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false);
|
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove the given room as a child.
|
||||||
|
*
|
||||||
|
* Will fail if the user doesn't have the required privileges or this room is
|
||||||
|
* not a space.
|
||||||
|
*/
|
||||||
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
|
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
|
||||||
|
|
||||||
bool isInvite() const;
|
bool isInvite() const;
|
||||||
@@ -867,6 +926,7 @@ Q_SIGNALS:
|
|||||||
void fileUploadingProgressChanged();
|
void fileUploadingProgressChanged();
|
||||||
void backgroundChanged();
|
void backgroundChanged();
|
||||||
void readMarkerLoadedChanged();
|
void readMarkerLoadedChanged();
|
||||||
|
void parentIdsChanged();
|
||||||
void lastActiveTimeChanged();
|
void lastActiveTimeChanged();
|
||||||
void isInviteChanged();
|
void isInviteChanged();
|
||||||
void displayNameChanged();
|
void displayNameChanged();
|
||||||
|
|||||||
@@ -278,6 +278,181 @@ FormCard.FormCardPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18n("Official Parent Spaces")
|
||||||
|
}
|
||||||
|
FormCard.FormCard {
|
||||||
|
Repeater {
|
||||||
|
id: officalParentRepeater
|
||||||
|
model: root.room.parentIds
|
||||||
|
|
||||||
|
delegate: FormCard.FormTextDelegate {
|
||||||
|
id: officalParentDelegate
|
||||||
|
required property string modelData
|
||||||
|
property NeoChatRoom space: root.connection.room(modelData)
|
||||||
|
text: {
|
||||||
|
if (space) {
|
||||||
|
return space.displayName;
|
||||||
|
} else {
|
||||||
|
return modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description: {
|
||||||
|
if (space) {
|
||||||
|
if (space.canonicalAlias.length > 0) {
|
||||||
|
return space.canonicalAlias;
|
||||||
|
} else {
|
||||||
|
return modelData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem.children: QQC2.ToolButton {
|
||||||
|
visible: officalParentDelegate?.space.canSendState("m.space.child") && root.room.canSendState("m.space.parent")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
action: Kirigami.Action {
|
||||||
|
id: removeParentAction
|
||||||
|
text: i18n("Remove parent")
|
||||||
|
icon.name: "edit-delete-remove"
|
||||||
|
onTriggered: root.room.removeParent(officalParentDelegate.modelData)
|
||||||
|
}
|
||||||
|
QQC2.ToolTip {
|
||||||
|
text: removeParentAction.text
|
||||||
|
delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
visible: officalParentRepeater.count <= 0
|
||||||
|
text: i18n("This room has no official parent spaces.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormCard.FormHeader {
|
||||||
|
visible: root.room.canSendState("m.space.parent")
|
||||||
|
title: i18n("Add Offical Parent Space")
|
||||||
|
}
|
||||||
|
FormCard.FormCard {
|
||||||
|
visible: root.room.canSendState("m.space.parent")
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
visible: !chosenRoomDelegate.visible
|
||||||
|
text: i18nc("@action:button", "Pick room")
|
||||||
|
onClicked: {
|
||||||
|
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
|
||||||
|
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||||
|
chosenRoomDelegate.roomId = roomId;
|
||||||
|
chosenRoomDelegate.displayName = displayName;
|
||||||
|
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||||
|
chosenRoomDelegate.alias = alias;
|
||||||
|
chosenRoomDelegate.topic = topic;
|
||||||
|
chosenRoomDelegate.memberCount = memberCount;
|
||||||
|
chosenRoomDelegate.isJoined = isJoined;
|
||||||
|
chosenRoomDelegate.visible = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormCard.AbstractFormDelegate {
|
||||||
|
id: chosenRoomDelegate
|
||||||
|
property string roomId
|
||||||
|
property string displayName
|
||||||
|
property url avatarUrl
|
||||||
|
property string alias
|
||||||
|
property string topic
|
||||||
|
property int memberCount
|
||||||
|
property bool isJoined
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
KirigamiComponents.Avatar {
|
||||||
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||||
|
|
||||||
|
source: chosenRoomDelegate.avatarUrl
|
||||||
|
name: chosenRoomDelegate.displayName
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Kirigami.Heading {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
level: 4
|
||||||
|
text: chosenRoomDelegate.displayName
|
||||||
|
font.bold: true
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
visible: chosenRoomDelegate.isJoined
|
||||||
|
text: i18n("Joined")
|
||||||
|
color: Kirigami.Theme.linkColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: text
|
||||||
|
text: chosenRoomDelegate.topic ? chosenRoomDelegate.topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: "user"
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: chosenRoomDelegate.memberCount + " " + (chosenRoomDelegate.alias ?? chosenRoomDelegate.roomId)
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
|
||||||
|
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||||
|
chosenRoomDelegate.roomId = roomId;
|
||||||
|
chosenRoomDelegate.displayName = displayName;
|
||||||
|
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||||
|
chosenRoomDelegate.alias = alias;
|
||||||
|
chosenRoomDelegate.topic = topic;
|
||||||
|
chosenRoomDelegate.memberCount = memberCount;
|
||||||
|
chosenRoomDelegate.isJoined = isJoined;
|
||||||
|
chosenRoomDelegate.visible = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormCard.FormCheckDelegate {
|
||||||
|
id: existingOfficialCheck
|
||||||
|
property NeoChatRoom space: root.connection.room(chosenRoomDelegate.roomId)
|
||||||
|
text: i18n("Set this room as a child of the space %1", space?.displayName ?? "")
|
||||||
|
checked: enabled
|
||||||
|
|
||||||
|
enabled: chosenRoomDelegate.visible && space && space.canSendState("m.space.child")
|
||||||
|
}
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
visible: chosenRoomDelegate.visible && !root.room.canModifyParent(chosenRoomDelegate.roomId)
|
||||||
|
text: existingOfficialCheck.space ? (existingOfficialCheck.space.isSpace ? i18n("You do not have a high enough privilege level in the parent to set this state") : i18n("The selected room is not a space")) : i18n("You do not have the privileges to complete this action")
|
||||||
|
textItem.color: Kirigami.Theme.negativeTextColor
|
||||||
|
}
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18nc("@action:button", "Ok")
|
||||||
|
enabled: chosenRoomDelegate.visible && root.room.canModifyParent(chosenRoomDelegate.roomId)
|
||||||
|
onClicked: {
|
||||||
|
root.room.addParent(chosenRoomDelegate.roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.InlineMessage {
|
Kirigami.InlineMessage {
|
||||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||||
|
|||||||
Reference in New Issue
Block a user