diff --git a/src/Cache_p.h b/src/Cache_p.h
index a48588e1..6a6b4e0c 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 90f1532b..7b323bb9 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 114e3f94..79f8c33a 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 07fb0417..3bc246b9 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");
|