Create a space module
This commit is contained in:
23
src/spaces/CMakeLists.txt
Normal file
23
src/spaces/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(Spaces STATIC)
|
||||
ecm_add_qml_module(Spaces GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.spaces
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/spaces
|
||||
QML_FILES
|
||||
SpaceHomePage.qml
|
||||
SpaceHierarchyDelegate.qml
|
||||
SOURCES
|
||||
models/spacechildrenmodel.cpp
|
||||
models/spacechildsortfiltermodel.cpp
|
||||
models/spacetreeitem.cpp
|
||||
)
|
||||
|
||||
target_include_directories(Spaces PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_link_libraries(Spaces PRIVATE
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
KF6::Kirigami
|
||||
LibNeoChat
|
||||
)
|
||||
219
src/spaces/SpaceHierarchyDelegate.qml
Normal file
219
src/spaces/SpaceHierarchyDelegate.qml
Normal file
@@ -0,0 +1,219 @@
|
||||
// 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.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property TreeView treeView
|
||||
required property bool isTreeNode
|
||||
required property bool expanded
|
||||
required property int hasChildren
|
||||
required property int depth
|
||||
required property int row
|
||||
required property string roomId
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
required property bool isSpace
|
||||
required property bool isSuggested
|
||||
required property int memberCount
|
||||
required property string topic
|
||||
required property bool isJoined
|
||||
required property bool canAddChildren
|
||||
required property string parentDisplayName
|
||||
required property bool canSetParent
|
||||
required property bool isDeclaredParent
|
||||
required property bool canRemove
|
||||
required property NeoChatRoom parentRoom
|
||||
|
||||
signal createRoom
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: mainDelegate
|
||||
property int row: root.row
|
||||
|
||||
anchors.horizontalCenter: root.horizontalCenter
|
||||
anchors.verticalCenter: root.verticalCenter
|
||||
width: sizeHelper.availableWidth
|
||||
|
||||
highlighted: dropArea.containsDrag
|
||||
|
||||
contentItem: RowLayout {
|
||||
z: 1
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Item {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium * (root.depth + (root.isSpace ? 0 : 1))
|
||||
}
|
||||
Kirigami.Icon {
|
||||
visible: root.isSpace
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
source: root.hasChildren ? (root.expanded ? "go-up" : "go-down") : "go-next"
|
||||
}
|
||||
}
|
||||
Components.Avatar {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
implicitWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
source: root.avatarUrl
|
||||
name: root.displayName
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
QQC2.Label {
|
||||
id: label
|
||||
text: root.displayName
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: root.isJoined || root.isSuggested
|
||||
text: root.isJoined ? i18n("Joined") : i18n("Suggested")
|
||||
color: root.isJoined ? Kirigami.Theme.linkColor : Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: subtitle
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
|
||||
text: root.topic.length > 0 ? i18ncp("number of room members", "%1 member - ", "%1 members - ", root.memberCount) + root.topic : i18ncp("number of room members", "%1 member", "%1 members", root.memberCount)
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: Text.PlainText
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: root.isSpace && root.canAddChildren
|
||||
text: i18nc("@button", "Add new room")
|
||||
icon.name: "list-add"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.createRoom()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: root.canRemove
|
||||
text: i18nc("@button", "Remove")
|
||||
icon.name: "list-remove"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
removeChildDialog.createObject(QQC2.Overlay.overlay, {
|
||||
parentRoom: root.parentRoom,
|
||||
roomId: root.roomId,
|
||||
displayName: root.displayName,
|
||||
parentDisplayName: root.parentDisplayName,
|
||||
canSetParent: root.canSetParent,
|
||||
isDeclaredParent: root.isDeclaredParent
|
||||
}).open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: root.parentRoom?.canSendState("m.space.child") ?? false
|
||||
text: root.isSuggested ? i18nc("@button", "Don't Make Suggested") : i18nc("@button", "Make Suggested")
|
||||
icon.name: root.isSuggested ? "edit-delete-remove" : "checkmark"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.parentRoom.toggleChildSuggested(root.roomId)
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
|
||||
drag.target: mainDelegate
|
||||
drag.axis: Drag.YAxis
|
||||
|
||||
drag.onActiveChanged: {
|
||||
if (!dragArea.drag.active) {
|
||||
mainDelegate.Drag.drop();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.isSpace) {
|
||||
root.treeView.toggleExpanded(root.row);
|
||||
} else {
|
||||
RoomManager.resolveResource(root.roomId, root.isJoined ? "" : "join");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: mainDelegate.Drag.active && root.parentRoom.canSendState("m.space.child")
|
||||
ParentChange {
|
||||
target: mainDelegate
|
||||
parent: root.treeView
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
target: mainDelegate
|
||||
anchors.horizontalCenter: undefined
|
||||
anchors.verticalCenter: undefined
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Drag.active: dragArea.drag.active
|
||||
Drag.hotSpot.x: mainDelegate.width / 2
|
||||
Drag.hotSpot.y: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
anchors.fill: parent
|
||||
onDropped: (drag) => {
|
||||
root.treeView.model.move(root.treeView.index(drag.source.row, 0), root.treeView.index(root.row, 0))
|
||||
}
|
||||
}
|
||||
|
||||
LibNeoChat.DelegateSizeHelper {
|
||||
id: sizeHelper
|
||||
parentItem: root
|
||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||
startPercentWidth: 100
|
||||
endPercentWidth: 85
|
||||
maxWidth: Kirigami.Units.gridUnit * 60
|
||||
}
|
||||
|
||||
Component {
|
||||
id: removeChildDialog
|
||||
RemoveChildDialog {}
|
||||
}
|
||||
}
|
||||
186
src/spaces/SpaceHomePage.qml
Normal file
186
src/spaces/SpaceHomePage.qml
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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.neochat
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
readonly property NeoChatRoom currentRoom: RoomManager.currentRoom
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: 0
|
||||
|
||||
QQC2.Control {
|
||||
id: headerItem
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: headerColumn.implicitHeight
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: headerColumn
|
||||
anchors.centerIn: headerItem
|
||||
width: sizeHelper.currentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
GroupChatDrawerHeader {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
room: root.currentRoom
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
QQC2.Button {
|
||||
visible: root.currentRoom.canSendState("invite")
|
||||
text: i18nc("@button", "Invite user to space")
|
||||
icon.name: "list-add-user"
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'InviteUserPage'), {
|
||||
room: root.currentRoom
|
||||
}, {
|
||||
title: i18nc("@title", "Invite a User")
|
||||
})
|
||||
}
|
||||
QQC2.Button {
|
||||
visible: root.currentRoom.canSendState("m.space.child")
|
||||
text: i18nc("@button", "Add new room")
|
||||
icon.name: "list-add"
|
||||
onClicked: _private.createRoom(root.currentRoom.id)
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Leave this space")
|
||||
icon.name: "go-previous"
|
||||
onClicked: RoomManager.leaveRoom(root.currentRoom)
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Button {
|
||||
id: settingsButton
|
||||
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("'Space' is a matrix space", "Space Settings")
|
||||
onClicked: {
|
||||
RoomSettingsView.openRoomSettings(root.currentRoom, RoomSettingsView.Space);
|
||||
drawer.close();
|
||||
}
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
Kirigami.SearchField {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
onTextChanged: spaceChildSortFilterModel.filterText = text
|
||||
}
|
||||
}
|
||||
LibNeoChat.DelegateSizeHelper {
|
||||
id: sizeHelper
|
||||
parentItem: root
|
||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||
startPercentWidth: 100
|
||||
endPercentWidth: 85
|
||||
maxWidth: Kirigami.Units.gridUnit * 60
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.ScrollView {
|
||||
id: hierarchyScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: !spaceChildrenModel.loading
|
||||
|
||||
TreeView {
|
||||
id: spaceTree
|
||||
columnWidthProvider: function (column) {
|
||||
return spaceTree.width;
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
model: SpaceChildSortFilterModel {
|
||||
id: spaceChildSortFilterModel
|
||||
sourceModel: SpaceChildrenModel {
|
||||
id: spaceChildrenModel
|
||||
space: root.currentRoom
|
||||
}
|
||||
}
|
||||
|
||||
delegate: SpaceHierarchyDelegate {
|
||||
onCreateRoom: _private.createRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
}
|
||||
}
|
||||
QQC2.Control {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: spaceChildrenModel.loading
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: spaceChildrenModel.loading
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: Kirigami.LoadingPlaceholder {}
|
||||
}
|
||||
}
|
||||
QtObject {
|
||||
id: _private
|
||||
function createRoom(parentId) {
|
||||
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
title: i18nc("@title", "Create a Child"),
|
||||
connection: root.currentRoom.connection,
|
||||
parentId: parentId,
|
||||
showChildType: true,
|
||||
showCreateChoice: true
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Child")
|
||||
});
|
||||
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, canonical);
|
||||
}
|
||||
});
|
||||
dialog.newChild.connect(childName => {
|
||||
spaceChildrenModel.addPendingChild(childName);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
374
src/spaces/models/spacechildrenmodel.cpp
Normal file
374
src/spaces/models/spacechildrenmodel.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
// 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
|
||||
|
||||
#include "spacechildrenmodel.h"
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
, m_rootItem(new SpaceTreeItem(nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
SpaceChildrenModel::~SpaceChildrenModel()
|
||||
{
|
||||
delete m_rootItem;
|
||||
}
|
||||
|
||||
NeoChatRoom *SpaceChildrenModel::space() const
|
||||
{
|
||||
return m_space;
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::setSpace(NeoChatRoom *space)
|
||||
{
|
||||
if (space == m_space) {
|
||||
return;
|
||||
}
|
||||
// disconnect the new room signal from the old connection in case it is different.
|
||||
if (m_space != nullptr) {
|
||||
m_space->connection()->disconnect(this);
|
||||
m_space->disconnect(this);
|
||||
}
|
||||
|
||||
m_space = space;
|
||||
Q_EMIT spaceChanged();
|
||||
|
||||
refreshModel();
|
||||
|
||||
if (!m_space) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto connection = m_space->connection();
|
||||
connect(connection, &NeoChatConnection::loadedRoomState, this, [this](Quotient::Room *room) {
|
||||
if (m_pendingChildren.contains(room->name())) {
|
||||
m_pendingChildren.removeAll(room->name());
|
||||
refreshModel();
|
||||
}
|
||||
});
|
||||
connect(m_space, &Quotient::Room::changed, this, [this]() {
|
||||
refreshModel();
|
||||
});
|
||||
}
|
||||
|
||||
bool SpaceChildrenModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::refreshModel()
|
||||
{
|
||||
for (auto job : m_currentJobs) {
|
||||
if (job) {
|
||||
job->abandon();
|
||||
}
|
||||
}
|
||||
m_currentJobs.clear();
|
||||
|
||||
if (m_space == nullptr) {
|
||||
beginResetModel();
|
||||
delete m_rootItem;
|
||||
m_rootItem = nullptr;
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_replacedRooms.clear();
|
||||
delete m_rootItem;
|
||||
m_loading = true;
|
||||
Q_EMIT loadingChanged();
|
||||
m_rootItem =
|
||||
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()), nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
|
||||
endResetModel();
|
||||
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), std::nullopt, std::nullopt, 1);
|
||||
m_currentJobs.append(job);
|
||||
connect(job, &Quotient::BaseJob::success, this, [this, job]() {
|
||||
insertChildren(job->rooms());
|
||||
});
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::SpaceHierarchyRoomsChunk> children, const QModelIndex &parent)
|
||||
{
|
||||
SpaceTreeItem *parentItem = getItem(parent);
|
||||
|
||||
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
|
||||
parentItem->setChildStates(std::move(children[0].childrenState));
|
||||
children.erase(children.begin());
|
||||
}
|
||||
|
||||
// If this is the first set of children added to the root item then we need to
|
||||
// set it so that we are no longer loading.
|
||||
if (rowCount(QModelIndex()) == 0 && !children.empty()) {
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
beginInsertRows(parent, parentItem->childCount(), parentItem->childCount() + children.size() - 1);
|
||||
for (unsigned long i = 0; i < children.size(); ++i) {
|
||||
if (children[i].roomId == m_space->id() || children[i].roomId == parentItem->id()) {
|
||||
continue;
|
||||
} else {
|
||||
int insertRow = parentItem->childCount();
|
||||
if (const auto room = m_space->connection()->room(children[i].roomId)) {
|
||||
const auto predecessorId = room->predecessorId();
|
||||
if (!predecessorId.isEmpty()) {
|
||||
m_replacedRooms += predecessorId;
|
||||
}
|
||||
const auto successorId = room->successorId();
|
||||
if (!successorId.isEmpty()) {
|
||||
m_replacedRooms += successorId;
|
||||
}
|
||||
if (dynamic_cast<NeoChatRoom *>(room)->isSpace()) {
|
||||
connect(room, &Quotient::Room::changed, this, [this]() {
|
||||
refreshModel();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (children[i].childrenState.size() > 0) {
|
||||
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, std::nullopt, std::nullopt, 1);
|
||||
m_currentJobs.append(job);
|
||||
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
|
||||
insertChildren(job->rooms(), index(insertRow, 0, parent));
|
||||
});
|
||||
}
|
||||
parentItem->insertChild(std::make_unique<SpaceTreeItem>(dynamic_cast<NeoChatConnection *>(m_space->connection()),
|
||||
parentItem,
|
||||
children[i].roomId,
|
||||
children[i].name,
|
||||
children[i].canonicalAlias,
|
||||
children[i].topic,
|
||||
children[i].numJoinedMembers,
|
||||
children[i].avatarUrl,
|
||||
children[i].guestCanJoin,
|
||||
children[i].worldReadable,
|
||||
children[i].roomType == u"m.space"_s,
|
||||
std::move(children[i].childrenState)));
|
||||
}
|
||||
}
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceChildrenModel::getItem(const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid()) {
|
||||
SpaceTreeItem *item = static_cast<SpaceTreeItem *>(index.internalPointer());
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return m_rootItem;
|
||||
}
|
||||
|
||||
QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
SpaceTreeItem *child = getItem(index);
|
||||
if (role == DisplayNameRole) {
|
||||
auto displayName = child->name();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = child->canonicalAlias();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return child->id();
|
||||
}
|
||||
if (role == AvatarUrlRole) {
|
||||
return child->avatarUrl();
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return child->topic();
|
||||
}
|
||||
if (role == RoomIDRole) {
|
||||
return child->id();
|
||||
}
|
||||
if (role == AliasRole) {
|
||||
return child->canonicalAlias();
|
||||
}
|
||||
if (role == MemberCountRole) {
|
||||
return child->memberCount();
|
||||
}
|
||||
if (role == AllowGuestsRole) {
|
||||
return child->allowGuests();
|
||||
}
|
||||
if (role == WorldReadableRole) {
|
||||
return child->worldReadable();
|
||||
}
|
||||
if (role == IsJoinedRole) {
|
||||
return child->isJoined();
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return child->isSpace();
|
||||
}
|
||||
if (role == IsSuggestedRole) {
|
||||
return child->isSuggested();
|
||||
}
|
||||
if (role == CanAddChildrenRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->canSendState(u"m.space.child"_s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == ParentDisplayNameRole) {
|
||||
const auto parent = child->parentItem();
|
||||
auto displayName = parent->name();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = parent->canonicalAlias();
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return parent->id();
|
||||
}
|
||||
if (role == CanSetParentRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->canSendState(u"m.space.parent"_s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == IsDeclaredParentRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->currentState().contains(u"m.space.parent"_s, child->parentItem()->id());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == CanRemove) {
|
||||
const auto parent = child->parentItem();
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(parent->id()))) {
|
||||
return room->canSendState(u"m.space.child"_s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == ParentRoomRole) {
|
||||
if (const auto parentRoom = static_cast<NeoChatRoom *>(m_space->connection()->room(child->parentItem()->id()))) {
|
||||
return QVariant::fromValue(parentRoom);
|
||||
}
|
||||
return QVariant::fromValue(nullptr);
|
||||
}
|
||||
if (role == OrderRole) {
|
||||
if (child->parentItem() == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
const auto childState = child->parentItem()->childStateContent(child);
|
||||
return childState["order"_L1].toString();
|
||||
}
|
||||
if (role == ChildTimestampRole) {
|
||||
if (child->parentItem() == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
const auto childState = child->parentItem()->childState(child);
|
||||
return childState["origin_server_ts"_L1].toString();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex SpaceChildrenModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *parentItem = getItem(parent);
|
||||
if (!parentItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *childItem = parentItem->child(row);
|
||||
if (childItem) {
|
||||
return createIndex(row, column, childItem);
|
||||
}
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex SpaceChildrenModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
SpaceTreeItem *childItem = static_cast<SpaceTreeItem *>(index.internalPointer());
|
||||
SpaceTreeItem *parentItem = childItem->parentItem();
|
||||
|
||||
if (parentItem == m_rootItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
return createIndex(parentItem->row(), 0, parentItem);
|
||||
}
|
||||
|
||||
int SpaceChildrenModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
SpaceTreeItem *parentItem;
|
||||
if (parent.column() > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!parent.isValid()) {
|
||||
parentItem = m_rootItem;
|
||||
} else {
|
||||
parentItem = static_cast<SpaceTreeItem *>(parent.internalPointer());
|
||||
}
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
int SpaceChildrenModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarUrlRole] = "avatarUrl";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[RoomIDRole] = "roomId";
|
||||
roles[MemberCountRole] = "memberCount";
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[AliasRole] = "alias";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[IsSuggestedRole] = "isSuggested";
|
||||
roles[CanAddChildrenRole] = "canAddChildren";
|
||||
roles[ParentDisplayNameRole] = "parentDisplayName";
|
||||
roles[CanSetParentRole] = "canSetParent";
|
||||
roles[IsDeclaredParentRole] = "isDeclaredParent";
|
||||
roles[CanRemove] = "canRemove";
|
||||
roles[ParentRoomRole] = "parentRoom";
|
||||
roles[OrderRole] = "order";
|
||||
roles[ChildTimestampRole] = "childTimestamp";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
bool SpaceChildrenModel::isRoomReplaced(const QString &roomId) const
|
||||
{
|
||||
return m_replacedRooms.contains(roomId);
|
||||
}
|
||||
|
||||
void SpaceChildrenModel::addPendingChild(const QString &childName)
|
||||
{
|
||||
m_pendingChildren += childName;
|
||||
}
|
||||
|
||||
#include "moc_spacechildrenmodel.cpp"
|
||||
148
src/spaces/models/spacechildrenmodel.h
Normal file
148
src/spaces/models/spacechildrenmodel.h
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/csapi/space_hierarchy.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "spacetreeitem.h"
|
||||
|
||||
/**
|
||||
* @class SpaceChildrenModel
|
||||
*
|
||||
* Create a model that contains a list of the child rooms for any given space id.
|
||||
*/
|
||||
class SpaceChildrenModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The current space that the hierarchy is being generated for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *space READ space WRITE setSpace NOTIFY spaceChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model is loading the initial set of children.
|
||||
*/
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
DisplayNameRole = Qt::DisplayRole,
|
||||
AvatarUrlRole,
|
||||
TopicRole,
|
||||
RoomIDRole,
|
||||
AliasRole,
|
||||
MemberCountRole,
|
||||
AllowGuestsRole,
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
IsSpaceRole,
|
||||
IsSuggestedRole,
|
||||
CanAddChildrenRole,
|
||||
ParentDisplayNameRole,
|
||||
CanSetParentRole,
|
||||
IsDeclaredParentRole,
|
||||
CanRemove,
|
||||
ParentRoomRole,
|
||||
OrderRole,
|
||||
ChildTimestampRole,
|
||||
};
|
||||
|
||||
explicit SpaceChildrenModel(QObject *parent = nullptr);
|
||||
~SpaceChildrenModel();
|
||||
|
||||
NeoChatRoom *space() const;
|
||||
void setSpace(NeoChatRoom *space);
|
||||
|
||||
bool loading() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the index of the item in the model specified by the given row, column and parent index.
|
||||
*
|
||||
* @sa QAbstractItemModel::index
|
||||
*/
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the parent of the model item with the given index.
|
||||
*
|
||||
* If the item has no parent, an invalid QModelIndex is returned.
|
||||
*
|
||||
* @sa QAbstractItemModel::parent
|
||||
*/
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of columns in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::columnCount
|
||||
*/
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Whether the room has been replaced.
|
||||
*
|
||||
* @note This information is only available if the local user is either a member
|
||||
* of the replaced room or is a member of the successor room as currently
|
||||
* there is no other way to obtain the required information.
|
||||
*/
|
||||
bool isRoomReplaced(const QString &roomId) const;
|
||||
|
||||
/**
|
||||
* @brief Add the name of new child room that is expected to be added soon.
|
||||
*
|
||||
* A pending child is one where Quotient::Connection::createRoom has been called
|
||||
* but the room hasn't synced with the server yet. This list is used to check
|
||||
* whether a new room loading should trigger a refresh of the model, as we only
|
||||
* want to trigger a refresh if the loading room is part of this space.
|
||||
*/
|
||||
Q_INVOKABLE void addPendingChild(const QString &childName);
|
||||
|
||||
Q_SIGNALS:
|
||||
void spaceChanged();
|
||||
void loadingChanged();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_space;
|
||||
SpaceTreeItem *m_rootItem;
|
||||
|
||||
bool m_loading = false;
|
||||
QList<QPointer<Quotient::GetSpaceHierarchyJob>> m_currentJobs;
|
||||
QList<QString> m_pendingChildren;
|
||||
|
||||
QList<QString> m_replacedRooms;
|
||||
|
||||
SpaceTreeItem *getItem(const QModelIndex &index) const;
|
||||
|
||||
void refreshModel();
|
||||
|
||||
void insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::SpaceHierarchyRoomsChunk> children, const QModelIndex &parent = QModelIndex());
|
||||
};
|
||||
124
src/spaces/models/spacechildsortfiltermodel.cpp
Normal file
124
src/spaces/models/spacechildsortfiltermodel.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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
|
||||
|
||||
#include "spacechildsortfiltermodel.h"
|
||||
|
||||
#include "spacechildrenmodel.h"
|
||||
|
||||
SpaceChildSortFilterModel::SpaceChildSortFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
setRecursiveFilteringEnabled(true);
|
||||
sort(0);
|
||||
}
|
||||
|
||||
void SpaceChildSortFilterModel::setFilterText(const QString &filterText)
|
||||
{
|
||||
m_filterText = filterText;
|
||||
Q_EMIT filterTextChanged();
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
QString SpaceChildSortFilterModel::filterText() const
|
||||
{
|
||||
return m_filterText;
|
||||
}
|
||||
|
||||
bool SpaceChildSortFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||
{
|
||||
if (source_left.data(SpaceChildrenModel::IsSpaceRole).toBool() && source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
|
||||
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty() && !source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
|
||||
return QString::compare(source_left.data(SpaceChildrenModel::OrderRole).toString(), source_right.data(SpaceChildrenModel::OrderRole).toString())
|
||||
< 0;
|
||||
}
|
||||
return source_left.data(SpaceChildrenModel::ChildTimestampRole).toDateTime() > source_right.data(SpaceChildrenModel::ChildTimestampRole).toDateTime();
|
||||
}
|
||||
if (source_left.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
|
||||
return true;
|
||||
} else if (source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
|
||||
return false;
|
||||
}
|
||||
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty() && !source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
|
||||
return QString::compare(source_left.data(SpaceChildrenModel::OrderRole).toString(), source_right.data(SpaceChildrenModel::OrderRole).toString()) < 0;
|
||||
}
|
||||
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
|
||||
return true;
|
||||
} else if (!source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return source_left.data(SpaceChildrenModel::ChildTimestampRole).toDateTime() > source_right.data(SpaceChildrenModel::ChildTimestampRole).toDateTime();
|
||||
}
|
||||
|
||||
bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
if (auto sourceModel = static_cast<SpaceChildrenModel *>(this->sourceModel())) {
|
||||
bool isReplaced = sourceModel->isRoomReplaced(index.data(SpaceChildrenModel::RoomIDRole).toString());
|
||||
bool acceptRoom = index.data(SpaceChildrenModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive);
|
||||
return !isReplaced && acceptRoom;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpaceChildSortFilterModel::move(const QModelIndex ¤tIndex, const QModelIndex &targetIndex)
|
||||
{
|
||||
const auto rootSpace = dynamic_cast<SpaceChildrenModel *>(sourceModel())->space();
|
||||
if (rootSpace == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto connection = rootSpace->connection();
|
||||
|
||||
const auto currentParent = currentIndex.parent();
|
||||
auto targetParent = targetIndex.parent();
|
||||
NeoChatRoom *currentParentSpace = nullptr;
|
||||
if (!currentParent.isValid()) {
|
||||
currentParentSpace = rootSpace;
|
||||
} else {
|
||||
currentParentSpace = static_cast<NeoChatRoom *>(connection->room(currentParent.data(SpaceChildrenModel::RoomIDRole).toString()));
|
||||
}
|
||||
NeoChatRoom *targetParentSpace = nullptr;
|
||||
if (!targetParent.isValid()) {
|
||||
targetParentSpace = rootSpace;
|
||||
} else {
|
||||
targetParentSpace = static_cast<NeoChatRoom *>(connection->room(targetParent.data(SpaceChildrenModel::RoomIDRole).toString()));
|
||||
}
|
||||
// If both parents are not resolvable to a room object we don't have the permissions
|
||||
// required for this action.
|
||||
if (currentParentSpace == nullptr || targetParentSpace == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto currentRow = currentIndex.row();
|
||||
auto targetRow = targetIndex.row();
|
||||
|
||||
const auto moveRoomId = currentIndex.data(SpaceChildrenModel::RoomIDRole).toString();
|
||||
auto targetRoom = static_cast<NeoChatRoom *>(connection->room(targetIndex.data(SpaceChildrenModel::RoomIDRole).toString()));
|
||||
// If the target room is a space, assume we want to drop the room into it.
|
||||
if (targetRoom != nullptr && targetRoom->isSpace()) {
|
||||
targetParent = targetIndex;
|
||||
targetParentSpace = targetRoom;
|
||||
targetRow = rowCount(targetParent);
|
||||
}
|
||||
|
||||
const auto newRowCount = rowCount(targetParent) + (currentParentSpace != targetParentSpace ? 1 : 0);
|
||||
for (int i = 0; i < newRowCount; i++) {
|
||||
if (currentParentSpace == targetParentSpace && i == currentRow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targetParentSpace->setChildOrder(index(i, 0, targetParent).data(SpaceChildrenModel::RoomIDRole).toString(),
|
||||
QString::number(i > targetRow ? i + 1 : i, 36));
|
||||
|
||||
if (i == targetRow) {
|
||||
if (currentParentSpace != targetParentSpace) {
|
||||
currentParentSpace->removeChild(moveRoomId, true);
|
||||
targetParentSpace->addChild(moveRoomId, true, false, false, QString::number(i + 1, 36));
|
||||
} else {
|
||||
targetParentSpace->setChildOrder(currentIndex.data(SpaceChildrenModel::RoomIDRole).toString(), QString::number(i + 1, 36));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_spacechildsortfiltermodel.cpp"
|
||||
56
src/spaces/models/spacechildsortfiltermodel.h
Normal file
56
src/spaces/models/spacechildsortfiltermodel.h
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class SpaceChildSortFilterModel
|
||||
*
|
||||
* This class creates a custom QSortFilterProxyModel for filtering and sorting spaces
|
||||
* in a SpaceChildrenModel.
|
||||
*
|
||||
* @sa SpaceChildrenModel
|
||||
*/
|
||||
class SpaceChildSortFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The text to use to filter room names.
|
||||
*/
|
||||
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
public:
|
||||
SpaceChildSortFilterModel(QObject *parent = nullptr);
|
||||
|
||||
void setFilterText(const QString &filterText);
|
||||
[[nodiscard]] QString filterText() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Returns true if the value of source_left is less than source_right.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::lessThan
|
||||
*/
|
||||
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||
|
||||
/**
|
||||
* @brief Custom filter function checking if an event type has been filtered out.
|
||||
*
|
||||
* The filter rejects a row if the room is known been replaced or if a search
|
||||
* string is set it will only return rooms that match.
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
Q_INVOKABLE void move(const QModelIndex ¤tIndex, const QModelIndex &targetIndex);
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTextChanged();
|
||||
|
||||
private:
|
||||
QString m_filterText;
|
||||
};
|
||||
205
src/spaces/models/spacetreeitem.cpp
Normal file
205
src/spaces/models/spacetreeitem.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
// 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
|
||||
|
||||
#include "spacetreeitem.h"
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
|
||||
SpaceTreeItem *parent,
|
||||
const QString &id,
|
||||
const QString &name,
|
||||
const QString &canonicalAlias,
|
||||
const QString &topic,
|
||||
int memberCount,
|
||||
const QUrl &avatarUrl,
|
||||
bool allowGuests,
|
||||
bool worldReadable,
|
||||
bool isSpace,
|
||||
Quotient::StateEvents childStates)
|
||||
: m_connection(connection)
|
||||
, m_parentItem(parent)
|
||||
, m_id(id)
|
||||
, m_name(name)
|
||||
, m_canonicalAlias(canonicalAlias)
|
||||
, m_topic(topic)
|
||||
, m_memberCount(memberCount)
|
||||
, m_avatarUrl(avatarUrl)
|
||||
, m_allowGuests(allowGuests)
|
||||
, m_worldReadable(worldReadable)
|
||||
, m_isSpace(isSpace)
|
||||
, m_childStates(std::move(childStates))
|
||||
{
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::operator==(const SpaceTreeItem &other) const
|
||||
{
|
||||
return m_id == other.id();
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceTreeItem::child(int row)
|
||||
{
|
||||
return row >= 0 && row < childCount() ? m_children.at(row).get() : nullptr;
|
||||
}
|
||||
|
||||
int SpaceTreeItem::childCount() const
|
||||
{
|
||||
return int(m_children.size());
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::insertChild(std::unique_ptr<SpaceTreeItem> newChild)
|
||||
{
|
||||
if (newChild == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) {
|
||||
if (*it == newChild) {
|
||||
*it = std::move(newChild);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
m_children.push_back(std::move(newChild));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::removeChild(int row)
|
||||
{
|
||||
if (row < 0 || row >= childCount()) {
|
||||
return false;
|
||||
}
|
||||
m_children.erase(m_children.begin() + row);
|
||||
return true;
|
||||
}
|
||||
|
||||
int SpaceTreeItem::row() const
|
||||
{
|
||||
if (m_parentItem == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto it = std::find_if(m_parentItem->m_children.cbegin(), m_parentItem->m_children.cend(), [this](const std::unique_ptr<SpaceTreeItem> &treeItem) {
|
||||
return treeItem.get() == this;
|
||||
});
|
||||
|
||||
if (it != m_parentItem->m_children.cend()) {
|
||||
return std::distance(m_parentItem->m_children.cbegin(), it);
|
||||
}
|
||||
Q_ASSERT(false); // should not happen
|
||||
return -1;
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceTreeItem::parentItem() const
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::canonicalAlias() const
|
||||
{
|
||||
return m_canonicalAlias;
|
||||
}
|
||||
|
||||
QString SpaceTreeItem::topic() const
|
||||
{
|
||||
return m_topic;
|
||||
}
|
||||
|
||||
int SpaceTreeItem::memberCount() const
|
||||
{
|
||||
return m_memberCount;
|
||||
}
|
||||
|
||||
QUrl SpaceTreeItem::avatarUrl() const
|
||||
{
|
||||
if (m_avatarUrl.isEmpty() || m_avatarUrl.scheme() != u"mxc"_s) {
|
||||
return {};
|
||||
}
|
||||
auto url = m_connection->makeMediaUrl(m_avatarUrl);
|
||||
if (url.scheme() == u"mxc"_s) {
|
||||
return url;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::allowGuests() const
|
||||
{
|
||||
return m_allowGuests;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::worldReadable() const
|
||||
{
|
||||
return m_worldReadable;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isJoined() const
|
||||
{
|
||||
if (!m_connection) {
|
||||
return false;
|
||||
}
|
||||
return m_connection->room(id(), Quotient::JoinState::Join) != nullptr;
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isSpace() const
|
||||
{
|
||||
return m_isSpace;
|
||||
}
|
||||
|
||||
QJsonObject SpaceTreeItem::childState(const SpaceTreeItem *child) const
|
||||
{
|
||||
if (child == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (child->parentItem() != this) {
|
||||
return {};
|
||||
}
|
||||
for (const auto &childState : m_childStates) {
|
||||
if (childState->stateKey() == child->id()) {
|
||||
return childState->fullJson();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject SpaceTreeItem::childStateContent(const SpaceTreeItem *child) const
|
||||
{
|
||||
if (child == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (child->parentItem() != this) {
|
||||
return {};
|
||||
}
|
||||
for (const auto &childState : m_childStates) {
|
||||
if (childState->stateKey() == child->id()) {
|
||||
return childState->contentJson();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void SpaceTreeItem::setChildStates(Quotient::StateEvents childStates)
|
||||
{
|
||||
m_childStates.clear();
|
||||
m_childStates = std::move(childStates);
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isSuggested() const
|
||||
{
|
||||
if (m_parentItem == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto childStateContent = m_parentItem->childStateContent(this);
|
||||
return childStateContent.value("suggested"_L1).toBool();
|
||||
}
|
||||
168
src/spaces/models/spacetreeitem.h
Normal file
168
src/spaces/models/spacetreeitem.h
Normal file
@@ -0,0 +1,168 @@
|
||||
// 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
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
#include <Quotient/csapi/space_hierarchy.h>
|
||||
#include <Quotient/events/stateevent.h>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class SpaceTreeItem
|
||||
*
|
||||
* This class defines an item in the space tree hierarchy model.
|
||||
*
|
||||
* @note This is separate from Quotient::Room and NeoChatRoom because we don't have
|
||||
* full room information for any room/space the user hasn't joined and we
|
||||
* don't want to create one for ever possible child in a space as that would
|
||||
* be expensive.
|
||||
*
|
||||
* @sa Quotient::Room, NeoChatRoom
|
||||
*/
|
||||
class SpaceTreeItem
|
||||
{
|
||||
public:
|
||||
explicit SpaceTreeItem(NeoChatConnection *connection,
|
||||
SpaceTreeItem *parent = nullptr,
|
||||
const QString &id = {},
|
||||
const QString &name = {},
|
||||
const QString &canonicalAlias = {},
|
||||
const QString &topic = {},
|
||||
int memberCount = {},
|
||||
const QUrl &avatarUrl = {},
|
||||
bool allowGuests = {},
|
||||
bool worldReadable = {},
|
||||
bool isSpace = {},
|
||||
Quotient::StateEvents childStates = {});
|
||||
|
||||
bool operator==(const SpaceTreeItem &other) const;
|
||||
|
||||
/**
|
||||
* @brief Return the child at the given row number.
|
||||
*
|
||||
* Nullptr is returned if there is no child at the given row number.
|
||||
*/
|
||||
SpaceTreeItem *child(int row);
|
||||
|
||||
/**
|
||||
* @brief The number of children this item has.
|
||||
*/
|
||||
int childCount() const;
|
||||
|
||||
/**
|
||||
* @brief Insert the given child.
|
||||
*/
|
||||
bool insertChild(std::unique_ptr<SpaceTreeItem> newChild);
|
||||
|
||||
/**
|
||||
* @brief Remove the child at the given row number.
|
||||
*
|
||||
* @return True if a child was removed, false if the given row isn't valid.
|
||||
*/
|
||||
bool removeChild(int row);
|
||||
|
||||
/**
|
||||
* @brief Return this item's parent.
|
||||
*/
|
||||
SpaceTreeItem *parentItem() const;
|
||||
|
||||
/**
|
||||
* @brief Return the row number for this child relative to the parent.
|
||||
*
|
||||
* @return The row value if the child has a parent, 0 otherwise.
|
||||
*/
|
||||
int row() const;
|
||||
|
||||
/**
|
||||
* @brief The ID of the room.
|
||||
*/
|
||||
QString id() const;
|
||||
|
||||
/**
|
||||
* @brief The name of the room, if any.
|
||||
*/
|
||||
QString name() const;
|
||||
|
||||
/**
|
||||
* @brief The canonical alias of the room, if any.
|
||||
*/
|
||||
QString canonicalAlias() const;
|
||||
|
||||
/**
|
||||
* @brief The topic of the room, if any.
|
||||
*/
|
||||
QString topic() const;
|
||||
|
||||
/**
|
||||
* @brief The number of members joined to the room.
|
||||
*/
|
||||
int memberCount() const;
|
||||
|
||||
/**
|
||||
* @brief The URL for the room's avatar, if one is set.
|
||||
*
|
||||
* @return A CS API QUrl.
|
||||
*/
|
||||
QUrl avatarUrl() const;
|
||||
|
||||
/**
|
||||
* @brief Whether guest users may join the room and participate in it.
|
||||
*
|
||||
* If they can, they will be subject to ordinary power level rules like any other users.
|
||||
*/
|
||||
bool allowGuests() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the room may be viewed by guest users without joining.
|
||||
*/
|
||||
bool worldReadable() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the local user is a member of the rooom.
|
||||
*/
|
||||
bool isJoined() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the room is a space.
|
||||
*/
|
||||
bool isSpace() const;
|
||||
|
||||
/**
|
||||
* @brief Return the m.space.child stripped state Json for the given child.
|
||||
*/
|
||||
QJsonObject childState(const SpaceTreeItem *child) const;
|
||||
|
||||
/**
|
||||
* @brief Return the m.space.child state event content for the given child.
|
||||
*/
|
||||
QJsonObject childStateContent(const SpaceTreeItem *child) const;
|
||||
|
||||
/**
|
||||
* @brief Set the list of m.space.child events.
|
||||
*
|
||||
* Overwrites existing states. Calling with no input will clear the existing states.
|
||||
*/
|
||||
void setChildStates(Quotient::StateEvents childStates = {});
|
||||
|
||||
/**
|
||||
* @brief Whether the room is suggested in the parent space.
|
||||
*/
|
||||
bool isSuggested() const;
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
std::vector<std::unique_ptr<SpaceTreeItem>> m_children;
|
||||
SpaceTreeItem *m_parentItem;
|
||||
|
||||
QString m_id;
|
||||
QString m_name;
|
||||
QString m_canonicalAlias;
|
||||
QString m_topic;
|
||||
int m_memberCount;
|
||||
QUrl m_avatarUrl;
|
||||
bool m_allowGuests;
|
||||
bool m_worldReadable;
|
||||
bool m_isSpace;
|
||||
Quotient::StateEvents m_childStates;
|
||||
};
|
||||
Reference in New Issue
Block a user