From fe49beb68ed7f582154d44c5af931a93c6b8e1bb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 1 Dec 2021 00:02:41 +0100 Subject: [PATCH] Hide me underneath the space tree --- resources/icons/ui/collapsed.svg | 1 + resources/icons/ui/expanded.svg | 1 + resources/qml/CommunitiesList.qml | 29 +++- resources/res.qrc | 28 ++-- src/Cache_p.h | 6 + src/timeline/CommunitiesModel.cpp | 234 +++++++++++++++++++++++++-- src/timeline/CommunitiesModel.h | 69 +++++++- src/timeline/TimelineViewManager.cpp | 7 + 8 files changed, 344 insertions(+), 31 deletions(-) create mode 100644 resources/icons/ui/collapsed.svg create mode 100644 resources/icons/ui/expanded.svg diff --git a/resources/icons/ui/collapsed.svg b/resources/icons/ui/collapsed.svg new file mode 100644 index 00000000..0ac6a30d --- /dev/null +++ b/resources/icons/ui/collapsed.svg @@ -0,0 +1 @@ + \ 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 00000000..8c03304a --- /dev/null +++ b/resources/icons/ui/expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index 0a8587b3..fab3316e 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 4bde40a5..2ab60e3a 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -1,24 +1,29 @@ - icons/ui/sticky-note-solid.svg icons/ui/add-square-button.svg - icons/ui/send.svg - icons/ui/smile.svg - icons/ui/user-friends-solid.svg - icons/ui/place-call.svg - icons/ui/attach.svg icons/ui/angle-arrow-left.svg + icons/ui/attach.svg + icons/ui/ban.svg icons/ui/chat.svg icons/ui/checkmark.svg icons/ui/clock.svg + icons/ui/collapsed.svg icons/ui/delete.svg + icons/ui/dismiss.svg + icons/ui/double-checkmark.svg + icons/ui/download.svg icons/ui/edit.svg icons/ui/end-call.svg + icons/ui/expanded.svg + icons/ui/image-failed.svg icons/ui/lowprio.svg icons/ui/microphone-mute.svg icons/ui/microphone-unmute.svg + icons/ui/options.svg icons/ui/pause-symbol.svg icons/ui/people.svg + icons/ui/picture-in-picture.svg + icons/ui/place-call.svg icons/ui/play-sign.svg icons/ui/power-off.svg icons/ui/refresh.svg @@ -26,21 +31,18 @@ icons/ui/round-remove-button.svg icons/ui/screen-share.svg icons/ui/search.svg + icons/ui/send.svg icons/ui/settings.svg + icons/ui/smile.svg icons/ui/speech-bubbles.svg icons/ui/star.svg + icons/ui/sticky-note-solid.svg icons/ui/tag.svg + icons/ui/user-friends-solid.svg icons/ui/video.svg icons/ui/volume-off-indicator.svg icons/ui/volume-up.svg icons/ui/world.svg - icons/ui/picture-in-picture.svg - icons/ui/options.svg - icons/ui/double-checkmark.svg - icons/ui/ban.svg - icons/ui/image-failed.svg - icons/ui/dismiss.svg - icons/ui/download.svg icons/emoji-categories/activity.svg icons/emoji-categories/flags.svg icons/emoji-categories/foods.svg 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(txn, room_id, state_key); } + template + std::vector> getStateEventsWithType(const std::string &room_id) + { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + return getStateEventsWithType(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 #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 children; + + void insert(const std::vector &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 path, + std::string child, + const std::map> &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 ts; - std::vector tempSpaces; + + std::set isSpace; + std::map> spaceChilds; + std::map> 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 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(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 #include +#include #include #include @@ -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 tree; + + int size() const { return static_cast(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 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 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(); qRegisterMetaType>(); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "FilteredCommunitiesModel", + "Use Communities.filtered() to create a FilteredCommunitiesModel"); + qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiModel"); qmlRegisterUncreatableType( "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");