Skip to content
Snippets Groups Projects
Verified Commit fe49beb6 authored by Nicolas Werner's avatar Nicolas Werner
Browse files

Hide me underneath the space tree

parent b505fa42
No related branches found
No related tags found
No related merge requests found
Pipeline #2207 passed
<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
<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
......@@ -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
}
......
<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>
......
......@@ -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
......
......@@ -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;
}
......@@ -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;
};
......@@ -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");
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment