From e4802995630f847b71832f65f3ac8f958f18f3b4 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 13 Oct 2023 12:00:47 +0000 Subject: [PATCH] Canonical Parent So the original space parent and child stuff was technically a bit naughty in that it allowed multiple rooms to be set as the canonical parent. Because while a room can have multiple parents only one should be canonical. This adds the following: - When adding a child or parent there is an extra check to select if the new parent should be canonical - Any parent can be selected as the canonical one from the room settings - All functions ensure that there is only ever one canonical parent by ensuring all others are false when a new one is set. --- src/neochatroom.cpp | 76 +++++++++++++++++++++++++++++++----- src/neochatroom.h | 26 +++++++++--- src/qml/CreateRoomDialog.qml | 11 +++++- src/qml/General.qml | 51 ++++++++++++++++++------ src/qml/SpaceHomePage.qml | 4 +- 5 files changed, 137 insertions(+), 31 deletions(-) diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index f2eac02c7..f76c4edc8 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -118,6 +118,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS connect(this, &Room::changed, this, [this] { Q_EMIT canEncryptRoomChanged(); Q_EMIT parentIdsChanged(); + Q_EMIT canonicalParentChanged(); }); connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged); connect(this, &Room::changed, this, [this]() { @@ -1117,12 +1118,41 @@ QVector NeoChatRoom::parentIds() const return parentIds; } -bool NeoChatRoom::isCanonicalParent(const QString &parentId) const +QString NeoChatRoom::canonicalParent() const { - if (auto parentEvent = currentState().get("m.space.parent"_ls, parentId)) { - return parentEvent->contentPart("canonical"_ls); + auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); + for (const auto &parentEvent : parentEvents) { + if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart("via"_ls).isEmpty()) { + if (parentEvent->contentPart("canonical"_ls)) { + return parentEvent->stateKey(); + } + } + } + return {}; +} + +void NeoChatRoom::setCanonicalParent(const QString &parentId) +{ + if (!canModifyParent(parentId)) { + return; + } + if (const auto &parent = currentState().get("m.space.parent"_ls, parentId)) { + auto content = parent->contentJson(); + content.insert("canonical"_ls, true); + setState("m.space.parent"_ls, parentId, content); + } else { + return; + } + + // Only one canonical parent can exist so make sure others are set false. + auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); + for (const auto &parentEvent : parentEvents) { + if (parentEvent->contentPart("canonical"_ls) && parentEvent->stateKey() != parentId) { + auto content = parentEvent->contentJson(); + content.insert("canonical"_ls, false); + setState("m.space.parent"_ls, parentEvent->stateKey(), content); + } } - return false; } bool NeoChatRoom::canModifyParent(const QString &parentId) const @@ -1151,13 +1181,29 @@ bool NeoChatRoom::canModifyParent(const QString &parentId) const return false; } -void NeoChatRoom::addParent(const QString &parentId) +void NeoChatRoom::addParent(const QString &parentId, bool canonical, bool setParentChild) { if (!canModifyParent(parentId)) { return; } - if (auto parent = static_cast(connection()->room(parentId))) { - setState("m.space.parent"_ls, parentId, QJsonObject{{"canonical"_ls, true}, {"via"_ls, QJsonArray{connection()->domain()}}}); + if (canonical) { + // Only one canonical parent can exist so make sure others are set false. + auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); + for (const auto &parentEvent : parentEvents) { + if (parentEvent->contentPart("canonical"_ls)) { + auto content = parentEvent->contentJson(); + content.insert("canonical"_ls, false); + setState("m.space.parent"_ls, parentEvent->stateKey(), content); + } + } + } + + setState("m.space.parent"_ls, parentId, QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}}); + + if (setParentChild) { + if (auto parent = static_cast(connection()->room(parentId))) { + parent->setState("m.space.child"_ls, id(), QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}}); + } } } @@ -1184,7 +1230,7 @@ bool NeoChatRoom::isSpace() return creationEvent->roomType() == RoomType::Space; } -void NeoChatRoom::addChild(const QString &childId, bool setChildParent) +void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical) { if (!isSpace()) { return; @@ -1197,7 +1243,19 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent) if (setChildParent) { if (auto child = static_cast(connection()->room(childId))) { if (child->canSendState("m.space.parent"_ls)) { - child->setState("m.space.parent"_ls, id(), QJsonObject{{"canonical"_ls, true}, {"via"_ls, QJsonArray{connection()->domain()}}}); + child->setState("m.space.parent"_ls, id(), QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}}); + + if (canonical) { + // Only one canonical parent can exist so make sure others are set to false. + auto parentEvents = child->currentState().eventsOfType("m.space.parent"_ls); + for (const auto &parentEvent : parentEvents) { + if (parentEvent->contentPart("canonical"_ls)) { + auto content = parentEvent->contentJson(); + content.insert("canonical"_ls, false); + setState("m.space.parent"_ls, parentEvent->stateKey(), content); + } + } + } } } } diff --git a/src/neochatroom.h b/src/neochatroom.h index 7c871d4d7..d9263485f 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -133,6 +133,21 @@ class NeoChatRoom : public Quotient::Room */ Q_PROPERTY(QVector parentIds READ parentIds NOTIFY parentIdsChanged) + /** + * @brief The current canonical parent for the room. + * + * Empty if no canonical parent is set. The write method can only be used to + * set an existing parent as canonical; If you wish to add a new parent and set + * it as canonical use the addParent method and pass true to the canonical + * parameter. + * + * Setting will fail if the user doesn't have the required privileges (see + * canModifyParent) or if the given room ID is not a parent room. + * + * @sa canModifyParent, addParent + */ + Q_PROPERTY(QString canonicalParent READ canonicalParent WRITE setCanonicalParent NOTIFY canonicalParentChanged) + /** * @brief If the room is a space. */ @@ -601,10 +616,8 @@ public: QVector parentIds() const; - /** - * @brief Whether the given parent is the canonical parent of the room. - */ - Q_INVOKABLE bool isCanonicalParent(const QString &parentId) const; + QString canonicalParent() const; + void setCanonicalParent(const QString &parentId); /** * @brief Whether the local user has permission to set the given space as a parent. @@ -622,7 +635,7 @@ public: * * @sa canModifyParent() */ - Q_INVOKABLE void addParent(const QString &parentId); + Q_INVOKABLE void addParent(const QString &parentId, bool canonical = false, bool setParentChild = false); /** * @brief Remove the given room as a parent. @@ -642,7 +655,7 @@ public: * 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, bool canonical = false); /** * @brief Remove the given room as a child. @@ -927,6 +940,7 @@ Q_SIGNALS: void backgroundChanged(); void readMarkerLoadedChanged(); void parentIdsChanged(); + void canonicalParentChanged(); void lastActiveTimeChanged(); void isInviteChanged(); void displayNameChanged(); diff --git a/src/qml/CreateRoomDialog.qml b/src/qml/CreateRoomDialog.qml index 99e90f75c..79b99d1ff 100644 --- a/src/qml/CreateRoomDialog.qml +++ b/src/qml/CreateRoomDialog.qml @@ -24,7 +24,7 @@ FormCard.FormCardPage { required property NeoChatConnection connection - signal addChild(string childId, bool setChildParent) + signal addChild(string childId, bool setChildParent, bool canonical) signal newChild(string childName) title: isSpace ? i18nc("@title", "Create a Space") : i18nc("@title", "Create a Room") @@ -214,11 +214,18 @@ FormCard.FormCardPage { return false; } } + FormCard.FormCheckDelegate { + id: makeCanonicalCheck + text: i18n("Make this space the canonical parent") + checked: enabled + + enabled: existingOfficialCheck.enabled + } FormCard.FormButtonDelegate { text: i18nc("@action:button", "Ok") enabled: chosenRoomDelegate.visible onClicked: { - root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked); + root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked, makeCanonicalCheck.checked); root.closeDialog(); } } diff --git a/src/qml/General.qml b/src/qml/General.qml index 179afbd49..d8453dc55 100644 --- a/src/qml/General.qml +++ b/src/qml/General.qml @@ -309,18 +309,38 @@ FormCard.FormCardPage { } } - 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) + contentItem.children: RowLayout { + QQC2.Label { + visible: root.room.canonicalParent === officalParentDelegate.modelData + text: i18n("Canonical") } - QQC2.ToolTip { - text: removeParentAction.text - delay: Kirigami.Units.toolTipDelay + QQC2.ToolButton { + visible: root.room.canSendState("m.space.parent") && root.room.canonicalParent !== officalParentDelegate.modelData + display: QQC2.AbstractButton.IconOnly + action: Kirigami.Action { + id: canonicalParentAction + text: i18n("Make canonical parent") + icon.name: "checkmark" + onTriggered: root.room.canonicalParent = officalParentDelegate.modelData + } + QQC2.ToolTip { + text: canonicalParentAction.text + delay: Kirigami.Units.toolTipDelay + } + } + 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 + } } } } @@ -445,11 +465,18 @@ FormCard.FormCardPage { 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.FormCheckDelegate { + id: makeCanonicalCheck + text: i18n("Make this space the canonical parent") + checked: enabled + + enabled: chosenRoomDelegate.visible + } FormCard.FormButtonDelegate { text: i18nc("@action:button", "Ok") enabled: chosenRoomDelegate.visible && root.room.canModifyParent(chosenRoomDelegate.roomId) onClicked: { - root.room.addParent(chosenRoomDelegate.roomId) + root.room.addParent(chosenRoomDelegate.roomId, makeCanonicalCheck.checked, existingOfficialCheck.checked) } } } diff --git a/src/qml/SpaceHomePage.qml b/src/qml/SpaceHomePage.qml index 62399d4a7..9dfa2b73a 100644 --- a/src/qml/SpaceHomePage.qml +++ b/src/qml/SpaceHomePage.qml @@ -157,12 +157,12 @@ Kirigami.Page { }, { title: i18nc("@title", "Create a Child") }) - dialog.addChild.connect((childId, setChildParent) => { + dialog.addChild.connect((childId, setChildParent, canonical) => { // We have to get a room object from the connection as we may not // be adding to the top level parent. let parent = root.currentRoom.connection.room(parentId) if (parent) { - parent.addChild(childId, setChildParent) + parent.addChild(childId, setChildParent, canonical) } }) dialog.newChild.connect(childName => {spaceChildrenModel.addPendingChild(childName)})