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
|