summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-07-08 21:15:50 -0400
committerJoseph Donofry <joedonofry@gmail.com>2021-07-08 21:15:50 -0400
commit1d204ce94c8b678442cccde87a9d8a670b30fe18 (patch)
treed8ec34fd579307dc35f33fc6780fdc923cae090e /src
parentAdd nheko logo spinner to relevant places in UI (diff)
parentFix cmake template define issue (diff)
downloadnheko-1d204ce94c8b678442cccde87a9d8a670b30fe18.tar.xz
Merge remote-tracking branch 'origin/master' into nheko_loading_spinner
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp502
-rw-r--r--src/Cache.h6
-rw-r--r--src/CacheStructs.h4
-rw-r--r--src/Cache_p.h89
-rw-r--r--src/ChatPage.cpp6
-rw-r--r--src/LoginPage.cpp13
-rw-r--r--src/Olm.cpp9
-rw-r--r--src/Olm.h2
-rw-r--r--src/RegisterPage.cpp145
-rw-r--r--src/RegisterPage.h6
-rw-r--r--src/UserSettingsPage.cpp37
-rw-r--r--src/UserSettingsPage.h12
-rw-r--r--src/Utils.cpp7
-rw-r--r--src/Utils.h4
-rw-r--r--src/WebRTCSession.cpp3
-rw-r--r--src/main.cpp39
-rw-r--r--src/timeline/CommunitiesModel.cpp118
-rw-r--r--src/timeline/CommunitiesModel.h24
-rw-r--r--src/timeline/EventStore.cpp62
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/InputBar.cpp2
-rw-r--r--src/timeline/RoomlistModel.cpp364
-rw-r--r--src/timeline/RoomlistModel.h35
-rw-r--r--src/timeline/TimelineModel.cpp49
-rw-r--r--src/timeline/TimelineModel.h12
-rw-r--r--src/timeline/TimelineViewManager.cpp36
-rw-r--r--src/ui/RoomSettings.cpp2
-rw-r--r--src/ui/Theme.cpp2
-rw-r--r--src/ui/UserProfile.cpp8
29 files changed, 1257 insertions, 343 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 4a99dd59..8146e2cc 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); + auto eventsDb = getEventsDb(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); + 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, room.first, room.second.timeline); + 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) { @@ -1681,6 +1720,27 @@ Cache::storeEvent(const std::string &room_id, txn.commit(); } +void +Cache::replaceEvent(const std::string &room_id, + const std::string &event_id, + const mtx::events::collections::TimelineEvent &event) +{ + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); + auto event_json = mtx::accessors::serialize_event(event.data).dump(); + + { + eventsDb.del(txn, event_id); + eventsDb.put(txn, event_id, event_json); + for (auto relation : mtx::accessors::relations(event.data).relations) { + relationsDb.put(txn, relation.event_id, event_id); + } + } + + txn.commit(); +} + std::vector<std::string> Cache::relatedEvents(const std::string &room_id, const std::string &event_id) { @@ -1712,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) { @@ -1727,8 +1794,6 @@ Cache::roomInfo(bool withInvites) while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { RoomInfo tmp = json::parse(std::move(room_data)); tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn); - tmp.msgInfo = getLastMessageInfo(txn, std::string(room_id)); - result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); } roomsCursor.close(); @@ -1955,96 +2020,6 @@ Cache::getTimelineEventId(const std::string &room_id, uint64_t index) return std::string(val); } -DescInfo -Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) -{ - lmdb::dbi orderDb; - try { - orderDb = getOrderToMessageDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } - - lmdb::dbi eventsDb; - try { - eventsDb = getEventsDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } - - lmdb::dbi membersdb; - try { - membersdb = getMembersDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } - - if (orderDb.size(txn) == 0) - return DescInfo{}; - - const auto local_user = utils::localUser(); - - DescInfo fallbackDesc{}; - - std::string_view indexVal, event_id; - - auto cursor = lmdb::cursor::open(txn, orderDb); - bool first = true; - while (cursor.get(indexVal, event_id, first ? MDB_LAST : MDB_PREV)) { - first = false; - - std::string_view event; - bool success = eventsDb.get(txn, event_id, event); - if (!success) - continue; - - auto obj = json::parse(event); - - if (fallbackDesc.event_id.isEmpty() && obj["type"] == "m.room.member" && - obj["state_key"] == local_user.toStdString() && - obj["content"]["membership"] == "join") { - uint64_t ts = obj["origin_server_ts"]; - auto time = QDateTime::fromMSecsSinceEpoch(ts); - fallbackDesc = DescInfo{QString::fromStdString(obj["event_id"]), - local_user, - tr("You joined this room."), - utils::descriptiveTime(time), - ts, - time}; - } - - if (!(obj["type"] == "m.room.message" || obj["type"] == "m.sticker" || - obj["type"] == "m.call.invite" || obj["type"] == "m.call.answer" || - obj["type"] == "m.call.hangup" || obj["type"] == "m.room.encrypted")) - continue; - - mtx::events::collections::TimelineEvent te; - mtx::events::collections::from_json(obj, te); - - std::string_view info; - MemberInfo m; - if (membersdb.get(txn, obj["sender"].get<std::string>(), info)) { - m = json::parse(std::string_view(info.data(), info.size())); - } - - cursor.close(); - return utils::getMessageDescription( - te.data, local_user, QString::fromStdString(m.name)); - } - cursor.close(); - - return fallbackDesc; -} - QHash<QString, RoomInfo> Cache::invites() { @@ -2316,6 +2291,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) { @@ -2443,6 +2441,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() { @@ -2479,47 +2498,12 @@ Cache::getMember(const std::string &room_id, const std::string &user_id) return m; } } catch (std::exception &e) { - nhlog::db()->warn("Failed to read member ({}): {}", user_id, e.what()); + nhlog::db()->warn( + "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); } 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) { @@ -2578,11 +2562,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); @@ -2650,13 +2635,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); @@ -3181,6 +3166,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) { @@ -3451,6 +3577,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query if (!updateToWrite.master_keys.keys.empty() && update.master_keys.keys != updateToWrite.master_keys.keys) { + nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", + user, + updateToWrite.master_keys.keys.size(), + update.master_keys.keys.size()); updateToWrite.master_key_changed = true; } @@ -3458,25 +3588,31 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query updateToWrite.self_signing_keys = update.self_signing_keys; updateToWrite.user_signing_keys = update.user_signing_keys; - // If we have keys for the device already, only update the signatures. + auto oldDeviceKeys = std::move(updateToWrite.device_keys); + updateToWrite.device_keys.clear(); + + // Don't insert keys, which we have seen once already for (const auto &[device_id, device_keys] : update.device_keys) { - if (updateToWrite.device_keys.count(device_id) && - updateToWrite.device_keys.at(device_id).keys == - device_keys.keys) { - updateToWrite.device_keys.at(device_id).signatures = - device_keys.signatures; + if (oldDeviceKeys.count(device_id) && + oldDeviceKeys.at(device_id).keys == device_keys.keys) { + // this is safe, since the keys are the same + updateToWrite.device_keys[device_id] = device_keys; } else { bool keyReused = false; for (const auto &[key_id, key] : device_keys.keys) { (void)key_id; if (updateToWrite.seen_device_keys.count(key)) { + nhlog::crypto()->warn( + "Key '{}' reused by ({}: {})", + key, + user, + device_id); keyReused = true; break; } } - if (!updateToWrite.device_keys.count(device_id) && - !keyReused) + if (!keyReused && !oldDeviceKeys.count(device_id)) updateToWrite.device_keys[device_id] = device_keys; } @@ -3858,6 +3994,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; @@ -3877,6 +4014,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"); @@ -4132,12 +4270,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) { @@ -4279,11 +4411,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) { @@ -4303,12 +4431,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..28c70055 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h
@@ -76,13 +76,13 @@ 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. mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; 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; }; diff --git a/src/Cache_p.h b/src/Cache_p.h
index f2911622..c76cc717 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); @@ -184,6 +184,9 @@ public: void storeEvent(const std::string &room_id, const std::string &event_id, const mtx::events::collections::TimelineEvent &event); + void replaceEvent(const std::string &room_id, + const std::string &event_id, + const mtx::events::collections::TimelineEvent &event); std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id); @@ -219,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); @@ -324,12 +329,13 @@ 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); @@ -348,11 +354,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> @@ -360,6 +367,7 @@ private: lmdb::dbi &statesdb, lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, + lmdb::dbi &eventsDb, const std::string &room_id, const T &event) { @@ -396,8 +404,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( @@ -412,6 +422,7 @@ private: }) .dump()); } + } }, event); } @@ -427,20 +438,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 { @@ -460,6 +473,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); @@ -479,6 +533,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); @@ -545,8 +603,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; } @@ -608,6 +666,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/ChatPage.cpp b/src/ChatPage.cpp
index 0f16f205..6003eb85 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -550,7 +550,7 @@ ChatPage::startInitialSync() nhlog::net()->error("initial sync error: {} {} {} {}", err->parse_error, status_code, - err->error_code.message(), + err->error_code, err_code); // non http related errors @@ -674,10 +674,10 @@ ChatPage::trySync() return; } - nhlog::net()->error("initial sync error: {} {} {} {}", + nhlog::net()->error("sync error: {} {} {} {}", err->parse_error, status_code, - err->error_code.message(), + err->error_code, err_code); emit tryDelayedSyncCb(); return; diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index c914d66f..f53d81ba 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp
@@ -263,9 +263,7 @@ LoginPage::onMatrixIdEntered() http::client()->well_known([this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { if (err) { - using namespace boost::beast::http; - - if (err->status_code == status::not_found) { + if (err->status_code == 404) { nhlog::net()->info("Autodiscovery: No .well-known."); checkHomeserverVersion(); return; @@ -282,8 +280,9 @@ LoginPage::onMatrixIdEntered() emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " "requesting .well-known.")); nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {}", - err->error_code.message()); + "requesting .well-known. {} {}", + err->status_code, + err->error_code); return; } @@ -301,9 +300,7 @@ LoginPage::checkHomeserverVersion() http::client()->versions( [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { if (err) { - using namespace boost::beast::http; - - if (err->status_code == status::not_found) { + if (err->status_code == 404) { emit versionErrorCb(tr("The required endpoints were not found. " "Possibly not a Matrix server.")); return; diff --git a/src/Olm.cpp b/src/Olm.cpp
index d08c1b3e..ff4c883b 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg) for (const auto &cipher : msg.ciphertext) { // We skip messages not meant for the current device. - if (cipher.first != my_key) + if (cipher.first != my_key) { + nhlog::crypto()->debug( + "Skipping message for {} since we are {}.", cipher.first, my_key); continue; + } const auto type = cipher.second.type; nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); @@ -661,8 +664,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip for (const auto &id : session_ids) { auto session = cache::getOlmSession(sender_key, id); - if (!session) + if (!session) { + nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); continue; + } mtx::crypto::BinaryBuf text; diff --git a/src/Olm.h b/src/Olm.h
index d356cb55..8479f4f2 100644 --- a/src/Olm.h +++ b/src/Olm.h
@@ -4,8 +4,6 @@ #pragma once -#include <boost/optional.hpp> - #include <memory> #include <mtx/events.hpp> #include <mtx/events/encrypted.hpp> diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 5c5545ec..1588d07d 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp
@@ -11,6 +11,7 @@ #include <QtMath> #include <mtx/responses/register.hpp> +#include <mtx/responses/well-known.hpp> #include "Config.h" #include "Logging.h" @@ -108,6 +109,10 @@ RegisterPage::RegisterPage(QWidget *parent) error_password_confirmation_label_->setWordWrap(true); error_password_confirmation_label_->hide(); + error_server_label_ = new QLabel(this); + error_server_label_->setWordWrap(true); + error_server_label_->hide(); + form_layout_->addWidget(username_input_, Qt::AlignHCenter); form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); form_layout_->addWidget(password_input_, Qt::AlignHCenter); @@ -115,6 +120,7 @@ RegisterPage::RegisterPage(QWidget *parent) form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); form_layout_->addWidget(server_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_server_label_, Qt::AlignHCenter); button_layout_ = new QHBoxLayout(); button_layout_->setSpacing(0); @@ -140,6 +146,17 @@ RegisterPage::RegisterPage(QWidget *parent) top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); top_layout_->addStretch(1); + connect( + this, + &RegisterPage::versionErrorCb, + this, + [this](const QString &msg) { + error_server_label_->show(); + server_input_->setValid(false); + showError(error_server_label_, msg); + }, + Qt::QueuedConnection); + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); @@ -272,7 +289,7 @@ RegisterPage::RegisterPage(QWidget *parent) } // The server requires registration flows. - if (err->status_code == boost::beast::http::status::unauthorized) { + if (err->status_code == 401) { if (err->matrix_error.unauthorized.flows.empty()) { nhlog::net()->warn( "failed to retrieve registration flows: ({}) " @@ -351,10 +368,12 @@ RegisterPage::checkFields() error_username_label_->setText(""); error_password_label_->setText(""); error_password_confirmation_label_->setText(""); + error_server_label_->setText(""); error_username_label_->hide(); error_password_label_->hide(); error_password_confirmation_label_->hide(); + error_server_label_->hide(); password_confirmation_->setValid(true); server_input_->setValid(true); @@ -379,7 +398,8 @@ RegisterPage::checkFields() all_fields_good = false; } else if (server_input_->isModified() && (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) { - showError(tr("Invalid server name")); + error_server_label_->show(); + showError(error_server_label_, tr("Invalid server name")); server_input_->setValid(false); all_fields_good = false; } @@ -406,45 +426,38 @@ RegisterPage::onRegisterButtonClicked() http::client()->set_server(server); http::client()->verify_certificates( !UserSettings::instance()->disableCertificateValidation()); - http::client()->registration( - username, - password, - [this, username, password](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - emit registerOk(); - return; - } + http::client()->well_known( + [this, username, password](const mtx::responses::WellKnown &res, + mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + nhlog::net()->info("Autodiscovery: No .well-known."); + checkVersionAndRegister(username, password); + return; + } - // The server requires registration flows. - if (err->status_code == boost::beast::http::status::unauthorized) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn( - "failed to retrieve registration flows1: ({}) " - "{}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb( - QString::fromStdString(err->matrix_error.error)); + if (!err->parse_error.empty()) { + emit versionErrorCb(tr( + "Autodiscovery failed. Received malformed response.")); + nhlog::net()->error( + "Autodiscovery failed. Received malformed response."); return; } - emit registrationFlow( - username, password, err->matrix_error.unauthorized); + emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); return; } - nhlog::net()->error( - "failed to register: status_code ({}), matrix_error({})", - static_cast<int>(err->status_code), - err->matrix_error.error); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); + nhlog::net()->info("Autodiscovery: Discovered '" + + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + checkVersionAndRegister(username, password); }); emit registering(); @@ -452,6 +465,72 @@ RegisterPage::onRegisterButtonClicked() } void +RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password) +{ + http::client()->versions( + [this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit versionErrorCb(tr("The required endpoints were not found. " + "Possibly not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Received malformed response. Make sure " + "the homeserver domain is valid.")); + return; + } + + emit versionErrorCb(tr( + "An unknown error occured. Make sure the homeserver domain is valid.")); + return; + } + + http::client()->registration( + username, + password, + [this, username, password](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == 401) { + if (err->matrix_error.unauthorized.flows.empty()) { + nhlog::net()->warn( + "failed to retrieve registration flows1: ({}) " + "{}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } + + emit registrationFlow( + username, password, err->matrix_error.unauthorized); + return; + } + + nhlog::net()->error( + "failed to register: status_code ({}), matrix_error({})", + static_cast<int>(err->status_code), + err->matrix_error.error); + + emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + }); + }); +} + +void RegisterPage::paintEvent(QPaintEvent *) { QStyleOption opt; diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 2f05d04c..0e4a45d0 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h
@@ -31,6 +31,10 @@ protected: signals: void backButtonClicked(); void errorOccurred(); + + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void registering(); void registerOk(); void registerErrorCb(const QString &msg); @@ -52,6 +56,7 @@ private: bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); bool checkFields(); void showError(QLabel *label, const QString &msg); + void checkVersionAndRegister(const std::string &username, const std::string &password); QVBoxLayout *top_layout_; QHBoxLayout *back_layout_; @@ -63,6 +68,7 @@ private: QLabel *error_username_label_; QLabel *error_password_label_; QLabel *error_password_confirmation_label_; + QLabel *error_server_label_; FlatButton *back_button_; RaisedButton *register_button_; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 99560678..740b8979 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -64,10 +64,14 @@ void UserSettings::load(std::optional<QString> profile) { QSettings settings; - tray_ = settings.value("user/window/tray", false).toBool(); + tray_ = settings.value("user/window/tray", false).toBool(); + startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); + + roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); + communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); + hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); - startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); groupView_ = settings.value("user/group_view", true).toBool(); hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); @@ -175,10 +179,11 @@ UserSettings::setMobileMode(bool state) void UserSettings::setGroupView(bool state) { - if (groupView_ != state) - emit groupViewStateChanged(state); + if (groupView_ == state) + return; groupView_ = state; + emit groupViewStateChanged(state); save(); } @@ -248,6 +253,24 @@ UserSettings::setTimelineMaxWidth(int state) emit timelineMaxWidthChanged(state); save(); } +void +UserSettings::setCommunityListWidth(int state) +{ + if (state == communityListWidth_) + return; + communityListWidth_ = state; + emit communityListWidthChanged(state); + save(); +} +void +UserSettings::setRoomListWidth(int state) +{ + if (state == roomListWidth_) + return; + roomListWidth_ = state; + emit roomListWidthChanged(state); + save(); +} void UserSettings::setDesktopNotifications(bool state) @@ -571,6 +594,11 @@ UserSettings::save() settings.setValue("start_in_tray", startInTray_); settings.endGroup(); // window + settings.beginGroup("sidebar"); + settings.setValue("community_list_width", communityListWidth_); + settings.setValue("room_list_width", roomListWidth_); + settings.endGroup(); // window + settings.beginGroup("timeline"); settings.setValue("buttons", buttonsInTimeline_); settings.setValue("message_hover_highlight", messageHoverHighlight_); @@ -586,6 +614,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/UserSettingsPage.h b/src/UserSettingsPage.h
index 3ad0293b..acb08569 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h
@@ -61,6 +61,10 @@ class UserSettings : public QObject NOTIFY privacyScreenTimeoutChanged) Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY timelineMaxWidthChanged) + Q_PROPERTY( + int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) + Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY + communityListWidthChanged) Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) @@ -129,6 +133,8 @@ public: void setSortByImportance(bool state); void setButtonsInTimeline(bool state); void setTimelineMaxWidth(int state); + void setCommunityListWidth(int state); + void setRoomListWidth(int state); void setDesktopNotifications(bool state); void setAlertOnNotification(bool state); void setAvatarCircles(bool state); @@ -178,6 +184,8 @@ public: return hasDesktopNotifications() || hasAlertOnNotification(); } int timelineMaxWidth() const { return timelineMaxWidth_; } + int communityListWidth() const { return communityListWidth_; } + int roomListWidth() const { return roomListWidth_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } QString emojiFont() const @@ -227,6 +235,8 @@ signals: void privacyScreenChanged(bool state); void privacyScreenTimeoutChanged(int state); void timelineMaxWidthChanged(int state); + void roomListWidthChanged(int state); + void communityListWidthChanged(int state); void mobileModeChanged(bool mode); void fontSizeChanged(double state); void fontChanged(QString state); @@ -276,6 +286,8 @@ private: bool shareKeysWithTrustedUsers_; bool mobileMode_; int timelineMaxWidth_; + int roomListWidth_; + int communityListWidth_; double baseFontSize_; QString font_; QString emojiFont_; diff --git a/src/Utils.cpp b/src/Utils.cpp
index c110aa18..562b94fd 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp
@@ -679,11 +679,10 @@ utils::hashQString(const QString &input) return hash; } -QString -utils::generateContrastingHexColor(const QString &input, const QString &background) +QColor +utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol) { - const QColor backgroundCol(background); - const qreal backgroundLum = luminance(background); + const qreal backgroundLum = luminance(backgroundCol); // Create a color for the input auto hash = hashQString(input); diff --git a/src/Utils.h b/src/Utils.h
index e976cf81..1d48e2c7 100644 --- a/src/Utils.h +++ b/src/Utils.h
@@ -301,8 +301,8 @@ hashQString(const QString &input); //! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based //! on the input string. -QString -generateContrastingHexColor(const QString &input, const QString &background); +QColor +generateContrastingHexColor(const QString &input, const QColor &background); //! Given two luminance values, compute the contrast ratio between them. qreal diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 880a14a2..72330435 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp
@@ -41,8 +41,7 @@ using webrtc::CallType; using webrtc::State; WebRTCSession::WebRTCSession() - : QObject() - , devices_(CallDevices::instance()) + : devices_(CallDevices::instance()) { qRegisterMetaType<webrtc::CallType>(); qmlRegisterUncreatableMetaObject( diff --git a/src/main.cpp b/src/main.cpp
index fe1a9ee3..29e93d49 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -40,15 +40,48 @@ QQmlDebuggingEnabler enabler; #endif -#if defined(Q_OS_LINUX) -#include <boost/stacktrace.hpp> +#if HAVE_BACKTRACE_SYMBOLS_FD #include <csignal> +#include <execinfo.h> +#include <fcntl.h> +#include <unistd.h> void stacktraceHandler(int signum) { std::signal(signum, SIG_DFL); - boost::stacktrace::safe_dump_to("./nheko-backtrace.dump"); + + // boost::stacktrace::safe_dump_to("./nheko-backtrace.dump"); + + // see + // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes/77336#77336 + void *array[50]; + size_t size; + + // get void*'s for all entries on the stack + size = backtrace(array, 50); + + // print out all the frames to stderr + fprintf(stderr, "Error: signal %d:\n", signum); + backtrace_symbols_fd(array, size, STDERR_FILENO); + + int file = ::open("/tmp/nheko-crash.dump", + O_CREAT | O_WRONLY | O_TRUNC +#if defined(S_IWUSR) && defined(S_IRUSR) + , + S_IWUSR | S_IRUSR +#elif defined(S_IWRITE) && defined(S_IREAD) + , + S_IWRITE | S_IREAD +#endif + ); + if (file != -1) { + constexpr char header[] = "Error: signal\n"; + [[maybe_unused]] auto ret = write(file, header, std::size(header) - 1); + backtrace_symbols_fd(array, size, file); + close(file); + } + std::raise(SIGABRT); } diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index cedaacce..97bfa76d 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp
@@ -21,6 +21,8 @@ CommunitiesModel::roleNames() const {DisplayName, "displayName"}, {Tooltip, "tooltip"}, {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, }; } @@ -37,11 +39,28 @@ CommunitiesModel::data(const QModelIndex &index, int role) const return tr("Shows all rooms without filtering."); case CommunitiesModel::Roles::ChildrenHidden: return false; + case CommunitiesModel::Roles::Hidden: + return false; 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: @@ -54,7 +73,7 @@ CommunitiesModel::data(const QModelIndex &index, int role) const } else if (tag == "m.lowpriority") { switch (role) { case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/star.png"); + return QString(":/icons/icons/ui/lowprio.png"); case CommunitiesModel::Roles::DisplayName: return tr("Low Priority"); case CommunitiesModel::Roles::Tooltip: @@ -74,15 +93,16 @@ 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.right(2); case CommunitiesModel::Roles::Tooltip: - return tag.right(2); + return tag.mid(2); } } switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); case CommunitiesModel::Roles::ChildrenHidden: - return UserSettings::instance()->hiddenTags().contains("tag:" + tag); + return true; case CommunitiesModel::Roles::Id: return "tag:" + tag; } @@ -93,22 +113,35 @@ 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)); + + hiddentTagIds_ = UserSettings::instance()->hiddenTags(); endResetModel(); emit tagsChanged(); + emit hiddenTagsChanged(); } void @@ -117,6 +150,7 @@ CommunitiesModel::clear() beginResetModel(); tags_.clear(); endResetModel(); + resetCurrentTagId(); emit tagsChanged(); } @@ -133,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) @@ -143,16 +196,51 @@ void CommunitiesModel::setCurrentTagId(QString tagId) { if (tagId.startsWith("tag:")) { - auto tag = tagId.remove(0, 4); + auto tag = tagId.mid(4); for (const auto &t : tags_) { if (t == tag) { this->currentTagId_ = tagId; - emit currentTagIdChanged(); + emit currentTagIdChanged(currentTagId_); + 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_ = ""; - emit currentTagIdChanged(); + emit currentTagIdChanged(currentTagId_); +} + +void +CommunitiesModel::toggleTagId(QString tagId) +{ + if (hiddentTagIds_.contains(tagId)) { + hiddentTagIds_.removeOne(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } else { + hiddentTagIds_.push_back(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } + + if (tagId.startsWith("tag:")) { + auto idx = tags_.indexOf(tagId.mid(4)); + if (idx != -1) + 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 3f6a2a4c..677581dc 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h
@@ -11,12 +11,15 @@ #include <mtx/responses/sync.hpp> +#include "CacheStructs.h" + class CommunitiesModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY currentTagIdChanged RESET resetCurrentTagId) Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) + Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged) public: enum Roles @@ -25,6 +28,7 @@ public: DisplayName, Tooltip, ChildrenHidden, + Hidden, Id, }; @@ -33,7 +37,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { (void)parent; - return 1 + tags_.size(); + return 1 + tags_.size() + spaceOrder_.size(); } QVariant data(const QModelIndex &index, int role) const override; @@ -46,15 +50,29 @@ public slots: void resetCurrentTagId() { currentTagId_.clear(); - emit currentTagIdChanged(); + emit currentTagIdChanged(currentTagId_); } QStringList tags() const { return tags_; } + QStringList tagsWithDefault() const + { + QStringList tagsWD = tags_; + tagsWD.prepend("m.lowpriority"); + tagsWD.prepend("m.favourite"); + tagsWD.removeOne("m.server_notice"); + tagsWD.removeDuplicates(); + return tagsWD; + } + void toggleTagId(QString tagId); signals: - void currentTagIdChanged(); + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); void tagsChanged(); 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 4a9f0fff..9a91ff79 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -185,6 +185,48 @@ EventStore::EventStore(std::string room_id, QObject *) [this](std::string txn_id, std::string event_id) { nhlog::ui()->debug("sent {}", txn_id); + // Replace the event_id in pending edits/replies/redactions with the actual + // event_id of this event. This allows one to edit and reply to events that are + // currently pending. + + // FIXME (introduced by balsoft): this doesn't work for encrypted events, but + // allegedly it's hard to fix so I'll leave my first contribution at that + for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) { + if (cache::client()->getEvent(room_id_, related_event_id)) { + auto related_event = + cache::client()->getEvent(room_id_, related_event_id).value(); + auto relations = mtx::accessors::relations(related_event.data); + + // Replace the blockquote in fallback reply + auto related_text = + std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>( + &related_event.data); + if (related_text && relations.reply_to() == txn_id) { + size_t index = + related_text->content.formatted_body.find(txn_id); + if (index != std::string::npos) { + related_text->content.formatted_body.replace( + index, event_id.length(), event_id); + } + } + + for (mtx::common::Relation &rel : relations.relations) { + if (rel.event_id == txn_id) + rel.event_id = event_id; + } + + mtx::accessors::set_relations(related_event.data, relations); + + cache::client()->replaceEvent( + room_id_, related_event_id, related_event); + + auto idx = idToIndex(related_event_id); + + events_by_id_.remove({room_id_, related_event_id}); + events_.remove({room_id_, toInternalIdx(*idx)}); + } + } + http::client()->read_event( room_id_, event_id, [this, event_id](mtx::http::RequestErr err) { if (err) { @@ -193,6 +235,11 @@ EventStore::EventStore(std::string room_id, QObject *) } }); + auto idx = idToIndex(event_id); + + if (idx) + emit dataChanged(*idx, *idx); + cache::client()->removePendingStatus(room_id_, txn_id); this->current_txn = ""; this->current_txn_error_count = 0; @@ -628,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_; @@ -769,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/InputBar.cpp b/src/timeline/InputBar.cpp
index c309daab..b0747a7c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -296,7 +296,7 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify) firstLine = false; body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); } else { - body = QString("%1\n> %2\n").arg(body).arg(line); + body += QString("> %1\n").arg(line); } } diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 4dd44b30..87940948 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp
@@ -33,6 +33,27 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) &RoomlistModel::totalUnreadMessageCountUpdated, ChatPage::instance(), &ChatPage::unreadMessages); + + connect( + this, + &RoomlistModel::fetchedPreview, + this, + [this](QString roomid, RoomInfo info) { + if (this->previewedRooms.contains(roomid)) { + this->previewedRooms.insert(roomid, std::move(info)); + auto idx = this->roomidToIndex(roomid); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + Roles::AvatarUrl, + Roles::IsSpace, + Roles::IsPreviewFetched, + Qt::DisplayRole, + }); + } + }, + Qt::QueuedConnection); } QHash<int, QByteArray> @@ -51,6 +72,7 @@ RoomlistModel::roleNames() const {IsInvite, "isInvite"}, {IsSpace, "isSpace"}, {Tags, "tags"}, + {ParentSpaces, "parentSpaces"}, }; } @@ -60,6 +82,16 @@ RoomlistModel::data(const QModelIndex &index, int role) const if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { auto roomid = roomids.at(index.row()); + if (role == Roles::ParentSpaces) { + auto parents = cache::client()->getParentRoomIds(roomid.toStdString()); + QStringList list; + for (const auto &t : parents) + list.push_back(QString::fromStdString(t)); + return list; + } else if (role == Roles::RoomId) { + return roomid; + } + if (models.contains(roomid)) { auto room = models.value(roomid); switch (role) { @@ -67,8 +99,6 @@ RoomlistModel::data(const QModelIndex &index, int role) const return room->roomAvatarUrl(); case Roles::RoomName: return room->plainRoomName(); - case Roles::RoomId: - return room->roomId(); case Roles::LastMessage: return room->lastMessage().body; case Roles::Time: @@ -84,7 +114,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const case Roles::NotificationCount: return room->notificationCount(); case Roles::IsInvite: + return false; case Roles::IsSpace: + return room->isSpace(); + case Roles::IsPreview: return false; case Roles::Tags: { auto info = cache::singleRoomInfo(roomid.toStdString()); @@ -103,14 +136,12 @@ RoomlistModel::data(const QModelIndex &index, int role) const return QString::fromStdString(room.avatar_url); case Roles::RoomName: return QString::fromStdString(room.name); - case Roles::RoomId: - return roomid; case Roles::LastMessage: - return room.msgInfo.body; + return QString(); case Roles::Time: - return room.msgInfo.descriptiveTime; + return QString(); case Roles::Timestamp: - return QVariant(static_cast<quint64>(room.msgInfo.timestamp)); + return QVariant(static_cast<quint64>(0)); case Roles::HasUnreadMessages: case Roles::HasLoudNotification: return false; @@ -120,13 +151,77 @@ RoomlistModel::data(const QModelIndex &index, int role) const return true; case Roles::IsSpace: return false; + case Roles::IsPreview: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } + } else if (previewedRooms.contains(roomid) && + previewedRooms.value(roomid).has_value()) { + auto room = previewedRooms.value(roomid).value(); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::LastMessage: + return tr("Previewing this room"); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return room.is_space; + case Roles::IsPreview: + return true; + case Roles::IsPreviewFetched: + return true; case Roles::Tags: return QStringList(); default: return {}; } } else { - return {}; + if (role == Roles::IsPreview) + return true; + else if (role == Roles::IsPreviewFetched) + return false; + + fetchPreview(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString(); + case Roles::RoomName: + return tr("No preview available"); + case Roles::LastMessage: + return QString(); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } } } else { return {}; @@ -230,26 +325,112 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) newRoom->updateLastMessage(); - bool wasInvite = invites.contains(room_id); - if (!suppressInsertNotification && !wasInvite) - beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + std::vector<QString> previewsToAdd; + if (newRoom->isSpace()) { + auto childs = cache::client()->getChildRoomIds(room_id.toStdString()); + for (const auto &c : childs) { + auto id = QString::fromStdString(c); + if (!(models.contains(id) || invites.contains(id) || + previewedRooms.contains(id))) { + previewsToAdd.push_back(std::move(id)); + } + } + } - models.insert(room_id, std::move(newRoom)); + bool wasInvite = invites.contains(room_id); + bool wasPreview = previewedRooms.contains(room_id); + if (!suppressInsertNotification && + ((!wasInvite && !wasPreview) || !previewedRooms.empty())) + // if the old room was already in the list, don't add it. Also add all + // previews at the same time. + beginInsertRows(QModelIndex(), + (int)roomids.size(), + (int)(roomids.size() + previewsToAdd.size() - + ((wasInvite || wasPreview) ? 0 : 1))); + models.insert(room_id, std::move(newRoom)); if (wasInvite) { auto idx = roomidToIndex(room_id); invites.remove(room_id); emit dataChanged(index(idx), index(idx)); + } else if (wasPreview) { + auto idx = roomidToIndex(room_id); + previewedRooms.remove(room_id); + emit dataChanged(index(idx), index(idx)); } else { roomids.push_back(room_id); } + for (auto p : previewsToAdd) { + previewedRooms.insert(p, std::nullopt); + roomids.push_back(std::move(p)); + } + if (!suppressInsertNotification && !wasInvite) endInsertRows(); } } void +RoomlistModel::fetchPreview(QString roomid_) const +{ + std::string roomid = roomid_.toStdString(); + http::client()->get_state_event<mtx::events::state::Create>( + roomid, + "", + [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) { + bool is_space = false; + if (!err) { + is_space = c.type == mtx::events::state::room_type::space; + } + + http::client()->get_state_event<mtx::events::state::Avatar>( + roomid, + "", + [this, roomid, is_space](const mtx::events::state::Avatar &a, + mtx::http::RequestErr) { + auto avatar_url = a.url; + + http::client()->get_state_event<mtx::events::state::Topic>( + roomid, + "", + [this, roomid, avatar_url, is_space]( + const mtx::events::state::Topic &t, mtx::http::RequestErr) { + auto topic = t.topic; + http::client()->get_state_event<mtx::events::state::Name>( + roomid, + "", + [this, roomid, topic, avatar_url, is_space]( + const mtx::events::state::Name &n, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "Failed to fetch name event to " + "create preview for {}", + roomid); + } + + // don't even add a preview, if we got not a single + // response + if (n.name.empty() && avatar_url.empty() && + topic.empty()) + return; + + RoomInfo info{}; + info.name = n.name; + info.is_space = is_space; + info.avatar_url = avatar_url; + info.topic = topic; + + const_cast<RoomlistModel *>(this)->fetchedPreview( + QString::fromStdString(roomid), info); + }); + }); + }); + }); +} + +void RoomlistModel::sync(const mtx::responses::Rooms &rooms) { for (const auto &[room_id, room] : rooms.join) { @@ -324,6 +505,7 @@ RoomlistModel::initializeRooms() models.clear(); roomids.clear(); invites.clear(); + currentRoom_ = nullptr; invites = cache::client()->invites(); for (const auto &id : invites.keys()) @@ -407,11 +589,15 @@ RoomlistModel::setCurrentRoom(QString roomid) namespace { enum NotificationImportance : short { - ImportanceDisabled = -1, + ImportanceDisabled = -3, + NoPreview = -2, + Preview = -1, AllEventsRead = 0, NewMessage = 1, NewMentions = 2, - Invite = 3 + Invite = 3, + SubSpace = 4, + CurrentSpace = 5, }; } @@ -421,7 +607,18 @@ 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::IsPreview).toBool()) { + if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool()) + return Preview; + else + return NoPreview; + } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { return Invite; } else if (!this->sortByImportance) { return ImportanceDisabled; @@ -433,6 +630,7 @@ FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const return AllEventsRead; } } + bool FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { @@ -486,6 +684,140 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare } void +FilteredRoomlistModel::updateHiddenTagsAndSpaces() +{ + hiddenTags.clear(); + hiddenSpaces.clear(); + for (const auto &t : UserSettings::instance()->hiddenTags()) { + if (t.startsWith("tag:")) + hiddenTags.push_back(t.mid(4)); + else if (t.startsWith("space:")) + hiddenSpaces.push_back(t.mid(6)); + } + + invalidateFilter(); +} + +bool +FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const +{ + if (filterType == FilterBy::Nothing) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) + .toBool()) { + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + 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()) { + 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::IsPreview) + .toBool()) { + return false; + } + + 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; + + 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; + } +} + +void FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) { if (on) { @@ -532,7 +864,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 7ee0419f..2005c35e 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h
@@ -37,7 +37,10 @@ public: NotificationCount, IsInvite, IsSpace, + IsPreview, + IsPreviewFetched, Tags, + ParentSpaces, }; RoomlistModel(TimelineViewManager *parent = nullptr); @@ -86,15 +89,18 @@ private slots: signals: void totalUnreadMessageCountUpdated(int unreadMessages); void currentRoomChanged(); + void fetchedPreview(QString roomid, RoomInfo info); private: void addRoom(const QString &room_id, bool suppressInsertNotification = false); + void fetchPreview(QString roomid) const; TimelineViewManager *manager = nullptr; std::vector<QString> roomids; QHash<QString, RoomInfo> invites; QHash<QString, QSharedPointer<TimelineModel>> models; std::map<QString, bool> roomReadStatus; + QHash<QString, std::optional<RoomInfo>> previewedRooms; QSharedPointer<TimelineModel> currentRoom_; @@ -109,6 +115,7 @@ class FilteredRoomlistModel : public QSortFilterProxyModel public: FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; public slots: int roomidToIndex(QString roomid) @@ -128,6 +135,24 @@ public slots: void nextRoom(); void previousRoom(); + void updateFilterTag(QString tagId) + { + 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(); + } + + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); + signals: void currentRoomChanged(); @@ -135,4 +160,14 @@ private: short int calculateImportance(const QModelIndex &idx) const; RoomlistModel *roomlistmodel; bool sortByImportance = true; + + enum class FilterBy + { + Tag, + Space, + Nothing, + }; + QString filterStr = ""; + FilterBy filterType = FilterBy::Nothing; + QStringList hiddenTags, hiddenSpaces; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index f29f929e..caa40353 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, @@ -376,6 +380,27 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj 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, + &EventStore::messageSent, + this, + [this](std::string txn_id, std::string event_id) { + if (edit_.toStdString() == txn_id) { + edit_ = QString::fromStdString(event_id); + emit editChanged(edit_); + } + if (reply_.toStdString() == txn_id) { + reply_ = QString::fromStdString(event_id); + emit replyChanged(reply_); + } + }); + + connect(manager_, + &TimelineViewManager::initialSyncChanged, + &events, + &EventStore::enableKeyRequests); + showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); } @@ -507,6 +532,10 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r const static QRegularExpression matchImgUri( "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); + // Same regex but for single quotes around the src + const static QRegularExpression matchImgUri2( + "(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); + formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); const static QRegularExpression matchEmoticonHeight( "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); formattedBody_.replace(matchEmoticonHeight, @@ -568,10 +597,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case IsEdited: return QVariant(relations(event).replaces().has_value()); case IsEditable: - return QVariant(!is_state_event(event) && - mtx::accessors::sender(event) == - http::client()->user_id().to_string() && - !event_id(event).empty() && event_id(event).front() == '$'); + return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == + http::client()->user_id().to_string()); case IsEncrypted: { auto id = event_id(event); auto encrypted_event = events.get(id, "", false); @@ -757,6 +784,7 @@ TimelineModel::syncState(const mtx::responses::State &s) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } } @@ -813,6 +841,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } updateLastMessage(); @@ -1787,7 +1816,8 @@ TimelineModel::formatMemberEvent(QString id) } if (event->content.reason != "") { - rendered += tr(" Reason: %1").arg(QString::fromStdString(event->content.reason)); + rendered += + " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); } return rendered; @@ -1796,9 +1826,6 @@ TimelineModel::formatMemberEvent(QString id) void TimelineModel::setEdit(QString newEdit) { - if (edit_.startsWith('m')) - return; - if (newEdit.isEmpty()) { resetEdit(); return; @@ -1921,3 +1948,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/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index faf56b85..a45294d1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -107,8 +107,7 @@ QColor TimelineViewManager::userColor(QString id, QColor background) { if (!userColors.contains(id)) - userColors.insert( - id, QColor(utils::generateContrastingHexColor(id, background.name()))); + userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background))); return userColors.value(id); } @@ -195,7 +194,17 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par }); qmlRegisterSingletonType<RoomlistModel>( "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new FilteredRoomlistModel(self->rooms_); + auto ptr = new FilteredRoomlistModel(self->rooms_); + + connect(self->communities_, + &CommunitiesModel::currentTagIdChanged, + ptr, + &FilteredRoomlistModel::updateFilterTag); + connect(self->communities_, + &CommunitiesModel::hiddenTagsChanged, + ptr, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); + return ptr; }); qmlRegisterSingletonType<RoomlistModel>( "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { @@ -386,18 +395,17 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) imgDialog->showFullScreen(); auto room = rooms_->currentRoom(); - connect( - imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() { - // hide the overlay while presenting the save dialog for better - // cross platform support. - imgDialog->hide(); + connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { + // hide the overlay while presenting the save dialog for better + // cross platform support. + imgDialog->hide(); - if (!room->saveMedia(eventId)) { - imgDialog->show(); - } else { - imgDialog->close(); - } - }); + if (!room->saveMedia(eventId)) { + imgDialog->show(); + } else { + imgDialog->close(); + } + }); } void diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp
index 0bc8759e..f78ef09b 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp
@@ -181,7 +181,7 @@ RoomSettings::RoomSettings(QString roomid, QObject *parent) roomid_.toStdString(), [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { if (err) { - if (err->status_code == boost::beast::http::status::not_found) + if (err->status_code == 404) http::client()->get_pushrules( "global", "room", 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; diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index da130242..3d9c4b6a 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp
@@ -39,7 +39,8 @@ UserProfile::UserProfile(QString roomid, getGlobalProfileData(); } - if (!cache::client() || !cache::client()->isDatabaseReady()) + if (!cache::client() || !cache::client()->isDatabaseReady() || + !ChatPage::instance()->timelineManager()) return; connect(cache::client(), @@ -127,10 +128,7 @@ UserProfile::displayName() QString UserProfile::avatarUrl() { - return (isGlobalUserProfile() && globalAvatarUrl != "") - ? globalAvatarUrl - : cache::avatarUrl(roomid_, userid_); - ; + return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_); } bool