diff --git a/imports/NeoChat/Page/RoomListPage.qml b/imports/NeoChat/Page/RoomListPage.qml index 1d0722d3c..777a4ed76 100644 --- a/imports/NeoChat/Page/RoomListPage.qml +++ b/imports/NeoChat/Page/RoomListPage.qml @@ -15,6 +15,95 @@ import NeoChat.Component 1.0 import NeoChat.Menu 1.0 Kirigami.ScrollablePage { + + header: ColumnLayout { + visible: !page.collapsedMode + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 0 + + ListView { + id: spaceList + property string activeSpaceId: '' + + orientation: Qt.Horizontal + spacing: Kirigami.Units.largeSpacing + clip:true + visible: spaceList.count > 0 + + Layout.preferredHeight: Kirigami.Units.gridUnit * 3 + Layout.fillWidth: true + + model: SortFilterSpaceListModel { + id: sortFilterSpaceListModel + sourceModel: RoomListModel { + id: spaceListModel + connection: Controller.activeConnection + } + } + + + Connections { + target: SpaceHierarchyCache + function onSpaceHierarchyChanged() { + if (spaceList.activeSpaceId !== '') { + sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(spaceList.activeSpaceId, false); + } + } + } + + header: QQC2.Control { + contentItem: QQC2.RoundButton { + id: homeButton + flat: true + padding: Kirigami.Units.gridUnit / 2 + icon.name: "home" + text: i18nc('@action:button', 'Show All Rooms') + display: QQC2.AbstractButton.IconOnly + + onClicked: { + sortFilterRoomListModel.activeSpaceRooms = []; + spaceList.activeSpaceId = ''; + listView.positionViewAtIndex(0, ListView.Beginning); + } + + QQC2.ToolTip { + text: homeButton.text + } + } + } + + delegate: QQC2.Control { + required property string avatar + required property var currentRoom + required property int index + required property string id + implicitWidth: ListView.view.headerItem.implicitWidth + implicitHeight: ListView.view.headerItem.implicitHeight + + contentItem: Kirigami.Avatar { + id: del + + actions.main: Kirigami.Action { + id: enterSpaceAction + onTriggered: { + spaceList.activeSpaceId = id; + sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(id, true); + } + } + + QQC2.ToolTip { + text: currentRoom.displayName + } + + source: avatar !== "" ? "image://mxc/" + avatar : "" + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + } id: page title: i18n("Rooms") @@ -68,7 +157,6 @@ Kirigami.ScrollablePage { } } - ListView { id: listView @@ -100,6 +188,8 @@ Kirigami.ScrollablePage { } } + Layout.fillWidth: true + Kirigami.PlaceholderMessage { anchors.centerIn: parent width: parent.width - (Kirigami.Units.largeSpacing * 4) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ace784b32..c027b827c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,8 @@ add_library(neochat STATIC messageeventmodel.cpp messagefiltermodel.cpp roomlistmodel.cpp + sortfilterspacelistmodel.cpp + spacehierarchycache.cpp roommanager.cpp neochatroom.cpp neochatuser.cpp diff --git a/src/main.cpp b/src/main.cpp index a207ff392..7a09d3e99 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,6 +69,8 @@ #include "roomlistmodel.h" #include "roommanager.h" #include "sortfilterroomlistmodel.h" +#include "sortfilterspacelistmodel.h" +#include "spacehierarchycache.h" #include "urlhelper.h" #include "userdirectorylistmodel.h" #include "userlistmodel.h" @@ -177,6 +179,7 @@ int main(int argc, char *argv[]) Login *login = new Login(); ChatBoxHelper chatBoxHelper; UrlHelper urlHelper; + SpaceHierarchyCache spaceHierarchyCache; #ifdef HAVE_COLORSCHEME ColorSchemer colorScheme; @@ -197,6 +200,7 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app)); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CommandModel", new CommandModel(&app)); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance()); + qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &spaceHierarchyCache); qmlRegisterType("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "ChatDocumentHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "RoomListModel"); @@ -209,6 +213,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "PublicRoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "UserDirectoryListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterRoomListModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "SortFilterSpaceListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "DevicesModel"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index bbf1cb026..a876843aa 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -30,6 +30,7 @@ #include "events/accountdataevents.h" #include "events/reactionevent.h" #include "events/roomcanonicalaliasevent.h" +#include "events/roomcreateevent.h" #include "events/roommessageevent.h" #include "events/roompowerlevelsevent.h" #include "events/typingevent.h" @@ -869,3 +870,17 @@ void NeoChatRoom::clearInvitationNotification() { NotificationsManager::instance().clearInvitationNotification(id()); } + +bool NeoChatRoom::isSpace() +{ + const auto creationEvent = this->creation(); + if (!creationEvent) { + return false; + } + +#ifdef QUOTIENT_07 + return creationEvent->roomType() == RoomType::Space; +#else + return false; +#endif +} diff --git a/src/neochatroom.h b/src/neochatroom.h index 8cb82945c..421e719fc 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -72,6 +72,8 @@ public: /// \see lastEventToString [[nodiscard]] QString subtitleText(); + [[nodiscard]] bool isSpace(); + bool isEventHighlighted(const Quotient::RoomEvent *e) const; [[nodiscard]] QString joinRule() const; diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index 1b0b47dc9..0ddf705f7 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -408,6 +408,10 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const if (role == IdRole) { return room->id(); } + if (role == IsSpaceRole) { + return room->isSpace(); + } + return QVariant(); } @@ -439,6 +443,8 @@ QHash RoomListModel::roleNames() const roles[CurrentRoomRole] = "currentRoom"; roles[CategoryVisibleRole] = "categoryVisible"; roles[SubtitleTextRole] = "subtitleText"; + roles[IsSpaceRole] = "isSpace"; + roles[IdRole] = "id"; return roles; } diff --git a/src/roomlistmodel.h b/src/roomlistmodel.h index 634599374..68e404fd7 100644 --- a/src/roomlistmodel.h +++ b/src/roomlistmodel.h @@ -52,6 +52,7 @@ public: SubtitleTextRole, AvatarImageRole, IdRole, + IsSpaceRole, }; Q_ENUM(EventRoles) @@ -99,6 +100,7 @@ private: QMap m_categoryVisibility; int m_notificationCount = 0; + QString m_activeSpaceId = ""; void connectRoomSignals(NeoChatRoom *room); void handleNotifications(); diff --git a/src/sortfilterroomlistmodel.cpp b/src/sortfilterroomlistmodel.cpp index 797d18059..074cdc826 100644 --- a/src/sortfilterroomlistmodel.cpp +++ b/src/sortfilterroomlistmodel.cpp @@ -76,6 +76,23 @@ QString SortFilterRoomListModel::filterText() const bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent); - return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive) - && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"; + + bool acceptRoom = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive) + && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded" + && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false; + + if (m_activeSpaceRooms.empty()) + return acceptRoom; + else + return std::find(m_activeSpaceRooms.begin(), + m_activeSpaceRooms.end(), + sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IdRole).toString()) + != m_activeSpaceRooms.end() + && acceptRoom; +} + +void SortFilterRoomListModel::setActiveSpaceRooms(QVector activeSpaceRooms) +{ + this->m_activeSpaceRooms = activeSpaceRooms; + invalidate(); } diff --git a/src/sortfilterroomlistmodel.h b/src/sortfilterroomlistmodel.h index ca06fa085..b87734cb0 100644 --- a/src/sortfilterroomlistmodel.h +++ b/src/sortfilterroomlistmodel.h @@ -11,6 +11,7 @@ class SortFilterRoomListModel : public QSortFilterProxyModel Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged) Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged) + Q_PROPERTY(QVector activeSpaceRooms WRITE setActiveSpaceRooms) public: enum RoomSortOrder { @@ -30,6 +31,8 @@ public: [[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + Q_INVOKABLE void setActiveSpaceRooms(QVector activeSpaceRooms); + protected: [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; @@ -40,4 +43,5 @@ Q_SIGNALS: private: RoomSortOrder m_sortOrder = Categories; QString m_filterText; + QVector m_activeSpaceRooms; }; diff --git a/src/sortfilterspacelistmodel.cpp b/src/sortfilterspacelistmodel.cpp new file mode 100644 index 000000000..33ebf2695 --- /dev/null +++ b/src/sortfilterspacelistmodel.cpp @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Snehit Sah +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#include "sortfilterspacelistmodel.h" + +#include "roomlistmodel.h" + +SortFilterSpaceListModel::SortFilterSpaceListModel(QObject *parent) + : QSortFilterProxyModel{parent} +{ + setSortRole(RoomListModel::IdRole); + sort(0); + invalidateFilter(); +} + +bool SortFilterSpaceListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_parent); + return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() + && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"; +} + +bool SortFilterSpaceListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + const auto idLeft = sourceModel()->data(source_left, RoomListModel::IdRole).toString(); + const auto idRight = sourceModel()->data(source_right, RoomListModel::IdRole).toString(); + return idLeft < idRight; +} diff --git a/src/sortfilterspacelistmodel.h b/src/sortfilterspacelistmodel.h new file mode 100644 index 000000000..5805fd8ee --- /dev/null +++ b/src/sortfilterspacelistmodel.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 Snehit Sah +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#pragma once + +#include + +class SortFilterSpaceListModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit SortFilterSpaceListModel(QObject *parent = nullptr); + + [[nodiscard]] QString activeSpaceId() const; + + [[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + +Q_SIGNALS: + void activeSpaceIdChanged(QString &activeSpaceId); + +protected: + [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; +}; diff --git a/src/spacehierarchycache.cpp b/src/spacehierarchycache.cpp new file mode 100644 index 000000000..1f4cbc091 --- /dev/null +++ b/src/spacehierarchycache.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2022 Snehit Sah +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#include "spacehierarchycache.h" + +#include "controller.h" +#ifdef QUOTIENT_07 +#include "csapi/space_hierarchy.h" +#endif +#include "neochatroom.h" + +SpaceHierarchyCache::SpaceHierarchyCache(QObject *parent) + : QObject{parent} +{ + cacheSpaceHierarchy(); + connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() { + cacheSpaceHierarchy(); + }); +} + +void SpaceHierarchyCache::cacheSpaceHierarchy() +{ +#ifdef QUOTIENT_07 + auto connection = Controller::instance().activeConnection(); + if (!connection) { + return; + } + + const auto roomList = connection->allRooms(); + for (const auto &room : roomList) { + const auto neoChatRoom = static_cast(room); + if (neoChatRoom->isSpace()) { + populateSpaceHierarchy(neoChatRoom->id()); + } else { + connect(neoChatRoom, &Room::baseStateLoaded, neoChatRoom, [this, neoChatRoom]() { + if (neoChatRoom->isSpace()) { + populateSpaceHierarchy(neoChatRoom->id()); + } + }); + } + } +#endif +} + +void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId) +{ + auto connection = Controller::instance().activeConnection(); + if (!connection) { + return; + } +#ifdef QUOTIENT_07 + GetSpaceHierarchyJob *job = connection->callApi(spaceId); + + connect(job, &BaseJob::success, this, [this, job, spaceId]() { + const auto rooms = job->rooms(); + QVector roomList; + for (unsigned long i = 0; i < rooms.size(); ++i) { + roomList.push_back(rooms.at(i).roomId); + } + m_spaceHierarchy.insert(spaceId, roomList); + Q_EMIT spaceHierarchyChanged(); + }); +#endif +} + +QVector SpaceHierarchyCache::getRoomListForSpace(const QString &spaceId, bool updateCache) +{ + if (updateCache) { + populateSpaceHierarchy(spaceId); + } + return m_spaceHierarchy[spaceId]; +} diff --git a/src/spacehierarchycache.h b/src/spacehierarchycache.h new file mode 100644 index 000000000..32299fca8 --- /dev/null +++ b/src/spacehierarchycache.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Snehit Sah +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#pragma once + +#include +#include +#include +#include + +class SpaceHierarchyCache : public QObject +{ + Q_OBJECT + +public: + explicit SpaceHierarchyCache(QObject *parent = nullptr); + + [[nodiscard]] Q_INVOKABLE QVector getRoomListForSpace(const QString &spaceId, bool updateCache); + +Q_SIGNALS: + void spaceHierarchyChanged(); + +private: + QVector m_activeSpaceRooms; + QHash> m_spaceHierarchy; + void cacheSpaceHierarchy(); + void populateSpaceHierarchy(const QString &spaceId); +};