Create a space module

This commit is contained in:
James Graham
2025-04-16 18:30:52 +01:00
parent 4aec891b1f
commit e787eaabcd
10 changed files with 27 additions and 9 deletions

View 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"

View 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());
};

View 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 &currentIndex, 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"

View 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 &currentIndex, const QModelIndex &targetIndex);
Q_SIGNALS:
void filterTextChanged();
private:
QString m_filterText;
};

View 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();
}

View 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;
};