diff --git a/src/Cache.cpp b/src/Cache.cpp
index 2178bbfb..144a2d9a 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -55,6 +55,10 @@ constexpr auto BATCH_SIZE = 100;
//! Format: room_id -> RoomInfo
constexpr auto ROOMS_DB("rooms");
constexpr auto INVITES_DB("invites");
+//! maps each room to its parent space (id->id)
+constexpr auto SPACES_PARENTS_DB("space_parents");
+//! maps each space to its current children (id->id)
+constexpr auto SPACES_CHILDREN_DB("space_children");
//! Information that must be kept between sync requests.
constexpr auto SYNC_STATE_DB("sync_state");
//! Read receipts per room/event.
@@ -237,12 +241,14 @@ Cache::setup()
env_.open(cacheDirectory_.toStdString().c_str());
}
- auto txn = lmdb::txn::begin(env_);
- syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
- roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
- invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
- readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
- notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+ auto txn = lmdb::txn::begin(env_);
+ syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
+ roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
+ spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
+ spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
+ invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
+ readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
+ notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
// Device management
devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
@@ -899,7 +905,9 @@ Cache::runMigrations()
std::reverse(oldMessages.events.begin(),
oldMessages.events.end());
// save messages using the new method
- saveTimelineMessages(txn, room_id, oldMessages);
+ auto eventsDb = getEventsDb(txn, room_id);
+ saveTimelineMessages(
+ txn, eventsDb, room_id, oldMessages);
}
// delete old messages db
@@ -1194,24 +1202,73 @@ Cache::saveState(const mtx::responses::Sync &res)
auto userKeyCacheDb = getUserKeysDb(txn);
+ std::set<std::string> spaces_with_updates;
+ std::set<std::string> rooms_with_space_updates;
+
// Save joined rooms
for (const auto &room : res.rooms.join) {
auto statesdb = getStatesDb(txn, room.first);
auto stateskeydb = getStatesKeyDb(txn, room.first);
auto membersdb = getMembersDb(txn, room.first);
-
- saveStateEvents(
- txn, statesdb, stateskeydb, membersdb, room.first, room.second.state.events);
- saveStateEvents(
- txn, statesdb, stateskeydb, membersdb, room.first, room.second.timeline.events);
-
- saveTimelineMessages(txn, room.first, room.second.timeline);
+ auto eventsDb = getEventsDb(txn, room.first);
+
+ saveStateEvents(txn,
+ statesdb,
+ stateskeydb,
+ membersdb,
+ eventsDb,
+ room.first,
+ room.second.state.events);
+ saveStateEvents(txn,
+ statesdb,
+ stateskeydb,
+ membersdb,
+ eventsDb,
+ room.first,
+ room.second.timeline.events);
+
+ saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
RoomInfo updatedInfo;
updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString();
updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
+ updatedInfo.is_space = getRoomIsSpace(txn, statesdb);
+
+ if (updatedInfo.is_space) {
+ bool space_updates = false;
+ for (const auto &e : room.second.state.events)
+ if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+ std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+ space_updates = true;
+ for (const auto &e : room.second.timeline.events)
+ if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+ std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+ space_updates = true;
+
+ if (space_updates)
+ spaces_with_updates.insert(room.first);
+ }
+
+ {
+ bool room_has_space_update = false;
+ for (const auto &e : room.second.state.events) {
+ if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+ spaces_with_updates.insert(se->state_key);
+ room_has_space_update = true;
+ }
+ }
+ for (const auto &e : room.second.timeline.events) {
+ if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+ spaces_with_updates.insert(se->state_key);
+ room_has_space_update = true;
+ }
+ }
+
+ if (room_has_space_update)
+ rooms_with_space_updates.insert(room.first);
+ }
bool has_new_tags = false;
// Process the account_data associated with this room
@@ -1291,6 +1348,8 @@ Cache::saveState(const mtx::responses::Sync &res)
removeLeftRooms(txn, res.rooms.leave);
+ updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
+
txn.commit();
std::map<QString, bool> readStatus;
@@ -1339,6 +1398,7 @@ Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::I
updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString();
updatedInfo.avatar_url =
getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+ updatedInfo.is_space = getInviteRoomIsSpace(txn, statesdb);
updatedInfo.is_invite = true;
invitesDb_.put(txn, room.first, json(updatedInfo).dump());
@@ -1427,27 +1487,6 @@ 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 (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) {
- hasUpdates = true;
- }
- }
-
- if (hasUpdates)
- rooms.emplace_back(room.first);
- }
-
- return rooms;
-}
-
RoomInfo
Cache::singleRoomInfo(const std::string &room_id)
{
@@ -1733,6 +1772,13 @@ Cache::relatedEvents(const std::string &room_id, const std::string &event_id)
return related_ids;
}
+size_t
+Cache::memberCount(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ return getMembersDb(txn, room_id).size(txn);
+}
+
QMap<QString, RoomInfo>
Cache::roomInfo(bool withInvites)
{
@@ -2337,6 +2383,29 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
return QString("1");
}
+bool
+Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb)
+{
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ std::string_view event;
+ bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+
+ if (res) {
+ try {
+ StateEvent<Create> msg = json::parse(event);
+
+ return msg.content.type == mtx::events::state::room_type::space;
+ } catch (const json::exception &e) {
+ nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
+ }
+ }
+
+ nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+ return false;
+}
+
std::optional<mtx::events::state::CanonicalAlias>
Cache::getRoomAliases(const std::string &roomid)
{
@@ -2464,6 +2533,27 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
return QString();
}
+bool
+Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db)
+{
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ std::string_view event;
+ bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+
+ if (res) {
+ try {
+ StrippedEvent<Create> msg = json::parse(event);
+ return msg.content.type == mtx::events::state::room_type::space;
+ } catch (const json::exception &e) {
+ nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
+ }
+ }
+
+ return false;
+}
+
std::vector<std::string>
Cache::joinedRooms()
{
@@ -2506,42 +2596,6 @@ Cache::getMember(const std::string &room_id, const std::string &user_id)
return std::nullopt;
}
-std::vector<RoomSearchResult>
-Cache::searchRooms(const std::string &query, std::uint8_t max_items)
-{
- std::multimap<int, std::pair<std::string, RoomInfo>> items;
-
- auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
- auto cursor = lmdb::cursor::open(txn, roomsDb_);
-
- std::string_view room_id, room_data;
- while (cursor.get(room_id, room_data, MDB_NEXT)) {
- RoomInfo tmp = json::parse(room_data);
-
- const int score = utils::levenshtein_distance(
- query, QString::fromStdString(tmp.name).toLower().toStdString());
- items.emplace(score, std::make_pair(room_id, tmp));
- }
-
- cursor.close();
-
- auto end = items.begin();
-
- if (items.size() >= max_items)
- std::advance(end, max_items);
- else if (items.size() > 0)
- std::advance(end, items.size());
-
- std::vector<RoomSearchResult> results;
- for (auto it = items.begin(); it != end; it++) {
- results.push_back(RoomSearchResult{it->second.first, it->second.second});
- }
-
- txn.commit();
-
- return results;
-}
-
std::vector<RoomMember>
Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
{
@@ -2600,11 +2654,12 @@ void
Cache::savePendingMessage(const std::string &room_id,
const mtx::events::collections::TimelineEvent &message)
{
- auto txn = lmdb::txn::begin(env_);
+ auto txn = lmdb::txn::begin(env_);
+ auto eventsDb = getEventsDb(txn, room_id);
mtx::responses::Timeline timeline;
timeline.events.push_back(message.data);
- saveTimelineMessages(txn, room_id, timeline);
+ saveTimelineMessages(txn, eventsDb, room_id, timeline);
auto pending = getPendingMessagesDb(txn, room_id);
@@ -2672,13 +2727,13 @@ Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id
void
Cache::saveTimelineMessages(lmdb::txn &txn,
+ lmdb::dbi &eventsDb,
const std::string &room_id,
const mtx::responses::Timeline &res)
{
if (res.events.empty())
return;
- auto eventsDb = getEventsDb(txn, room_id);
auto relationsDb = getRelationsDb(txn, room_id);
auto orderDb = getEventOrderDb(txn, room_id);
@@ -3203,6 +3258,147 @@ Cache::deleteOldData() noexcept
}
}
+void
+Cache::updateSpaces(lmdb::txn &txn,
+ const std::set<std::string> &spaces_with_updates,
+ std::set<std::string> rooms_with_updates)
+{
+ if (spaces_with_updates.empty() && rooms_with_updates.empty())
+ return;
+
+ for (const auto &space : spaces_with_updates) {
+ // delete old entries
+ {
+ auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+ bool first = true;
+ std::string_view sp = space, space_child = "";
+
+ if (cursor.get(sp, space_child, MDB_SET)) {
+ while (cursor.get(
+ sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+ first = false;
+ spacesParentsDb_.del(txn, space_child, space);
+ }
+ }
+ cursor.close();
+ spacesChildrenDb_.del(txn, space);
+ }
+
+ for (const auto &event :
+ getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) {
+ if (event.content.via.has_value() && event.state_key.size() > 3 &&
+ event.state_key.at(0) == '!') {
+ spacesChildrenDb_.put(txn, space, event.state_key);
+ spacesParentsDb_.put(txn, event.state_key, space);
+ }
+ }
+ }
+
+ const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels);
+
+ for (const auto &room : rooms_with_updates) {
+ for (const auto &event :
+ getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) {
+ if (event.content.via.has_value() && event.state_key.size() > 3 &&
+ event.state_key.at(0) == '!') {
+ const std::string &space = event.state_key;
+
+ auto pls =
+ getStateEvent<mtx::events::state::PowerLevels>(txn, space);
+
+ if (!pls)
+ continue;
+
+ if (pls->content.user_level(event.sender) >=
+ pls->content.state_level(space_event_type)) {
+ spacesChildrenDb_.put(txn, space, room);
+ spacesParentsDb_.put(txn, room, space);
+ }
+ }
+ }
+ }
+}
+
+QMap<QString, std::optional<RoomInfo>>
+Cache::spaces()
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+ QMap<QString, std::optional<RoomInfo>> ret;
+ {
+ auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+ bool first = true;
+ std::string_view space_id, space_child;
+ while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
+ first = false;
+
+ if (!space_child.empty()) {
+ std::string_view room_data;
+ if (roomsDb_.get(txn, space_id, room_data)) {
+ RoomInfo tmp = json::parse(std::move(room_data));
+ ret.insert(
+ QString::fromUtf8(space_id.data(), space_id.size()), tmp);
+ } else {
+ ret.insert(
+ QString::fromUtf8(space_id.data(), space_id.size()),
+ std::nullopt);
+ }
+ }
+ }
+ cursor.close();
+ }
+
+ return ret;
+}
+
+std::vector<std::string>
+Cache::getParentRoomIds(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+ std::vector<std::string> roomids;
+ {
+ auto cursor = lmdb::cursor::open(txn, spacesParentsDb_);
+ bool first = true;
+ std::string_view sp = room_id, space_parent;
+ if (cursor.get(sp, space_parent, MDB_SET)) {
+ while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+ first = false;
+
+ if (!space_parent.empty())
+ roomids.emplace_back(space_parent);
+ }
+ }
+ cursor.close();
+ }
+
+ return roomids;
+}
+
+std::vector<std::string>
+Cache::getChildRoomIds(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+ std::vector<std::string> roomids;
+ {
+ auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+ bool first = true;
+ std::string_view sp = room_id, space_child;
+ if (cursor.get(sp, space_child, MDB_SET)) {
+ while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+ first = false;
+
+ if (!space_child.empty())
+ roomids.emplace_back(space_child);
+ }
+ }
+ cursor.close();
+ }
+
+ return roomids;
+}
+
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{
@@ -3884,6 +4080,7 @@ to_json(json &j, const RoomInfo &info)
j["avatar_url"] = info.avatar_url;
j["version"] = info.version;
j["is_invite"] = info.is_invite;
+ j["is_space"] = info.is_space;
j["join_rule"] = info.join_rule;
j["guest_access"] = info.guest_access;
@@ -3903,6 +4100,7 @@ from_json(const json &j, RoomInfo &info)
info.version = j.value(
"version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
info.is_invite = j.at("is_invite");
+ info.is_space = j.value("is_space", false);
info.join_rule = j.at("join_rule");
info.guest_access = j.at("guest_access");
@@ -4158,12 +4356,6 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
}
-QString
-getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
-{
- return instance_->getRoomVersion(txn, statesdb);
-}
-
std::vector<RoomMember>
getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
{
@@ -4305,11 +4497,7 @@ roomsWithStateUpdates(const mtx::responses::Sync &res)
{
return instance_->roomsWithStateUpdates(res);
}
-std::vector<std::string>
-roomsWithTagUpdates(const mtx::responses::Sync &res)
-{
- return instance_->roomsWithTagUpdates(res);
-}
+
std::map<QString, RoomInfo>
getRoomInfo(const std::vector<std::string> &rooms)
{
@@ -4329,12 +4517,6 @@ calculateRoomReadStatus()
instance_->calculateRoomReadStatus();
}
-std::vector<RoomSearchResult>
-searchRooms(const std::string &query, std::uint8_t max_items)
-{
- return instance_->searchRooms(query, max_items);
-}
-
void
markSentNotification(const std::string &event_id)
{
diff --git a/src/Cache.h b/src/Cache.h
index 74ec9695..b0520f6b 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -79,9 +79,6 @@ getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
//! Retrieve the room avatar's url if any.
QString
getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-//! Retrieve the version of the room if any.
-QString
-getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
//! Retrieve member info from a room.
std::vector<RoomMember>
@@ -166,9 +163,6 @@ calculateRoomReadStatus(const std::string &room_id);
void
calculateRoomReadStatus();
-std::vector<RoomSearchResult>
-searchRooms(const std::string &query, std::uint8_t max_items = 5);
-
void
markSentNotification(const std::string &event_id);
//! Removes an event from the sent notifications.
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index f7d6f0e2..1d0f0d70 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -76,6 +76,8 @@ struct RoomInfo
std::string version;
//! Whether or not the room is an invite.
bool is_invite = false;
+ //! Wheter or not the room is a space
+ bool is_space = false;
//! Total number of members in the room.
size_t member_count = 0;
//! Who can access to the room.
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 669f1895..cfcf9c9e 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -72,6 +72,7 @@ public:
std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid);
QHash<QString, RoomInfo> invites();
std::optional<RoomInfo> invite(std::string_view roomid);
+ QMap<QString, std::optional<RoomInfo>> spaces();
//! Calculate & return the name of the room.
QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
@@ -84,6 +85,8 @@ public:
QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
//! Retrieve the version of the room if any.
QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
+ //! Retrieve if the room is a space
+ bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
//! Get a specific state event
template<typename T>
@@ -98,6 +101,7 @@ public:
std::vector<RoomMember> getMembers(const std::string &room_id,
std::size_t startIndex = 0,
std::size_t len = 30);
+ size_t memberCount(const std::string &room_id);
void saveState(const mtx::responses::Sync &res);
bool isInitialized();
@@ -146,7 +150,6 @@ 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);
//! Calculates which the read status of a room.
@@ -154,9 +157,6 @@ public:
bool calculateRoomReadStatus(const std::string &room_id);
void calculateRoomReadStatus();
- std::vector<RoomSearchResult> searchRooms(const std::string &query,
- std::uint8_t max_items = 5);
-
void markSentNotification(const std::string &event_id);
//! Removes an event from the sent notifications.
void removeReadNotification(const std::string &event_id);
@@ -222,6 +222,8 @@ public:
void deleteOldData() noexcept;
//! Retrieve all saved room ids.
std::vector<std::string> getRoomIds(lmdb::txn &txn);
+ std::vector<std::string> getParentRoomIds(const std::string &room_id);
+ std::vector<std::string> getChildRoomIds(const std::string &room_id);
//! Mark a room that uses e2e encryption.
void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
@@ -327,12 +329,14 @@ private:
QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+ bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id);
std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id);
void saveTimelineMessages(lmdb::txn &txn,
+ lmdb::dbi &eventsDb,
const std::string &room_id,
const mtx::responses::Timeline &res);
@@ -351,11 +355,12 @@ private:
lmdb::dbi &statesdb,
lmdb::dbi &stateskeydb,
lmdb::dbi &membersdb,
+ lmdb::dbi &eventsDb,
const std::string &room_id,
const std::vector<T> &events)
{
for (const auto &e : events)
- saveStateEvent(txn, statesdb, stateskeydb, membersdb, room_id, e);
+ saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
}
template<class T>
@@ -363,6 +368,7 @@ private:
lmdb::dbi &statesdb,
lmdb::dbi &stateskeydb,
lmdb::dbi &membersdb,
+ lmdb::dbi &eventsDb,
const std::string &room_id,
const T &event)
{
@@ -399,8 +405,10 @@ private:
}
std::visit(
- [&txn, &statesdb, &stateskeydb](auto e) {
- if constexpr (isStateEvent(e))
+ [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
+ if constexpr (isStateEvent(e)) {
+ eventsDb.put(txn, e.event_id, json(e).dump());
+
if (e.type != EventType::Unsupported) {
if (e.state_key.empty())
statesdb.put(
@@ -415,6 +423,7 @@ private:
})
.dump());
}
+ }
},
event);
}
@@ -430,20 +439,22 @@ private:
if (room_id.empty())
return std::nullopt;
+ const auto typeStr = to_string(type);
std::string_view value;
if (state_key.empty()) {
auto db = getStatesDb(txn, room_id);
- if (!db.get(txn, to_string(type), value)) {
+ if (!db.get(txn, typeStr, value)) {
return std::nullopt;
}
} else {
- auto db = getStatesKeyDb(txn, room_id);
- std::string d = json::object({{"key", state_key}}).dump();
- std::string_view data = d;
+ auto db = getStatesKeyDb(txn, room_id);
+ std::string d = json::object({{"key", state_key}}).dump();
+ std::string_view data = d;
+ std::string_view typeStrV = typeStr;
auto cursor = lmdb::cursor::open(txn, db);
- if (!cursor.get(state_key, data, MDB_GET_BOTH))
+ if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
return std::nullopt;
try {
@@ -463,6 +474,47 @@ private:
}
}
+ template<typename T>
+ std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn,
+ const std::string &room_id)
+
+ {
+ constexpr auto type = mtx::events::state_content_to_type<T>;
+ static_assert(type != mtx::events::EventType::Unsupported,
+ "Not a supported type in state events.");
+
+ if (room_id.empty())
+ return {};
+
+ std::vector<mtx::events::StateEvent<T>> events;
+
+ {
+ auto db = getStatesKeyDb(txn, room_id);
+ auto eventsDb = getEventsDb(txn, room_id);
+ const auto typeStr = to_string(type);
+ std::string_view typeStrV = typeStr;
+ std::string_view data;
+ std::string_view value;
+
+ auto cursor = lmdb::cursor::open(txn, db);
+ bool first = true;
+ if (cursor.get(typeStrV, data, MDB_SET)) {
+ while (cursor.get(
+ typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+ first = false;
+
+ if (eventsDb.get(txn,
+ json::parse(data)["id"].get<std::string>(),
+ value))
+ events.push_back(
+ json::parse(value)
+ .get<mtx::events::StateEvent<T>>());
+ }
+ }
+ }
+
+ return events;
+ }
void saveInvites(lmdb::txn &txn,
const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
@@ -482,6 +534,10 @@ private:
}
}
+ void updateSpaces(lmdb::txn &txn,
+ const std::set<std::string> &spaces_with_updates,
+ std::set<std::string> rooms_with_updates);
+
lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
@@ -548,8 +604,8 @@ private:
lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
{
- auto db =
- lmdb::dbi::open(txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE);
+ auto db = lmdb::dbi::open(
+ txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
lmdb::dbi_set_dupsort(txn, db, compare_state_key);
return db;
}
@@ -611,6 +667,7 @@ private:
lmdb::env env_;
lmdb::dbi syncStateDb_;
lmdb::dbi roomsDb_;
+ lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
lmdb::dbi invitesDb_;
lmdb::dbi readReceiptsDb_;
lmdb::dbi notificationsDb_;
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 9b906555..6c0d8728 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -613,6 +613,7 @@ UserSettings::save()
settings.setValue("mobile_mode", mobileMode_);
settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", typingNotifications_);
+ settings.setValue("sort_by_unread", sortByImportance_);
settings.setValue("minor_events", sortByImportance_);
settings.setValue("read_receipts", readReceipts_);
settings.setValue("group_view", groupView_);
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 6c236784..97bfa76d 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -44,8 +44,23 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
case CommunitiesModel::Roles::Id:
return "";
}
- } else if (index.row() - 1 < tags_.size()) {
- auto tag = tags_.at(index.row() - 1);
+ } else if (index.row() - 1 < spaceOrder_.size()) {
+ auto id = spaceOrder_.at(index.row() - 1);
+ 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::Hidden:
+ return hiddentTagIds_.contains("space:" + id);
+ case CommunitiesModel::Roles::Id:
+ return "space:" + id;
+ }
+ } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) {
+ auto tag = tags_.at(index.row() - 1 - spaceOrder_.size());
if (tag == "m.favourite") {
switch (role) {
case CommunitiesModel::Roles::AvatarUrl:
@@ -78,7 +93,6 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
case CommunitiesModel::Roles::AvatarUrl:
return QString(":/icons/icons/ui/tag.png");
case CommunitiesModel::Roles::DisplayName:
- return tag.mid(2);
case CommunitiesModel::Roles::Tooltip:
return tag.mid(2);
}
@@ -99,17 +113,27 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
void
CommunitiesModel::initializeSidebar()
{
+ beginResetModel();
+ tags_.clear();
+ spaceOrder_.clear();
+ spaces_.clear();
+
std::set<std::string> ts;
- for (const auto &e : cache::roomInfo()) {
- for (const auto &t : e.tags) {
- if (t.find("u.") == 0 || t.find("m." == 0)) {
- ts.insert(t);
+ std::vector<RoomInfo> tempSpaces;
+ 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();
+ } else {
+ for (const auto &t : it.value().tags) {
+ if (t.find("u.") == 0 || t.find("m." == 0)) {
+ ts.insert(t);
+ }
}
}
}
- beginResetModel();
- tags_.clear();
for (const auto &t : ts)
tags_.push_back(QString::fromStdString(t));
@@ -143,6 +167,25 @@ CommunitiesModel::sync(const mtx::responses::Rooms &rooms)
mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
tagsUpdated = true;
}
+ for (const auto &e : room.state.events)
+ if (std::holds_alternative<
+ mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
+ std::holds_alternative<
+ mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
+ tagsUpdated = true;
+ }
+ for (const auto &e : room.timeline.events)
+ if (std::holds_alternative<
+ mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
+ std::holds_alternative<
+ mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
+ tagsUpdated = true;
+ }
+ }
+ for (const auto &[roomid, room] : rooms.leave) {
+ (void)room;
+ if (spaceOrder_.contains(QString::fromStdString(roomid)))
+ tagsUpdated = true;
}
if (tagsUpdated)
@@ -161,6 +204,15 @@ CommunitiesModel::setCurrentTagId(QString tagId)
return;
}
}
+ } else if (tagId.startsWith("space:")) {
+ auto tag = tagId.mid(6);
+ for (const auto &t : spaceOrder_) {
+ if (t == tag) {
+ this->currentTagId_ = tagId;
+ emit currentTagIdChanged(currentTagId_);
+ return;
+ }
+ }
}
this->currentTagId_ = "";
@@ -181,7 +233,13 @@ CommunitiesModel::toggleTagId(QString tagId)
if (tagId.startsWith("tag:")) {
auto idx = tags_.indexOf(tagId.mid(4));
if (idx != -1)
- emit dataChanged(index(idx), index(idx), {Hidden});
+ emit dataChanged(index(idx + 1 + spaceOrder_.size()),
+ index(idx + 1 + spaceOrder_.size()),
+ {Hidden});
+ } else if (tagId.startsWith("space:")) {
+ auto idx = spaceOrder_.indexOf(tagId.mid(6));
+ if (idx != -1)
+ emit dataChanged(index(idx + 1), index(idx + 1), {Hidden});
}
emit hiddenTagsChanged();
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 66d6b21b..8c40ec5b 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -11,6 +11,8 @@
#include <mtx/responses/sync.hpp>
+#include "CacheStructs.h"
+
class CommunitiesModel : public QAbstractListModel
{
Q_OBJECT
@@ -71,4 +73,6 @@ private:
QStringList tags_;
QString currentTagId_;
QStringList hiddentTagIds_;
+ QStringList spaceOrder_;
+ std::map<QString, RoomInfo> spaces_;
};
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 04f7ef76..9a91ff79 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -675,6 +675,9 @@ EventStore::decryptEvent(const IdIndex &idx,
index.room_id,
index.session_id,
e.sender);
+ // we may not want to request keys during initial sync and such
+ if (suppressKeyRequests)
+ break;
// TODO: Check if this actually works and look in key backup
auto copy = e;
copy.room_id = room_id_;
@@ -816,6 +819,18 @@ EventStore::decryptEvent(const IdIndex &idx,
return asCacheEntry(std::move(decryptionResult.event.value()));
}
+void
+EventStore::enableKeyRequests(bool suppressKeyRequests_)
+{
+ if (!suppressKeyRequests_) {
+ for (const auto &key : decryptedEvents_.keys())
+ if (key.room == this->room_id_)
+ decryptedEvents_.remove(key);
+ suppressKeyRequests = false;
+ } else
+ suppressKeyRequests = true;
+}
+
mtx::events::collections::TimelineEvents *
EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits)
{
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index d9bb86cb..7c404102 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -115,6 +115,7 @@ public slots:
void addPending(mtx::events::collections::TimelineEvents event);
void receivedSessionKey(const std::string &session_id);
void clearTimeline();
+ void enableKeyRequests(bool suppressKeyRequests_);
private:
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
@@ -142,4 +143,5 @@ private:
std::string current_txn;
int current_txn_error_count = 0;
bool noMoreMessages = false;
+ bool suppressKeyRequests = true;
};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 0f980c6c..7f59b112 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -51,6 +51,7 @@ RoomlistModel::roleNames() const
{IsInvite, "isInvite"},
{IsSpace, "isSpace"},
{Tags, "tags"},
+ {ParentSpaces, "parentSpaces"},
};
}
@@ -84,8 +85,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const
case Roles::NotificationCount:
return room->notificationCount();
case Roles::IsInvite:
- case Roles::IsSpace:
return false;
+ case Roles::IsSpace:
+ return room->isSpace();
case Roles::Tags: {
auto info = cache::singleRoomInfo(roomid.toStdString());
QStringList list;
@@ -93,6 +95,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const
list.push_back(QString::fromStdString(t));
return list;
}
+ case Roles::ParentSpaces: {
+ auto parents =
+ cache::client()->getParentRoomIds(roomid.toStdString());
+ QStringList list;
+ for (const auto &t : parents)
+ list.push_back(QString::fromStdString(t));
+ return list;
+ }
default:
return {};
}
@@ -122,6 +132,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const
return false;
case Roles::Tags:
return QStringList();
+ case Roles::ParentSpaces: {
+ auto parents =
+ cache::client()->getParentRoomIds(roomid.toStdString());
+ QStringList list;
+ for (const auto &t : parents)
+ list.push_back(QString::fromStdString(t));
+ return list;
+ }
default:
return {};
}
@@ -412,7 +430,9 @@ enum NotificationImportance : short
AllEventsRead = 0,
NewMessage = 1,
NewMentions = 2,
- Invite = 3
+ Invite = 3,
+ SubSpace = 4,
+ CurrentSpace = 5,
};
}
@@ -422,7 +442,13 @@ FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const
// Returns the degree of importance of the unread messages in the room.
// If sorting by importance is disabled in settings, this only ever
// returns ImportanceDisabled or Invite
- if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
+ if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) {
+ if (filterType == FilterBy::Space &&
+ filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString())
+ return CurrentSpace;
+ else
+ return SubSpace;
+ } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
return Invite;
} else if (!this->sortByImportance) {
return ImportanceDisabled;
@@ -505,6 +531,12 @@ bool
FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
{
if (filterType == FilterBy::Nothing) {
+ if (sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+ .toBool()) {
+ return false;
+ }
+
if (!hiddenTags.empty()) {
auto tags =
sourceModel()
@@ -516,19 +548,86 @@ FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) cons
return false;
}
+ if (!hiddenSpaces.empty()) {
+ auto parents =
+ sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+ .toStringList();
+ for (const auto &t : parents)
+ if (hiddenSpaces.contains(t))
+ return false;
+ }
+
return true;
} else if (filterType == FilterBy::Tag) {
+ if (sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+ .toBool()) {
+ return false;
+ }
+
auto tags = sourceModel()
->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
.toStringList();
if (!tags.contains(filterStr))
return false;
- else if (!hiddenTags.empty()) {
+
+ if (!hiddenTags.empty()) {
for (const auto &t : tags)
if (t != filterStr && hiddenTags.contains(t))
return false;
}
+
+ if (!hiddenSpaces.empty()) {
+ auto parents =
+ sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+ .toStringList();
+ for (const auto &t : parents)
+ if (hiddenSpaces.contains(t))
+ return false;
+ }
+
+ return true;
+ } else if (filterType == FilterBy::Space) {
+ if (filterStr == sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId)
+ .toString())
+ return true;
+
+ auto parents =
+ sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+ .toStringList();
+
+ if (!parents.contains(filterStr))
+ return false;
+
+ if (!hiddenTags.empty()) {
+ auto tags =
+ sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+ .toStringList();
+
+ for (const auto &t : tags)
+ if (hiddenTags.contains(t))
+ return false;
+ }
+
+ if (!hiddenSpaces.empty()) {
+ for (const auto &t : parents)
+ if (hiddenSpaces.contains(t))
+ return false;
+ }
+
+ if (sourceModel()
+ ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+ .toBool() &&
+ !parents.contains(filterStr)) {
+ return false;
+ }
+
return true;
} else {
return true;
@@ -582,7 +681,7 @@ FilteredRoomlistModel::previousRoom()
if (r) {
int idx = roomidToIndex(r->roomId());
idx--;
- if (idx > 0) {
+ if (idx >= 0) {
setCurrentRoom(
data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index b0244886..d6cbb462 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -38,6 +38,7 @@ public:
IsInvite,
IsSpace,
Tags,
+ ParentSpaces,
};
RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -134,6 +135,9 @@ public slots:
if (tagId.startsWith("tag:")) {
filterType = FilterBy::Tag;
filterStr = tagId.mid(4);
+ } else if (tagId.startsWith("space:")) {
+ filterType = FilterBy::Space;
+ filterStr = tagId.mid(6);
} else {
filterType = FilterBy::Nothing;
filterStr.clear();
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 99547b15..067f219a 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -320,6 +320,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
{
lastMessage_.timestamp = 0;
+ if (auto create =
+ cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString()))
+ this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
+
connect(
this,
&TimelineModel::redactionFailed,
@@ -375,6 +379,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
this->updateFlowEventId(event_id);
});
+
// When a message is sent, check if the current edit/reply relates to that message,
// and update the event_id so that it points to the sent message and not the pending one.
connect(&events,
@@ -391,6 +396,11 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
}
});
+ connect(manager_,
+ &TimelineViewManager::initialSyncChanged,
+ &events,
+ &EventStore::enableKeyRequests);
+
showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
}
@@ -770,6 +780,7 @@ TimelineModel::syncState(const mtx::responses::State &s)
} else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
+ emit roomMemberCountChanged();
}
}
}
@@ -826,6 +837,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
} else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
+ emit roomMemberCountChanged();
}
}
updateLastMessage();
@@ -1931,3 +1943,9 @@ TimelineModel::roomTopic() const
return utils::replaceEmoji(utils::linkifyMessage(
QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
}
+
+int
+TimelineModel::roomMemberCount() const
+{
+ return (int)cache::client()->memberCount(room_id_.toStdString());
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3ebbe120..3392d474 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -161,6 +161,8 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+ Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
+ Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(InputBar *input READ input CONSTANT)
Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
@@ -262,6 +264,8 @@ public:
RelatedInfo relatedInfo(QString id);
DescInfo lastMessage() const { return lastMessage_; }
+ bool isSpace() const { return isSpace_; }
+ int roomMemberCount() const;
public slots:
void setCurrentIndex(int index);
@@ -348,6 +352,7 @@ signals:
void roomNameChanged();
void roomTopicChanged();
void roomAvatarUrlChanged();
+ void roomMemberCountChanged();
void permissionsChanged();
void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
@@ -366,9 +371,6 @@ private:
QString room_id_;
- bool decryptDescription = true;
- bool m_paginationInProgress = false;
-
QString currentId, currentReadId;
QString reply_, edit_;
QString textBeforeEdit, replyBeforeEdit;
@@ -388,6 +390,10 @@ private:
friend struct SendMessageVisitor;
int notification_count = 0, highlight_count = 0;
+
+ bool decryptDescription = true;
+ bool m_paginationInProgress = false;
+ bool isSpace_ = false;
};
template<class T>
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 26119393..732a0443 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -47,7 +47,7 @@ Theme::paletteFromTheme(std::string_view theme)
darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
- darkActive.setColor(QPalette::ButtonText, "#727274");
+ darkActive.setColor(QPalette::ButtonText, "#828284");
return darkActive;
} else {
return original;
|