diff --git a/resources/icons/ui/collapsed.svg b/resources/icons/ui/collapsed.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0ac6a30d5edcb3adccb63fbcf761df4ee5f46853
--- /dev/null
+++ b/resources/icons/ui/collapsed.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.47 4.22a.75.75 0 0 0 0 1.06L15.19 12l-6.72 6.72a.75.75 0 1 0 1.06 1.06l7.25-7.25a.75.75 0 0 0 0-1.06L9.53 4.22a.75.75 0 0 0-1.06 0Z" fill="#212121"/></svg>
\ No newline at end of file
diff --git a/resources/icons/ui/expanded.svg b/resources/icons/ui/expanded.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8c03304ad05cce7c079d793420bc6f4d5682e0f6
--- /dev/null
+++ b/resources/icons/ui/expanded.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z" fill="#212121"/></svg>
\ No newline at end of file
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 0a8587b3dc743baa06f257253545edcae9038eaa..fab3316e336505690b15275b919a5c0bc67d7cc7 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -11,6 +11,7 @@ import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Page {
+    id: communitySidebar
     //leftPadding: Nheko.paddingSmall
     //rightPadding: Nheko.paddingSmall
     property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6)
@@ -22,7 +23,7 @@ Page {
         anchors.left: parent.left
         anchors.right: parent.right
         height: parent.height
-        model: Communities
+        model: Communities.filtered()
 
         ScrollHelper {
             flickable: parent
@@ -107,9 +108,31 @@ Page {
             }
 
             RowLayout {
+                id: r
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
                 anchors.margins: Nheko.paddingMedium
+                anchors.leftMargin: communitySidebar.collapsed ? Nheko.paddingMedium : (Nheko.paddingMedium * (model.depth + 1))
+
+                ImageButton {
+                    visible: !communitySidebar.collapsed && model.collapsible
+                    Layout.preferredHeight: fontMetrics.lineSpacing
+                    Layout.preferredWidth: fontMetrics.lineSpacing
+                    Layout.alignment: Qt.AlignVCenter
+                    height: fontMetrics.lineSpacing
+                    width: fontMetrics.lineSpacing
+                    image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
+                    ToolTip.visible: hovered
+                    ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
+                    hoverEnabled: true
+
+                    onClicked: model.collapsed = !model.collapsed
+                }
+
+                Item {
+                    Layout.preferredWidth: fontMetrics.lineSpacing
+                    visible: !communitySidebar.collapsed && !model.collapsible
+                }
 
                 Avatar {
                     id: avatar
@@ -130,10 +153,10 @@ Page {
                 }
 
                 ElidedLabel {
-                    visible: !collapsed
+                    visible: !communitySidebar.collapsed
                     Layout.alignment: Qt.AlignVCenter
                     color: communityItem.importantText
-                    elideWidth: parent.width - avatar.width - Nheko.paddingMedium
+                    elideWidth: parent.width - avatar.width - r.anchors.leftMargin - Nheko.paddingMedium - fontMetrics.lineSpacing
                     fullText: model.displayName
                     textFormat: Text.PlainText
                 }
diff --git a/resources/res.qrc b/resources/res.qrc
index 4bde40a5b0b0cd55355665e702e28a25272fea51..2ab60e3a69bf25440bac44176a3654717e3442d9 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -1,24 +1,29 @@
 <RCC>
     <qresource prefix="/icons">
-        <file>icons/ui/sticky-note-solid.svg</file>
         <file>icons/ui/add-square-button.svg</file>
-        <file>icons/ui/send.svg</file>
-        <file>icons/ui/smile.svg</file>
-        <file>icons/ui/user-friends-solid.svg</file>
-        <file>icons/ui/place-call.svg</file>
-        <file>icons/ui/attach.svg</file>
         <file>icons/ui/angle-arrow-left.svg</file>
+        <file>icons/ui/attach.svg</file>
+        <file>icons/ui/ban.svg</file>
         <file>icons/ui/chat.svg</file>
         <file>icons/ui/checkmark.svg</file>
         <file>icons/ui/clock.svg</file>
+        <file>icons/ui/collapsed.svg</file>
         <file>icons/ui/delete.svg</file>
+        <file>icons/ui/dismiss.svg</file>
+        <file>icons/ui/double-checkmark.svg</file>
+        <file>icons/ui/download.svg</file>
         <file>icons/ui/edit.svg</file>
         <file>icons/ui/end-call.svg</file>
+        <file>icons/ui/expanded.svg</file>
+        <file>icons/ui/image-failed.svg</file>
         <file>icons/ui/lowprio.svg</file>
         <file>icons/ui/microphone-mute.svg</file>
         <file>icons/ui/microphone-unmute.svg</file>
+        <file>icons/ui/options.svg</file>
         <file>icons/ui/pause-symbol.svg</file>
         <file>icons/ui/people.svg</file>
+        <file>icons/ui/picture-in-picture.svg</file>
+        <file>icons/ui/place-call.svg</file>
         <file>icons/ui/play-sign.svg</file>
         <file>icons/ui/power-off.svg</file>
         <file>icons/ui/refresh.svg</file>
@@ -26,21 +31,18 @@
         <file>icons/ui/round-remove-button.svg</file>
         <file>icons/ui/screen-share.svg</file>
         <file>icons/ui/search.svg</file>
+        <file>icons/ui/send.svg</file>
         <file>icons/ui/settings.svg</file>
+        <file>icons/ui/smile.svg</file>
         <file>icons/ui/speech-bubbles.svg</file>
         <file>icons/ui/star.svg</file>
+        <file>icons/ui/sticky-note-solid.svg</file>
         <file>icons/ui/tag.svg</file>
+        <file>icons/ui/user-friends-solid.svg</file>
         <file>icons/ui/video.svg</file>
         <file>icons/ui/volume-off-indicator.svg</file>
         <file>icons/ui/volume-up.svg</file>
         <file>icons/ui/world.svg</file>
-        <file>icons/ui/picture-in-picture.svg</file>
-        <file>icons/ui/options.svg</file>
-        <file>icons/ui/double-checkmark.svg</file>
-        <file>icons/ui/ban.svg</file>
-        <file>icons/ui/image-failed.svg</file>
-        <file>icons/ui/dismiss.svg</file>
-        <file>icons/ui/download.svg</file>
         <file>icons/emoji-categories/activity.svg</file>
         <file>icons/emoji-categories/flags.svg</file>
         <file>icons/emoji-categories/foods.svg</file>
diff --git a/src/Cache_p.h b/src/Cache_p.h
index a48588e1f91eaa2edef578c8297303e5cc26f0df..6a6b4e0c92f76147c72191375a62b194ab5ad4d5 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -95,6 +95,12 @@ public:
         auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
         return getStateEvent<T>(txn, room_id, state_key);
     }
+    template<typename T>
+    std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(const std::string &room_id)
+    {
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        return getStateEventsWithType<T>(txn, room_id);
+    }
 
     //! retrieve a specific event from account data
     //! pass empty room_id for global account data
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 90f1532bd50edb203ea3a8bdb9255e5198d0a3fa..7b323bb96e0e15a5f8a5a034c466d49b0ccac0c9 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -7,6 +7,8 @@
 #include <set>
 
 #include "Cache.h"
+#include "Cache_p.h"
+#include "Logging.h"
 #include "UserSettingsPage.h"
 
 CommunitiesModel::CommunitiesModel(QObject *parent)
@@ -20,12 +22,29 @@ CommunitiesModel::roleNames() const
       {AvatarUrl, "avatarUrl"},
       {DisplayName, "displayName"},
       {Tooltip, "tooltip"},
-      {ChildrenHidden, "childrenHidden"},
+      {Collapsed, "collapsed"},
+      {Collapsible, "collapsible"},
       {Hidden, "hidden"},
+      {Depth, "depth"},
       {Id, "id"},
     };
 }
 
+bool
+CommunitiesModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+    if (role != CommunitiesModel::Collapsed)
+        return false;
+    else if (index.row() >= 2 || index.row() - 2 < spaceOrder_.size()) {
+        spaceOrder_.tree.at(index.row() - 2).collapsed = value.toBool();
+
+        const auto cindex = spaceOrder_.lastChild(index.row() - 2);
+        emit dataChanged(index, this->index(cindex + 2), {Collapsed, Qt::DisplayRole});
+        return true;
+    } else
+        return false;
+}
+
 QVariant
 CommunitiesModel::data(const QModelIndex &index, int role) const
 {
@@ -37,10 +56,16 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return tr("All rooms");
         case CommunitiesModel::Roles::Tooltip:
             return tr("Shows all rooms without filtering.");
-        case CommunitiesModel::Roles::ChildrenHidden:
+        case CommunitiesModel::Roles::Collapsed:
+            return false;
+        case CommunitiesModel::Roles::Collapsible:
             return false;
         case CommunitiesModel::Roles::Hidden:
             return false;
+        case CommunitiesModel::Roles::Parent:
+            return "";
+        case CommunitiesModel::Roles::Depth:
+            return 0;
         case CommunitiesModel::Roles::Id:
             return "";
         }
@@ -52,25 +77,43 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return tr("Direct Chats");
         case CommunitiesModel::Roles::Tooltip:
             return tr("Show direct chats.");
-        case CommunitiesModel::Roles::ChildrenHidden:
+        case CommunitiesModel::Roles::Collapsed:
+            return false;
+        case CommunitiesModel::Roles::Collapsible:
             return false;
         case CommunitiesModel::Roles::Hidden:
             return hiddentTagIds_.contains("dm");
+        case CommunitiesModel::Roles::Parent:
+            return "";
+        case CommunitiesModel::Roles::Depth:
+            return 0;
         case CommunitiesModel::Roles::Id:
             return "dm";
         }
     } else if (index.row() - 2 < spaceOrder_.size()) {
-        auto id = spaceOrder_.at(index.row() - 2);
+        auto id = spaceOrder_.tree.at(index.row() - 2).name;
         switch (role) {
         case CommunitiesModel::Roles::AvatarUrl:
             return QString::fromStdString(spaces_.at(id).avatar_url);
         case CommunitiesModel::Roles::DisplayName:
         case CommunitiesModel::Roles::Tooltip:
             return QString::fromStdString(spaces_.at(id).name);
-        case CommunitiesModel::Roles::ChildrenHidden:
-            return true;
+        case CommunitiesModel::Roles::Collapsed:
+            return spaceOrder_.tree.at(index.row() - 2).collapsed;
+        case CommunitiesModel::Roles::Collapsible: {
+            auto idx = index.row() - 2;
+            return idx != spaceOrder_.lastChild(idx);
+        }
         case CommunitiesModel::Roles::Hidden:
             return hiddentTagIds_.contains("space:" + id);
+        case CommunitiesModel::Roles::Parent: {
+            if (auto p = spaceOrder_.parent(index.row() - 2); p >= 0)
+                return spaceOrder_.tree[p].name;
+
+            return "";
+        }
+        case CommunitiesModel::Roles::Depth:
+            return spaceOrder_.tree.at(index.row() - 2).depth;
         case CommunitiesModel::Roles::Id:
             return "space:" + id;
         }
@@ -116,8 +159,14 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
         switch (role) {
         case CommunitiesModel::Roles::Hidden:
             return hiddentTagIds_.contains("tag:" + tag);
-        case CommunitiesModel::Roles::ChildrenHidden:
+        case CommunitiesModel::Roles::Collapsed:
             return true;
+        case CommunitiesModel::Roles::Collapsible:
+            return false;
+        case CommunitiesModel::Roles::Parent:
+            return "";
+        case CommunitiesModel::Roles::Depth:
+            return 0;
         case CommunitiesModel::Roles::Id:
             return "tag:" + tag;
         }
@@ -125,21 +174,67 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
     return QVariant();
 }
 
+namespace {
+struct temptree
+{
+    std::map<std::string, temptree> children;
+
+    void insert(const std::vector<std::string> &parents, const std::string &child)
+    {
+        temptree *t = this;
+        for (const auto &e : parents)
+            t = &t->children[e];
+        t->children[child];
+    }
+
+    void flatten(CommunitiesModel::FlatTree &to, int i = 0) const
+    {
+        for (const auto &[child, subtree] : children) {
+            to.tree.push_back({QString::fromStdString(child), i, false});
+            subtree.flatten(to, i + 1);
+        }
+    }
+};
+
+void
+addChildren(temptree &t,
+            std::vector<std::string> path,
+            std::string child,
+            const std::map<std::string, std::set<std::string>> &children)
+{
+    if (std::find(path.begin(), path.end(), child) != path.end())
+        return;
+
+    path.push_back(child);
+
+    if (children.count(child)) {
+        for (const auto &c : children.at(child)) {
+            t.insert(path, c);
+            addChildren(t, path, c, children);
+        }
+    }
+}
+}
+
 void
 CommunitiesModel::initializeSidebar()
 {
     beginResetModel();
     tags_.clear();
-    spaceOrder_.clear();
+    spaceOrder_.tree.clear();
     spaces_.clear();
 
     std::set<std::string> ts;
-    std::vector<RoomInfo> tempSpaces;
+
+    std::set<std::string> isSpace;
+    std::map<std::string, std::set<std::string>> spaceChilds;
+    std::map<std::string, std::set<std::string>> spaceParents;
+
     auto infos = cache::roomInfo();
     for (auto it = infos.begin(); it != infos.end(); it++) {
         if (it.value().is_space) {
-            spaceOrder_.push_back(it.key());
             spaces_[it.key()] = it.value();
+            isSpace.insert(it.key().toStdString());
         } else {
             for (const auto &t : it.value().tags) {
                 if (t.find("u.") == 0 || t.find("m." == 0)) {
@@ -149,6 +244,34 @@ CommunitiesModel::initializeSidebar()
         }
     }
 
+    // NOTE(Nico): We build a forrest from the Directed Cyclic(!) Graph of spaces. To do that we
+    // start with orphan spaces at the top. This leaves out some space circles, but there is no good
+    // way to break that cycle imo anyway. Then we carefully walk a tree down from each root in our
+    // forrest, carefully checking not to run in a circle and get lost forever.
+    // TODO(Nico): Optimize this. We can do this with a lot fewer allocations and checks.
+    for (const auto &space : isSpace) {
+        spaceParents[space];
+        for (const auto &p : cache::client()->getParentRoomIds(space)) {
+            spaceParents[space].insert(p);
+            spaceChilds[p].insert(space);
+        }
+    }
+
+    temptree spacetree;
+    std::vector<std::string> path;
+    for (const auto &space : isSpace) {
+        if (!spaceParents[space].empty())
+            continue;
+
+        spacetree.children[space] = {};
+    }
+    for (const auto &space : spacetree.children) {
+        addChildren(spacetree, path, space.first, spaceChilds);
+    }
+
+    // NOTE(Nico): This flattens the tree into a list, preserving the depth at each element.
+    spacetree.flatten(spaceOrder_);
+
     for (const auto &t : ts)
         tags_.push_back(QString::fromStdString(t));
 
@@ -199,7 +322,7 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
     }
     for (const auto &[roomid, room] : sync_.rooms.leave) {
         (void)room;
-        if (spaceOrder_.contains(QString::fromStdString(roomid)))
+        if (spaces_.count(QString::fromStdString(roomid)))
             tagsUpdated = true;
     }
     for (const auto &e : sync_.account_data.events) {
@@ -228,8 +351,8 @@ CommunitiesModel::setCurrentTagId(QString tagId)
         }
     } else if (tagId.startsWith("space:")) {
         auto tag = tagId.mid(6);
-        for (const auto &t : spaceOrder_) {
-            if (t == tag) {
+        for (const auto &t : spaceOrder_.tree) {
+            if (t.name == tag) {
                 this->currentTagId_ = tagId;
                 emit currentTagIdChanged(currentTagId_);
                 return;
@@ -271,3 +394,88 @@ CommunitiesModel::toggleTagId(QString tagId)
 
     emit hiddenTagsChanged();
 }
+
+FilteredCommunitiesModel::FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent)
+  : QSortFilterProxyModel(parent)
+{
+    setSourceModel(model);
+    setDynamicSortFilter(true);
+    sort(0);
+}
+
+namespace {
+enum Categories
+{
+    World,
+    Direct,
+    Favourites,
+    Server,
+    LowPrio,
+    Space,
+    UserTag,
+};
+
+Categories
+tagIdToCat(QString tagId)
+{
+    if (tagId.isEmpty())
+        return World;
+    else if (tagId == "dm")
+        return Direct;
+    else if (tagId == "tag:m.favourite")
+        return Favourites;
+    else if (tagId == "tag:m.server_notice")
+        return Server;
+    else if (tagId == "tag:m.lowpriority")
+        return LowPrio;
+    else if (tagId.startsWith("space:"))
+        return Space;
+    else
+        return UserTag;
+}
+}
+
+bool
+FilteredCommunitiesModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+    nhlog::ui()->debug("lessThan");
+    QModelIndex const left_idx  = sourceModel()->index(left.row(), 0, QModelIndex());
+    QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex());
+
+    Categories leftCat = tagIdToCat(sourceModel()->data(left_idx, CommunitiesModel::Id).toString());
+    Categories rightCat =
+      tagIdToCat(sourceModel()->data(right_idx, CommunitiesModel::Id).toString());
+
+    if (leftCat != rightCat)
+        return leftCat < rightCat;
+
+    if (leftCat == Space) {
+        return left.row() < right.row();
+    }
+
+    QString leftName  = sourceModel()->data(left_idx, CommunitiesModel::DisplayName).toString();
+    QString rightName = sourceModel()->data(right_idx, CommunitiesModel::DisplayName).toString();
+
+    return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
+}
+bool
+FilteredCommunitiesModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
+{
+    CommunitiesModel *m = qobject_cast<CommunitiesModel *>(this->sourceModel());
+    if (!m)
+        return true;
+
+    if (sourceRow < 2 || sourceRow - 2 >= m->spaceOrder_.size())
+        return true;
+
+    auto idx = sourceRow - 2;
+
+    while (idx >= 0 && m->spaceOrder_.tree[idx].depth > 0) {
+        idx = m->spaceOrder_.parent(idx);
+
+        if (idx >= 0 && m->spaceOrder_.tree.at(idx).collapsed)
+            return false;
+    }
+
+    return true;
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 114e3f9414ce174456ccf65961e02299f8e4acba..79f8c33ad016da1a28086734fc9d1cb72d906f70 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -6,6 +6,7 @@
 
 #include <QAbstractListModel>
 #include <QHash>
+#include <QSortFilterProxyModel>
 #include <QString>
 #include <QStringList>
 
@@ -13,6 +14,18 @@
 
 #include "CacheStructs.h"
 
+class CommunitiesModel;
+
+class FilteredCommunitiesModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+
+public:
+    FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent = nullptr);
+    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+    bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
+};
+
 class CommunitiesModel : public QAbstractListModel
 {
     Q_OBJECT
@@ -27,11 +40,59 @@ public:
         AvatarUrl = Qt::UserRole,
         DisplayName,
         Tooltip,
-        ChildrenHidden,
+        Collapsed,
+        Collapsible,
         Hidden,
+        Parent,
+        Depth,
         Id,
     };
 
+    struct FlatTree
+    {
+        struct Elem
+        {
+            QString name;
+            int depth      = 0;
+            bool collapsed = false;
+        };
+
+        std::vector<Elem> tree;
+
+        int size() const { return static_cast<int>(tree.size()); }
+        int indexOf(const QString &s) const
+        {
+            for (int i = 0; i < size(); i++)
+                if (tree[i].name == s)
+                    return i;
+            return -1;
+        }
+        int lastChild(int index) const
+        {
+            if (index >= size() || index < 0)
+                return index;
+            const auto depth = tree[index].depth;
+            int i            = index + 1;
+            for (; i < size(); i++)
+                if (tree[i].depth == depth)
+                    break;
+            return i - 1;
+        }
+        int parent(int index) const
+        {
+            if (index >= size() || index < 0)
+                return -1;
+            const auto depth = tree[index].depth;
+            if (depth == 0)
+                return -1;
+            int i = index - 1;
+            for (; i >= 0; i--)
+                if (tree[i].depth < depth)
+                    break;
+            return i;
+        }
+    };
+
     CommunitiesModel(QObject *parent = nullptr);
     QHash<int, QByteArray> roleNames() const override;
     int rowCount(const QModelIndex &parent = QModelIndex()) const override
@@ -40,6 +101,7 @@ public:
         return 2 + tags_.size() + spaceOrder_.size();
     }
     QVariant data(const QModelIndex &index, int role) const override;
+    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
 
 public slots:
     void initializeSidebar();
@@ -63,6 +125,7 @@ public slots:
         return tagsWD;
     }
     void toggleTagId(QString tagId);
+    FilteredCommunitiesModel *filtered() { return new FilteredCommunitiesModel(this, this); }
 
 signals:
     void currentTagIdChanged(QString tagId);
@@ -73,6 +136,8 @@ private:
     QStringList tags_;
     QString currentTagId_;
     QStringList hiddentTagIds_;
-    QStringList spaceOrder_;
+    FlatTree spaceOrder_;
     std::map<QString, RoomInfo> spaces_;
+
+    friend class FilteredCommunitiesModel;
 };
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 07fb0417e2e132766adb3d5e65e02ae229a55dba..3bc246b91121793712253047d8265661d53c1a00 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -260,6 +260,13 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
     qRegisterMetaType<mtx::events::collections::TimelineEvents>();
     qRegisterMetaType<std::vector<DeviceInfo>>();
 
+    qmlRegisterUncreatableType<FilteredCommunitiesModel>(
+      "im.nheko",
+      1,
+      0,
+      "FilteredCommunitiesModel",
+      "Use Communities.filtered() to create a FilteredCommunitiesModel");
+
     qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
     qmlRegisterUncreatableType<emoji::Emoji>(
       "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");