diff --git a/resources/icons/ui/lowprio.png b/resources/icons/ui/lowprio.png
new file mode 100644
index 00000000..b815d8bb
Binary files /dev/null and b/resources/icons/ui/lowprio.png differ
diff --git a/resources/icons/ui/lowprio@2x.png b/resources/icons/ui/lowprio@2x.png
new file mode 100644
index 00000000..4581946e
Binary files /dev/null and b/resources/icons/ui/lowprio@2x.png differ
diff --git a/resources/icons/ui/star.png b/resources/icons/ui/star.png
new file mode 100644
index 00000000..f2c73243
Binary files /dev/null and b/resources/icons/ui/star.png differ
diff --git a/resources/icons/ui/star@2x.png b/resources/icons/ui/star@2x.png
new file mode 100644
index 00000000..0cde94d8
Binary files /dev/null and b/resources/icons/ui/star@2x.png differ
diff --git a/resources/icons/ui/tag.png b/resources/icons/ui/tag.png
new file mode 100644
index 00000000..61ae6b83
Binary files /dev/null and b/resources/icons/ui/tag.png differ
diff --git a/resources/icons/ui/tag@2x.png b/resources/icons/ui/tag@2x.png
new file mode 100644
index 00000000..5a6769b0
Binary files /dev/null and b/resources/icons/ui/tag@2x.png differ
diff --git a/resources/res.qrc b/resources/res.qrc
index d024a5d5..cef55773 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -53,6 +53,13 @@
icons/ui/world.png
icons/ui/world@2x.png
+ icons/ui/tag.png
+ icons/ui/tag@2x.png
+ icons/ui/star.png
+ icons/ui/star@2x.png
+ icons/ui/lowprio.png
+ icons/ui/lowprio@2x.png
+
icons/ui/edit.png
icons/ui/edit@2x.png
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 372dd44a..a9094e2d 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -936,6 +936,8 @@ Cache::calculateRoomReadStatus(const std::string &room_id)
void
Cache::saveState(const mtx::responses::Sync &res)
{
+ using namespace mtx::events;
+
auto txn = lmdb::txn::begin(env_);
setNextBatchToken(txn, res.next_batch);
@@ -957,6 +959,35 @@ Cache::saveState(const mtx::responses::Sync &res)
getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first))
.toStdString();
+ // Process the account_data associated with this room
+ bool has_new_tags = false;
+ for (const auto &evt : room.second.account_data.events) {
+ // for now only fetch tag events
+ if (evt.type() == typeid(Event)) {
+ auto tags_evt = boost::get>(evt);
+ has_new_tags = true;
+ for (const auto &tag : tags_evt.content.tags) {
+ updatedInfo.tags.push_back(tag.first);
+ }
+ }
+ }
+ if (!has_new_tags) {
+ // retrieve the old tags, they haven't changed
+ lmdb::val data;
+ if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room.first), data)) {
+ try {
+ RoomInfo tmp =
+ json::parse(std::string(data.data(), data.size()));
+ updatedInfo.tags = tmp.tags;
+ } catch (const json::exception &e) {
+ nhlog::db()->warn(
+ "failed to parse room info: room_id ({}), {}",
+ room.first,
+ std::string(data.data(), data.size()));
+ }
+ }
+ }
+
lmdb::dbi_put(
txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump()));
@@ -1078,6 +1109,27 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
return rooms;
}
+std::vector
+Cache::roomsWithTagUpdates(const mtx::responses::Sync &res)
+{
+ using namespace mtx::events;
+
+ std::vector rooms;
+ for (const auto &room : res.rooms.join) {
+ bool hasUpdates = false;
+ for (const auto &evt : room.second.account_data.events) {
+ if (evt.type() == typeid(Event)) {
+ hasUpdates = true;
+ }
+ }
+
+ if (hasUpdates)
+ rooms.emplace_back(room.first);
+ }
+
+ return rooms;
+}
+
RoomInfo
Cache::singleRoomInfo(const std::string &room_id)
{
diff --git a/src/Cache.h b/src/Cache.h
index 5bdfb113..b730d6fc 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -115,6 +115,8 @@ struct RoomInfo
bool guest_access = false;
//! Metadata describing the last message in the timeline.
DescInfo msgInfo;
+ //! The list of tags associated with this room
+ std::vector tags;
};
inline void
@@ -129,6 +131,9 @@ to_json(json &j, const RoomInfo &info)
if (info.member_count != 0)
j["member_count"] = info.member_count;
+
+ if (info.tags.size() != 0)
+ j["tags"] = info.tags;
}
inline void
@@ -143,6 +148,9 @@ from_json(const json &j, RoomInfo &info)
if (j.count("member_count"))
info.member_count = j.at("member_count");
+
+ if (j.count("tags"))
+ info.tags = j.at("tags").get>();
}
//! Basic information per member;
@@ -384,11 +392,16 @@ public:
RoomInfo singleRoomInfo(const std::string &room_id);
std::vector roomsWithStateUpdates(const mtx::responses::Sync &res);
+ std::vector roomsWithTagUpdates(const mtx::responses::Sync &res);
std::map getRoomInfo(const std::vector &rooms);
std::map roomUpdates(const mtx::responses::Sync &sync)
{
return getRoomInfo(roomsWithStateUpdates(sync));
}
+ std::map roomTagUpdates(const mtx::responses::Sync &sync)
+ {
+ return getRoomInfo(roomsWithTagUpdates(sync));
+ }
//! Calculates which the read status of a room.
//! Whether all the events in the timeline have been read.
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 3a534df1..6a7e7d81 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -569,6 +569,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
});
});
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
+ connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
connect(
this, &ChatPage::syncTopBar, this, [this](const std::map &updates) {
if (updates.find(currentRoom()) != updates.end())
@@ -797,6 +798,7 @@ ChatPage::loadStateFromCache()
emit initializeEmptyViews(cache::client()->roomMessages());
emit initializeRoomList(cache::client()->roomInfo());
+ emit syncTags(cache::client()->roomInfo().toStdMap());
cache::client()->calculateRoomReadStatus();
@@ -1079,6 +1081,8 @@ ChatPage::trySync()
emit syncTopBar(updates);
emit syncRoomlist(updates);
+ emit syncTags(cache::client()->roomTagUpdates(res));
+
cache::client()->deleteOldData();
} catch (const lmdb::map_full_error &e) {
nhlog::db()->error("lmdb is full: {}", e.what());
@@ -1213,6 +1217,7 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request
emit initializeRoomList(cache::client()->roomInfo());
cache::client()->calculateRoomReadStatus();
+ emit syncTags(cache::client()->roomInfo().toStdMap());
} catch (const lmdb::error &e) {
nhlog::db()->error("failed to save state after initial sync: {}", e.what());
startInitialSync();
diff --git a/src/ChatPage.h b/src/ChatPage.h
index dc30e497..2c728c17 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -136,6 +136,7 @@ signals:
void initializeEmptyViews(const std::map &msgs);
void syncUI(const mtx::responses::Rooms &rooms);
void syncRoomlist(const std::map &updates);
+ void syncTags(const std::map &updates);
void syncTopBar(const std::map &updates);
void dropToLoginPageCb(const QString &msg);
diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp
index 7054db9d..fc762376 100644
--- a/src/CommunitiesList.cpp
+++ b/src/CommunitiesList.cpp
@@ -47,7 +47,15 @@ CommunitiesList::CommunitiesList(QWidget *parent)
void
CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response)
{
- communities_.clear();
+ // remove all non-tag communities
+ auto it = communities_.begin();
+ while (it != communities_.end()) {
+ if (it->second->is_tag()) {
+ ++it;
+ } else {
+ it = communities_.erase(it);
+ }
+ }
addGlobalItem();
@@ -56,6 +64,60 @@ CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response)
communities_["world"]->setPressedState(true);
emit communityChanged("world");
+ sortEntries();
+}
+
+void
+CommunitiesList::syncTags(const std::map &info)
+{
+ for (const auto &room : info)
+ setTagsForRoom(room.first, room.second.tags);
+ sortEntries();
+}
+
+void
+CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector &tags)
+{
+ // create missing tag if any
+ for (const auto &tag : tags) {
+ // filter out tags we should ignore according to the spec
+ // https://matrix.org/docs/spec/client_server/r0.4.0.html#id154
+ // nheko currently does not make use of internal tags
+ // so we ignore any tag containig a `.` (which would indicate a tag
+ // in the form `tld.domain.*`) except for `m.*` and `u.*`.
+ if (tag.find(".") != ::std::string::npos && tag.compare(0, 2, "m.") &&
+ tag.compare(0, 2, "u."))
+ continue;
+ QString name = QString("tag:") + QString::fromStdString(tag);
+ if (!communityExists(name)) {
+ addCommunity(std::string("tag:") + tag);
+ }
+ }
+ // update membership of the room for all tags
+ auto it = communities_.begin();
+ while (it != communities_.end()) {
+ // Skip if the community is not a tag
+ if (!it->second->is_tag()) {
+ ++it;
+ continue;
+ }
+ // insert or remove the room from the tag as appropriate
+ std::string current_tag =
+ it->first.right(it->first.size() - strlen("tag:")).toStdString();
+ if (std::find(tags.begin(), tags.end(), current_tag) != tags.end()) {
+ // the room has this tag
+ it->second->addRoom(room_id);
+ } else {
+ // the room does not have this tag
+ it->second->delRoom(room_id);
+ }
+ // Check if the tag is now empty, if yes delete it
+ if (it->second->rooms().empty()) {
+ it = communities_.erase(it);
+ } else {
+ ++it;
+ }
+ }
}
void
@@ -193,3 +255,47 @@ CommunitiesList::roomList(const QString &id) const
return {};
}
+
+void
+CommunitiesList::sortEntries()
+{
+ std::vector header;
+ std::vector communities;
+ std::vector tags;
+ std::vector footer;
+ // remove all the contents and sort them in the 4 vectors
+ for (auto &entry : communities_) {
+ CommunitiesListItem *item = entry.second.data();
+ contentsLayout_->removeWidget(item);
+ // world is handled separately
+ if (entry.first == "world")
+ continue;
+ // sort the rest
+ if (item->is_tag())
+ if (entry.first == "tag:m.favourite")
+ header.push_back(item);
+ else if (entry.first == "tag:m.lowpriority")
+ footer.push_back(item);
+ else
+ tags.push_back(item);
+ else
+ communities.push_back(item);
+ }
+
+ // now there remains only the stretch in the layout, remove it
+ QLayoutItem *stretch = contentsLayout_->itemAt(0);
+ contentsLayout_->removeItem(stretch);
+
+ contentsLayout_->addWidget(communities_["world"].data());
+
+ auto insert_widgets = [this](auto &vec) {
+ for (auto item : vec)
+ contentsLayout_->addWidget(item);
+ };
+ insert_widgets(header);
+ insert_widgets(communities);
+ insert_widgets(tags);
+ insert_widgets(footer);
+
+ contentsLayout_->addItem(stretch);
+}
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
index d4db54cc..b18df654 100644
--- a/src/CommunitiesList.h
+++ b/src/CommunitiesList.h
@@ -4,6 +4,7 @@
#include
#include
+#include "Cache.h"
#include "CommunitiesListItem.h"
#include "ui/Theme.h"
@@ -20,6 +21,9 @@ public:
void removeCommunity(const QString &id) { communities_.erase(id); };
std::map roomList(const QString &id) const;
+ void syncTags(const std::map &info);
+ void setTagsForRoom(const QString &id, const std::vector &tags);
+
signals:
void communityChanged(const QString &id);
void avatarRetrieved(const QString &id, const QPixmap &img);
@@ -34,6 +38,7 @@ public slots:
private:
void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
void addGlobalItem() { addCommunity("world"); }
+ void sortEntries();
//! Check whether or not a community id is currently managed.
bool communityExists(const QString &id) const
diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp
index f2777e66..0fad6624 100644
--- a/src/CommunitiesListItem.cpp
+++ b/src/CommunitiesListItem.cpp
@@ -19,6 +19,21 @@ CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent)
if (groupId_ == "world")
avatar_ = QPixmap(":/icons/icons/ui/world.png");
+ else if (groupId_ == "tag:m.favourite")
+ avatar_ = QPixmap(":/icons/icons/ui/star.png");
+ else if (groupId_ == "tag:m.lowpriority")
+ avatar_ = QPixmap(":/icons/icons/ui/lowprio.png");
+ else if (groupId_.startsWith("tag:"))
+ avatar_ = QPixmap(":/icons/icons/ui/tag.png");
+
+ updateTooltip();
+}
+
+void
+CommunitiesListItem::setName(QString name)
+{
+ name_ = name;
+ updateTooltip();
}
void
@@ -98,7 +113,8 @@ CommunitiesListItem::resolveName() const
{
if (!name_.isEmpty())
return name_;
-
+ if (groupId_.startsWith("tag:"))
+ return groupId_.right(groupId_.size() - strlen("tag:"));
if (!groupId_.startsWith("+"))
return QString("Group"); // Group with no name or id.
@@ -106,3 +122,24 @@ CommunitiesListItem::resolveName() const
auto firstPart = groupId_.split(':').at(0);
return firstPart.right(firstPart.size() - 1);
}
+
+void
+CommunitiesListItem::updateTooltip()
+{
+ if (groupId_ == "world")
+ setToolTip(tr("All rooms"));
+ else if (is_tag()) {
+ QString tag = groupId_.right(groupId_.size() - strlen("tag:"));
+ if (tag == "m.favourite")
+ setToolTip(tr("Favourite rooms"));
+ else if (tag == "m.lowpriority")
+ setToolTip(tr("Low priority rooms"));
+ else if (tag.startsWith("u."))
+ setToolTip(tag.right(tag.size() - 2) + tr(" (tag)"));
+ else
+ setToolTip(tag + tr(" (tag)"));
+ } else {
+ QString name = resolveName();
+ setToolTip(name + tr(" (community)"));
+ }
+}
\ No newline at end of file
diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
index bfd54661..d4d7e9c6 100644
--- a/src/CommunitiesListItem.h
+++ b/src/CommunitiesListItem.h
@@ -28,13 +28,17 @@ class CommunitiesListItem : public QWidget
public:
CommunitiesListItem(QString group_id, QWidget *parent = nullptr);
- void setName(QString name) { name_ = name; }
+ void setName(QString name);
bool isPressed() const { return isPressed_; }
void setAvatar(const QImage &img);
void setRooms(std::map room_ids) { room_ids_ = std::move(room_ids); }
+ void addRoom(const QString &id) { room_ids_[id] = true; }
+ void delRoom(const QString &id) { room_ids_.erase(id); }
std::map rooms() const { return room_ids_; }
+ bool is_tag() const { return groupId_.startsWith("tag:"); }
+
QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
QColor backgroundColor() const { return backgroundColor_; }
@@ -68,6 +72,7 @@ private:
const int IconSize = 36;
QString resolveName() const;
+ void updateTooltip();
std::map room_ids_;