diff --git a/src/models/spacechildrenmodel.cpp b/src/models/spacechildrenmodel.cpp index 5447642f3..1844aad2c 100644 --- a/src/models/spacechildrenmodel.cpp +++ b/src/models/spacechildrenmodel.cpp @@ -8,6 +8,7 @@ #include #include "neochatconnection.h" +#include "neochatroom.h" SpaceChildrenModel::SpaceChildrenModel(QObject *parent) : QAbstractItemModel(parent) @@ -32,7 +33,8 @@ void SpaceChildrenModel::setSpace(NeoChatRoom *space) } // disconnect the new room signal from the old connection in case it is different. if (m_space != nullptr) { - disconnect(m_space->connection(), &Quotient::Connection::loadedRoomState, this, nullptr); + m_space->connection()->disconnect(this); + m_space->disconnect(this); } m_space = space; @@ -116,6 +118,11 @@ void SpaceChildrenModel::insertChildren(std::vector(room)->isSpace()) { + connect(room, &Quotient::Room::changed, this, [this]() { + refreshModel(); + }); + } } if (children[i].childrenState.size() > 0) { auto job = m_space->connection()->callApi(children[i].roomId, Quotient::none, Quotient::none, 1); diff --git a/src/models/spacechildsortfiltermodel.cpp b/src/models/spacechildsortfiltermodel.cpp index 8866264f3..6958b1d27 100644 --- a/src/models/spacechildsortfiltermodel.cpp +++ b/src/models/spacechildsortfiltermodel.cpp @@ -60,4 +60,65 @@ bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelInde return true; } +void SpaceChildSortFilterModel::move(const QModelIndex ¤tIndex, const QModelIndex &targetIndex) +{ + const auto rootSpace = dynamic_cast(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(connection->room(currentParent.data(SpaceChildrenModel::RoomIDRole).toString())); + } + NeoChatRoom *targetParentSpace = nullptr; + if (!targetParent.isValid()) { + targetParentSpace = rootSpace; + } else { + targetParentSpace = static_cast(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(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" diff --git a/src/models/spacechildsortfiltermodel.h b/src/models/spacechildsortfiltermodel.h index fff837b89..9f10364fa 100644 --- a/src/models/spacechildsortfiltermodel.h +++ b/src/models/spacechildsortfiltermodel.h @@ -46,6 +46,8 @@ protected: */ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + Q_INVOKABLE void move(const QModelIndex ¤tIndex, const QModelIndex &targetIndex); + Q_SIGNALS: void filterTextChanged(); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 16ae1c0dc..03a83962d 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -1329,7 +1329,7 @@ bool NeoChatRoom::childrenHaveHighlightNotifications() const return SpaceHierarchyCache::instance().spaceHasHighlightNotifications(id()); } -void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested) +void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested, const QString &order) { if (!isSpace()) { return; @@ -1337,7 +1337,9 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can if (!canSendEvent("m.space.child"_ls)) { return; } - setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}}); + setState("m.space.child"_ls, + childId, + QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}, {"order"_ls, order}}); if (setChildParent) { if (auto child = static_cast(connection()->room(childId))) { @@ -1403,6 +1405,28 @@ void NeoChatRoom::toggleChildSuggested(const QString &childId) } } +void NeoChatRoom::setChildOrder(const QString &childId, const QString &order) +{ + if (!isSpace()) { + return; + } + if (!canSendEvent("m.space.child"_ls)) { + return; + } + if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) { + auto content = childEvent->contentJson(); + if (!content.contains("via"_ls)) { + return; + } + if (content.value("order"_ls).toString() == order) { + return; + } + + content.insert("order"_ls, order); + setState("m.space.child"_ls, childId, content); + } +} + PushNotificationState::State NeoChatRoom::pushNotificationState() const { return m_currentPushNotificationState; diff --git a/src/neochatroom.h b/src/neochatroom.h index 1821e0bdb..02e29d04f 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -560,7 +560,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, bool canonical = false, bool suggested = false); + Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false, const QString &order = {}); /** * @brief Remove the given room as a child. @@ -583,6 +583,8 @@ public: */ Q_INVOKABLE void toggleChildSuggested(const QString &childId); + void setChildOrder(const QString &childId, const QString &order = {}); + bool isInvite() const; bool readOnly() const; diff --git a/src/qml/SpaceHierarchyDelegate.qml b/src/qml/SpaceHierarchyDelegate.qml index 6bd9385b2..db1fd7323 100644 --- a/src/qml/SpaceHierarchyDelegate.qml +++ b/src/qml/SpaceHierarchyDelegate.qml @@ -18,6 +18,7 @@ Item { 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 @@ -36,10 +37,17 @@ Item { signal createRoom Delegates.RoundedItemDelegate { - anchors.centerIn: root + id: mainDelegate + property int row: root.row + + anchors.horizontalCenter: root.horizontalCenter + anchors.verticalCenter: root.verticalCenter width: sizeHelper.currentWidth + highlighted: dropArea.containsDrag + contentItem: RowLayout { + z: 1 spacing: Kirigami.Units.largeSpacing RowLayout { @@ -140,15 +148,55 @@ Item { } } - TapHandler { - onTapped: { + 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(row); + 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)) + } } DelegateSizeHelper {