From 18a98a7c1d712e39fbdc49c3a5d3a037e2fe1bad Mon Sep 17 00:00:00 2001 From: Victor Berger Date: Fri, 28 Sep 2018 14:40:51 +0200 Subject: [PATCH] Display tags as sorting items in the community panel (#401) --- resources/icons/ui/lowprio.png | Bin 0 -> 395 bytes resources/icons/ui/lowprio@2x.png | Bin 0 -> 779 bytes resources/icons/ui/star.png | Bin 0 -> 475 bytes resources/icons/ui/star@2x.png | Bin 0 -> 841 bytes resources/icons/ui/tag.png | Bin 0 -> 477 bytes resources/icons/ui/tag@2x.png | Bin 0 -> 1004 bytes resources/res.qrc | 7 ++ src/Cache.cpp | 52 ++++++++++++++ src/Cache.h | 13 ++++ src/ChatPage.cpp | 5 ++ src/ChatPage.h | 1 + src/CommunitiesList.cpp | 108 +++++++++++++++++++++++++++++- src/CommunitiesList.h | 5 ++ src/CommunitiesListItem.cpp | 39 ++++++++++- src/CommunitiesListItem.h | 7 +- 15 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 resources/icons/ui/lowprio.png create mode 100644 resources/icons/ui/lowprio@2x.png create mode 100644 resources/icons/ui/star.png create mode 100644 resources/icons/ui/star@2x.png create mode 100644 resources/icons/ui/tag.png create mode 100644 resources/icons/ui/tag@2x.png diff --git a/resources/icons/ui/lowprio.png b/resources/icons/ui/lowprio.png new file mode 100644 index 0000000000000000000000000000000000000000..b815d8bb46017f5871553ec0c43acfdb2169929f GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pI!Q<6tcl>NSkECU0hgr|#RNW|f@(;c~*97G)E&s#2h z#qmytz?M~hLasae)?H9byT0jgN$Z9cXT_ad{?!~m*~q_0{(f=W&(EyCEg1C|FnKBU zTYq4jb|7k9Y|f7-&+nN`bCI^0od!M8n za8=6o%bcI}Liyta4z}$cEbYHNWK`K+bH4ktQBl&>+pKF@iAe5GjXIxODwA$}SayWz n#*wz`k9Cf}6t;+n|HL}^|E#?~|M`9e1{;H?tDnm{r-UW|Teh4W literal 0 HcmV?d00001 diff --git a/resources/icons/ui/lowprio@2x.png b/resources/icons/ui/lowprio@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4581946e60692041a5ac7447bd1ebe38f3642323 GIT binary patch literal 779 zcmV+m1N8ifP)le zK~#9!<(oZk6EPTopPJ8sks>vM4h&I;N~jVN8xu0r>An^}asF0YLSQ%JA z>M!Wf2&pU?_$a6qB_jhZP{PnR(wkFqjdMPqKVHd_?=HIY^VqrAjt$MkW8fn&l+*_{ z$sn!(`wG06dMU$S29AJ#l7_$=;B7&|UJmfzfvf6z3ph?yLJ|H*&gobHZlow-7WiZ( z*uW`GEZ|Ro+rZyI$0Bn{Fc$F3((xG*Zdgk&7Vv|>;pdWI5`(vv zU=oD4l3)^rpIO34Krcb~^Ca{}0ycrQMB&RMtO+}P)sJpT|Jp}%)pNTZhte(e+`a+Y za`7E-m{`qEY6*v&{UHm`)-S<7fDP_W*_&Hn-@4+5mNOfc+GO&V%CY19o-LT%g+R2>03n!i!y3F209h)(*b@0 zr@-}!HUCvr$OqtF@q;es_*WQT_R^W71#}AcSaDSIrNd*F5}utSp-ey#{;3c6IwWkB zNSG#|2*2eM-lv2~0)l||C1G71uZITjL&B>m`C#CENSKs@g7+;UNO)fof`+eKLKN_I zNO&$O8u&USL3TC}pH0$-kS+5w+de1=CyGF9!H$p#pkzj9hfLxLqi6S?-UYvi+WWC~y<< zWQiy7Uf^3=f@OhnTOs5>bI9BTmK^JsAqdzKKLcYO_6(6#EeTH@`;RR9Hzfp)GszY@ zh?!0OcoN_ug#5R_V2+;A2V6_bFsFm}1qayGIaYmw4}r-5m$;(uhLf@9{kNG`2Si+btr0#MD%B9`MQ_@vp#g8|KtQ z2FTyS{0W!;utOvHqqhFL>d`>{q|HlWTQnp&(+$)@#=-p%3!sfgvslouN&)Sf$jef* zt_1otertMdEP+pAeNh(*HYs44X7X|-YE1%6Y+{tK2yYEdg{{D%6a=mT{V9ImX+r{z zCkT8D*cP(U#>A7OD($Kb33wSZ==(rR4O>@Y+A@B&aDL2y4=LE}j%jnF$pjoz401#) zr>UY9c&cdog2@CtQwVSjo4!SP{cHozCxt z9QR@l8yG)f9snP8IhBW=UNF8dU>eQ9T{Io40@$ZpZi+YvoYmoI2KHhalZc0ZNb<>T TvLeA(00000NkvXXu0mjfT&j11 literal 0 HcmV?d00001 diff --git a/resources/icons/ui/tag.png b/resources/icons/ui/tag.png new file mode 100644 index 0000000000000000000000000000000000000000..61ae6b83cb8c983bc04b42e385ca8dddd995d151 GIT binary patch literal 477 zcmV<30V4j1P)VGd000McNliru;t388IS1hBFn9m}0cuG^ zK~z}7?bf|Y9Z?ho;P3hew=^+I0t&(<#99*#SO!w~2(k19gn(cmg}y+_6h4E6K7c5a zMifPoA_#(l5iEodViO&kT?~VRa_%^3?t;Uaz4y#n>#W)HLkAr^4nAQIf3ShsI>`@_ z;VV94HR7c@$mfD=ypW%d{PU`a8ffBej5m0Ob^Iu~Cc{E8*^?-s7snXN<(4smO>Cis zcR4|4cu`!aD}lU;qXIb$;v3#$JM8|DtM}u&9RZDS>?elrsQ2JIMzM(Bg&jAeo_q_s z@e2bb^#knZ;;j$^Q!($PHeC~^(MVXuW1W9k zNfFQ&JIfNs4QAr_X^QP9ZIEX<+n-42_R|W;uToz!7Zs3ac$p$#ts?TV)JJZ&BJxJ6 zfXOz=TUC7RChNBSE=<>Hdl_CoX17-*VoVK~uG991HfGCzz$<(y`620`gCyJnTOjF( TqxqB900000NkvXXu0mjf$5zXY literal 0 HcmV?d00001 diff --git a/resources/icons/ui/tag@2x.png b/resources/icons/ui/tag@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6769b09e89aa7c25b703b5b6d438b20fedc0d7 GIT binary patch literal 1004 zcmVVGd000McNliru;t388I5*!bF_i!S19?eA zK~#9!?V8(57C{)sf0+jYqmzkdp30z1$nK1apo?yzpddvMy2(4Sx~YF6l8Pw1=_(>5 zi0G=IE-LeYK~QwE6LvDmf)vZscJaL*4a0ZX&-R<0)p=mp-PxJleP-Ty-)CkP92^`R z92^`LDi%fPTMK|qz;56mupXEI`hhN>8~8k5JE{`633w^e1ke`2j!lb=z<3(+YPv9= z$gcoK6>_7%5g<<~Qv#d^CY5r>=L7j1Ar>dq4=hhnz8Ux-WygVaQE!0N;?z7LR_CM6 zDW$ zDh&#jufFYQDcOP)--0Xr@cVA za{XAI^A(s?@)L!{w{mYD#E!V8BQjU|N2|_XZra73so{lE* z>SZEq;^jKE@OfAAzXKb>^7hmJNj{!hwPzOtue35ZwEOY=p)kldXBg`Iq@OYjUL?8S zt-Sx5l7Gy!*|%t>X(|oM`-?OwZCu;WhCto}-pe=KQu0ok*7jkM_ka)b9hW5JeFRpT z)b?W`lGmNxJ_&W4-`O_WP@V=LX65C<~$1piFwjRL@(9&HUSZ$X1-s$MWA?N@bNc8xF68 zh+wVg@gTK*J`qC=@GffFJ{yfNK!mh?wuZx8d_rFo%|4Z5%p^MW*d%g-43U0xLa&V? zCrGiBN-;jPS>yyMn+F$)*(XCaaFy6r^#pK+*w2qvA=^EXC!{^#8?n6v2L}fS2L}fS aEASUQ-+TLtu+0hp0000 literal 0 HcmV?d00001 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_;