summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/icons/ui/lowprio.pngbin0 -> 395 bytes
-rw-r--r--resources/icons/ui/lowprio@2x.pngbin0 -> 779 bytes
-rw-r--r--resources/icons/ui/star.pngbin0 -> 475 bytes
-rw-r--r--resources/icons/ui/star@2x.pngbin0 -> 841 bytes
-rw-r--r--resources/icons/ui/tag.pngbin0 -> 477 bytes
-rw-r--r--resources/icons/ui/tag@2x.pngbin0 -> 1004 bytes
-rw-r--r--resources/res.qrc7
-rw-r--r--src/Cache.cpp52
-rw-r--r--src/Cache.h13
-rw-r--r--src/ChatPage.cpp5
-rw-r--r--src/ChatPage.h1
-rw-r--r--src/CommunitiesList.cpp108
-rw-r--r--src/CommunitiesList.h5
-rw-r--r--src/CommunitiesListItem.cpp39
-rw-r--r--src/CommunitiesListItem.h7
15 files changed, 234 insertions, 3 deletions
diff --git a/resources/icons/ui/lowprio.png b/resources/icons/ui/lowprio.png
new file mode 100644
index 00000000..b815d8bb
--- /dev/null
+++ b/resources/icons/ui/lowprio.png
Binary files differdiff --git a/resources/icons/ui/lowprio@2x.png b/resources/icons/ui/lowprio@2x.png
new file mode 100644
index 00000000..4581946e
--- /dev/null
+++ b/resources/icons/ui/lowprio@2x.png
Binary files differdiff --git a/resources/icons/ui/star.png b/resources/icons/ui/star.png
new file mode 100644
index 00000000..f2c73243
--- /dev/null
+++ b/resources/icons/ui/star.png
Binary files differdiff --git a/resources/icons/ui/star@2x.png b/resources/icons/ui/star@2x.png
new file mode 100644
index 00000000..0cde94d8
--- /dev/null
+++ b/resources/icons/ui/star@2x.png
Binary files differdiff --git a/resources/icons/ui/tag.png b/resources/icons/ui/tag.png
new file mode 100644
index 00000000..61ae6b83
--- /dev/null
+++ b/resources/icons/ui/tag.png
Binary files differdiff --git a/resources/icons/ui/tag@2x.png b/resources/icons/ui/tag@2x.png
new file mode 100644
index 00000000..5a6769b0
--- /dev/null
+++ b/resources/icons/ui/tag@2x.png
Binary files differdiff --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 @@
         <file>icons/ui/world.png</file>
         <file>icons/ui/world@2x.png</file>
 
+        <file>icons/ui/tag.png</file>
+        <file>icons/ui/tag@2x.png</file>
+        <file>icons/ui/star.png</file>
+        <file>icons/ui/star@2x.png</file>
+        <file>icons/ui/lowprio.png</file>
+        <file>icons/ui/lowprio@2x.png</file>
+
         <file>icons/ui/edit.png</file>
         <file>icons/ui/edit@2x.png</file>
 
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<account_data::Tag>)) {
+                                auto tags_evt = boost::get<Event<account_data::Tag>>(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<std::string>
+Cache::roomsWithTagUpdates(const mtx::responses::Sync &res)
+{
+        using namespace mtx::events;
+
+        std::vector<std::string> 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<account_data::Tag>)) {
+                                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<std::string> 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<std::vector<std::string>>();
 }
 
 //! Basic information per member;
@@ -384,11 +392,16 @@ public:
 
         RoomInfo singleRoomInfo(const std::string &room_id);
         std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
+        std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res);
         std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);
         std::map<QString, RoomInfo> roomUpdates(const mtx::responses::Sync &sync)
         {
                 return getRoomInfo(roomsWithStateUpdates(sync));
         }
+        std::map<QString, RoomInfo> 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> 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<QString, RoomInfo> &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<QString, mtx::responses::Timeline> &msgs);
         void syncUI(const mtx::responses::Rooms &rooms);
         void syncRoomlist(const std::map<QString, RoomInfo> &updates);
+        void syncTags(const std::map<QString, RoomInfo> &updates);
         void syncTopBar(const std::map<QString, RoomInfo> &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<QString, RoomInfo> &info)
+{
+        for (const auto &room : info)
+                setTagsForRoom(room.first, room.second.tags);
+        sortEntries();
+}
+
+void
+CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector<std::string> &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<CommunitiesListItem *> header;
+        std::vector<CommunitiesListItem *> communities;
+        std::vector<CommunitiesListItem *> tags;
+        std::vector<CommunitiesListItem *> 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 <QSharedPointer>
 #include <QVBoxLayout>
 
+#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<QString, bool> roomList(const QString &id) const;
 
+        void syncTags(const std::map<QString, RoomInfo> &info);
+        void setTagsForRoom(const QString &id, const std::vector<std::string> &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<QString, bool> 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<QString, bool> 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<QString, bool> room_ids_;