Restricted Room Security

Create the required ux to allow the restricted room security setting to be re-enabled

BUG: 471307
This commit is contained in:
James Graham
2023-11-07 20:43:49 +00:00
parent 08b84c6592
commit dbbad2cf13
5 changed files with 198 additions and 13 deletions

View File

@@ -289,6 +289,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png

View File

@@ -123,6 +123,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
Q_EMIT canEncryptRoomChanged();
Q_EMIT parentIdsChanged();
Q_EMIT canonicalParentChanged();
Q_EMIT joinRuleChanged();
});
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
connect(this, &Room::changed, this, [this]() {
@@ -712,16 +713,51 @@ QString NeoChatRoom::joinRule() const
return joinRulesEvent->joinRule();
}
void NeoChatRoom::setJoinRule(const QString &joinRule)
void NeoChatRoom::setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces)
{
if (!canSendState("m.room.join_rules"_ls)) {
qWarning() << "Power level too low to set join rules";
return;
}
setState("m.room.join_rules"_ls, {}, QJsonObject{{"join_rule"_ls, joinRule}});
auto actualRule = joinRule;
if (joinRule == "restricted"_ls && allowedSpaces.isEmpty()) {
actualRule = "private"_ls;
}
QJsonArray allowConditions;
if (actualRule == "restricted"_ls) {
for (auto allowedSpace : allowedSpaces) {
allowConditions += QJsonObject{{"type"_ls, "m.room_membership"_ls}, {"room_id"_ls, allowedSpace}};
}
}
QJsonObject content;
content.insert("join_rule"_ls, joinRule);
if (!allowConditions.isEmpty()) {
content.insert("allow"_ls, allowConditions);
}
qWarning() << content;
setState("m.room.join_rules"_ls, {}, content);
// Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
QList<QString> NeoChatRoom::restrictedIds() const
{
auto joinRulesEvent = currentState().get<JoinRulesEvent>();
if (!joinRulesEvent) {
return {};
}
if (joinRulesEvent->joinRule() != "restricted"_ls) {
return {};
}
QList<QString> roomIds;
for (auto allow : joinRulesEvent->allow()) {
roomIds += allow.toObject().value("room_id"_ls).toString();
}
return roomIds;
}
QString NeoChatRoom::historyVisibility() const
{
return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString();
@@ -1141,6 +1177,21 @@ QList<QString> NeoChatRoom::parentIds() const
return parentIds;
}
QList<NeoChatRoom *> NeoChatRoom::parentObjects(bool multiLevel) const
{
QList<NeoChatRoom *> parentObjects;
QList<QString> parentIds = this->parentIds();
for (const auto &parentId : parentIds) {
if (auto parentObject = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
parentObjects += parentObject;
if (multiLevel) {
parentObjects += parentObject->parentObjects(true);
}
}
}
return parentObjects;
}
QString NeoChatRoom::canonicalParent() const
{
auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);

View File

@@ -160,6 +160,13 @@ class NeoChatRoom : public Quotient::Room
*/
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
/**
* @brief The space IDs that members of can join this room.
*
* Empty if the join rule is not restricted.
*/
Q_PROPERTY(QList<QString> restrictedIds READ restrictedIds NOTIFY joinRuleChanged)
/**
* @brief Get the maximum room version that the server supports.
*
@@ -505,6 +512,17 @@ public:
QList<QString> parentIds() const;
/**
* @brief Get a list of parent space objects for this room.
*
* Will only return retrun spaces that are know, i.e. the user has joined and
* a valid NeoChatRoom is available.
*
* @param multiLevel whether the function should recursively gather all levels
* of parents
*/
Q_INVOKABLE QList<NeoChatRoom *> parentObjects(bool multiLevel = false) const;
QString canonicalParent() const;
void setCanonicalParent(const QString &parentId);
@@ -559,7 +577,23 @@ public:
Q_INVOKABLE void clearInvitationNotification();
[[nodiscard]] QString joinRule() const;
void setJoinRule(const QString &joinRule);
/**
* @brief Set the join rule for the room.
*
* Will fail if the user doesn't have the required privileges.
*
* @param joinRule the join rule [public, knock, invite, private, restricted].
* @param allowedSpaces only used when the join rule is restricted. This is a
* list of space Matrix IDs that members of can join without an invite.
* If the rule is restricted and this list is empty it is treated as a join
* rule of private instead.
*
* @sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules
*/
Q_INVOKABLE void setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces = {});
QList<QString> restrictedIds() const;
int maxRoomVersion() const;

View File

@@ -3,7 +3,10 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
@@ -42,18 +45,37 @@ FormCard.FormCardPage {
description: i18n("Only invited people can join.")
checked: room.joinRule === "invite"
enabled: room.canSendState("m.room.join_rules")
onCheckedChanged: if (checked) {
room.joinRule = "invite";
onCheckedChanged: if (checked && room.joinRule != "invite") {
root.room.joinRule = "invite";
}
}
FormCard.FormRadioDelegate {
text: i18nc("@option:check", "Space members")
description: i18n("Anyone in a space can find and join.") +
description: i18n("Anyone in the selected spaces can find and join.") +
(!["8", "9", "10"].includes(room.version) ? `\n${needUpgradeRoom}` : "")
checked: room.joinRule === "restricted"
enabled: room.canSendState("m.room.join_rules") && ["8", "9", "10"].includes(room.version) && false
onCheckedChanged: if (checked) {
room.joinRule = "restricted";
enabled: room.canSendState("m.room.join_rules") && ["8", "9", "10"].includes(room.version)
onCheckedChanged: if (checked && room.joinRule != "restricted") {
selectSpacesDialog.createObject(applicationWindow().overlay).open();
}
contentItem.children: QQC2.Button {
visible: root.room.joinRule === "restricted"
text: i18n("Select spaces")
icon.name: "list-add"
onClicked: selectSpacesDialog.createObject(applicationWindow().overlay).open();
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
Component {
id: selectSpacesDialog
SelectSpacesDialog {
room: root.room
}
}
}
FormCard.FormRadioDelegate {
@@ -63,8 +85,8 @@ FormCard.FormCardPage {
checked: room.joinRule === "knock"
// https://spec.matrix.org/v1.4/rooms/#feature-matrix
enabled: room.canSendState("m.room.join_rules") && ["7", "8", "9", "10"].includes(room.version)
onCheckedChanged: if (checked) {
room.joinRule = "knock";
onCheckedChanged: if (checked && room.joinRule != "knock") {
root.room.joinRule = "knock";
}
}
FormCard.FormRadioDelegate {
@@ -72,8 +94,8 @@ FormCard.FormCardPage {
description: i18nc("@option:check", "Anyone can find and join.")
checked: room.joinRule === "public"
enabled: room.canSendState("m.room.join_rules")
onCheckedChanged: if (checked) {
room.joinRule = "public";
onCheckedChanged: if (checked && root.room.joinRule != "public") {
root.room.joinRule = "public";
}
}
}

View File

@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat
Kirigami.Dialog {
id: root
/**
* @brief The current room this dialog is opened for.
*/
required property NeoChatRoom room
/**
* @brief The current list of space IDs that members of can join this room.
*/
property list<string> restrictedIds: room.restrictedIds
parent: applicationWindow().overlay
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title", "Select Spaces")
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
let ids = [];
for (var i in spaceGroup.buttons) {
if (spaceGroup.buttons[i].checked) {
ids.push(spaceGroup.buttons[i].modelData.id);
}
}
root.room.setJoinRule("restricted", ids)
console.warn(ids)
}
QQC2.ButtonGroup {
id: spaceGroup
exclusive: false
}
contentItem: ColumnLayout {
spacing: 0
Repeater {
model: root.room.parentObjects(true)
delegate: FormCard.FormCheckDelegate {
required property var modelData
text: modelData.displayName
description: modelData.canonicalAlias
checked: root.restrictedIds.includes(modelData.id)
QQC2.ButtonGroup.group: spaceGroup
leading: Components.Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
source: modelData.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(modelData.avatarUrl) : ""
name: modelData.displayName
}
}
}
}
}