diff options
Diffstat (limited to 'src')
29 files changed, 1119 insertions, 224 deletions
diff --git a/src/AliasEditModel.cpp b/src/AliasEditModel.cpp index 218c5b36..c6dc35c6 100644 --- a/src/AliasEditModel.cpp +++ b/src/AliasEditModel.cpp @@ -42,7 +42,7 @@ AliasEditingModel::AliasEditingModel(const std::string &rid, QObject *parent) } } - for (const auto &alias : qAsConst(aliases)) { + for (const auto &alias : std::as_const(aliases)) { fetchAliasesStatus(alias.alias); } fetchPublishedAliases(); @@ -148,7 +148,7 @@ void AliasEditingModel::addAlias(QString newAlias) { const auto aliasStr = newAlias.toStdString(); - for (const auto &e : qAsConst(aliases)) { + for (const auto &e : std::as_const(aliases)) { if (e.alias == aliasStr) { return; } diff --git a/src/Cache.cpp b/src/Cache.cpp index 0426f38a..8ad850ac 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -37,7 +37,7 @@ //! Should be changed when a breaking change occurs in the cache format. //! This will reset client's data. -static const std::string CURRENT_CACHE_FORMAT_VERSION{"2023.03.12"}; +static const std::string CURRENT_CACHE_FORMAT_VERSION{"2023.10.22"}; //! Keys used for the DB static const std::string_view NEXT_BATCH_KEY("next_batch"); @@ -91,10 +91,30 @@ static constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); static constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); //! MegolmSessionIndex -> session data about which devices have access to this static constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db"); +//! Curve25519 key to session_id and json encoded olm session, separated by null. Dupsorted. +static constexpr auto OLM_SESSIONS_DB("olm_sessions.v3"); + +//! flag to be set, when the db should be compacted on startup +bool needsCompact = false; using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; +static std::string +combineOlmSessionKeyFromCurveAndSessionId(std::string_view curve25519, std::string_view session_id) +{ + std::string combined(curve25519.size() + 1 + session_id.size(), '\0'); + combined.replace(0, curve25519.size(), curve25519); + combined.replace(curve25519.size() + 1, session_id.size(), session_id); + return combined; +} +static std::pair<std::string_view, std::string_view> +splitCurve25519AndOlmSessionId(std::string_view input) +{ + auto separator = input.find('\0'); + return std::pair(input.substr(0, separator), input.substr(separator + 1)); +} + namespace { std::unique_ptr<Cache> instance_ = nullptr; } @@ -132,6 +152,49 @@ ro_txn(lmdb::env &env) return RO_txn{txn}; } +static void +compactDatabase(lmdb::env &from, lmdb::env &to) +{ + auto fromTxn = lmdb::txn::begin(from, nullptr, MDB_RDONLY); + auto toTxn = lmdb::txn::begin(to); + + auto rootDb = lmdb::dbi::open(fromTxn); + auto dbNames = lmdb::cursor::open(fromTxn, rootDb); + + std::string_view dbName; + while (dbNames.get(dbName, MDB_cursor_op::MDB_NEXT_NODUP)) { + nhlog::db()->info("Compacting db: {}", dbName); + + auto flags = MDB_CREATE; + + if (dbName.ends_with("/event_order") || dbName.ends_with("/order2msg") || + dbName.ends_with("/pending")) + flags |= MDB_INTEGERKEY; + if (dbName.ends_with("/related") || dbName.ends_with("/states_key") || + dbName == SPACES_CHILDREN_DB || dbName == SPACES_PARENTS_DB) + flags |= MDB_DUPSORT; + + auto dbNameStr = std::string(dbName); + auto fromDb = lmdb::dbi::open(fromTxn, dbNameStr.c_str(), flags); + auto toDb = lmdb::dbi::open(toTxn, dbNameStr.c_str(), flags); + + if (dbName.ends_with("/states_key")) { + lmdb::dbi_set_dupsort(fromTxn, fromDb, Cache::compare_state_key); + lmdb::dbi_set_dupsort(toTxn, toDb, Cache::compare_state_key); + } + + auto fromCursor = lmdb::cursor::open(fromTxn, fromDb); + auto toCursor = lmdb::cursor::open(toTxn, toDb); + + std::string_view key, val; + while (fromCursor.get(key, val, MDB_cursor_op::MDB_NEXT)) { + toCursor.put(key, val, MDB_APPENDDUP); + } + } + + toTxn.commit(); +} + template<class T> bool containsStateUpdates(const T &e) @@ -266,9 +329,13 @@ Cache::setup() nhlog::db()->info("completed state migration"); } - env_ = lmdb::env::create(); - env_.set_mapsize(DB_SIZE); - env_.set_max_dbs(MAX_DBS); + auto openEnv = [](const QString &name) { + auto e = lmdb::env::create(); + e.set_mapsize(DB_SIZE); + e.set_max_dbs(MAX_DBS); + e.open(name.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC); + return e; + }; if (isInitial) { nhlog::db()->info("initializing LMDB"); @@ -291,7 +358,41 @@ Cache::setup() // corruption is an lmdb or filesystem bug. See // https://github.com/Nheko-Reborn/nheko/issues/1355 // https://github.com/Nheko-Reborn/nheko/issues/1303 - env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC); + env_ = openEnv(cacheDirectory_); + + if (needsCompact) { + auto compactDir = QStringLiteral("%1-compacting").arg(cacheDirectory_); + auto toDeleteDir = QStringLiteral("%1-olddb").arg(cacheDirectory_); + if (QFile::exists(cacheDirectory_)) + QDir(compactDir).removeRecursively(); + if (QFile::exists(toDeleteDir)) + QDir(toDeleteDir).removeRecursively(); + if (!QDir().mkpath(compactDir)) { + nhlog::db()->warn( + "Failed to create directory '{}' for database compaction, skipping compaction!", + compactDir.toStdString()); + } else { + // lmdb::env_copy(env_, compactDir.toStdString().c_str(), MDB_CP_COMPACT); + + // create a temporary db + auto temp = openEnv(compactDir); + + // copy data + compactDatabase(env_, temp); + + // close envs + temp.close(); + env_.close(); + + // swap the databases and delete old one + QDir().rename(cacheDirectory_, toDeleteDir); + QDir().rename(compactDir, cacheDirectory_); + QDir(toDeleteDir).removeRecursively(); + + // reopen env + env_ = openEnv(cacheDirectory_); + } + } } catch (const lmdb::error &e) { if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { throw std::runtime_error("LMDB initialization failed" + std::string(e.what())); @@ -302,11 +403,11 @@ Cache::setup() QDir stateDir(cacheDirectory_); auto eList = stateDir.entryList(QDir::NoDotAndDotDot); - for (const auto &file : qAsConst(eList)) { + for (const auto &file : std::as_const(eList)) { if (!stateDir.remove(file)) throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str()); } - env_.open(cacheDirectory_.toStdString().c_str()); + env_ = openEnv(cacheDirectory_); } auto txn = lmdb::txn::begin(env_); @@ -328,6 +429,8 @@ Cache::setup() outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); + olmSessionDb_ = lmdb::dbi::open(txn, OLM_SESSIONS_DB, MDB_CREATE); + // What rooms are encrypted encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); eventExpiryBgJob_ = lmdb::dbi::open(txn, EVENT_EXPIRATION_BG_JOB_DB, MDB_CREATE); @@ -991,8 +1094,6 @@ Cache::saveOlmSessions(std::vector<std::pair<std::string, mtx::crypto::OlmSessio auto txn = lmdb::txn::begin(env_); for (const auto &[curve25519, session] : sessions) { - auto db = getOlmSessionsDb(txn, curve25519); - const auto pickled = pickle<SessionObject>(session.get(), pickle_secret_); const auto session_id = mtx::crypto::session_id(session.get()); @@ -1000,7 +1101,9 @@ Cache::saveOlmSessions(std::vector<std::pair<std::string, mtx::crypto::OlmSessio stored_session.pickled_session = pickled; stored_session.last_message_ts = timestamp; - db.put(txn, session_id, nlohmann::json(stored_session).dump()); + olmSessionDb_.put(txn, + combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), + nlohmann::json(stored_session).dump()); } txn.commit(); @@ -1014,7 +1117,6 @@ Cache::saveOlmSession(const std::string &curve25519, using namespace mtx::crypto; auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); const auto pickled = pickle<SessionObject>(session.get(), pickle_secret_); const auto session_id = mtx::crypto::session_id(session.get()); @@ -1023,7 +1125,9 @@ Cache::saveOlmSession(const std::string &curve25519, stored_session.pickled_session = pickled; stored_session.last_message_ts = timestamp; - db.put(txn, session_id, nlohmann::json(stored_session).dump()); + olmSessionDb_.put(txn, + combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), + nlohmann::json(stored_session).dump()); txn.commit(); } @@ -1035,10 +1139,10 @@ Cache::getOlmSession(const std::string &curve25519, const std::string &session_i try { auto txn = ro_txn(env_); - auto db = getOlmSessionsDb(txn, curve25519); std::string_view pickled; - bool found = db.get(txn, session_id, pickled); + bool found = olmSessionDb_.get( + txn, combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), pickled); if (found) { auto data = nlohmann::json::parse(pickled).get<StoredOlmSession>(); @@ -1057,14 +1161,20 @@ Cache::getLatestOlmSession(const std::string &curve25519) try { auto txn = ro_txn(env_); - auto db = getOlmSessionsDb(txn, curve25519); - std::string_view session_id, pickled_session; + std::string_view key = curve25519, pickled_session; std::optional<StoredOlmSession> currentNewest; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(session_id, pickled_session, MDB_NEXT)) { + auto cursor = lmdb::cursor::open(txn, olmSessionDb_); + bool first = true; + while (cursor.get(key, pickled_session, first ? MDB_SET_RANGE : MDB_NEXT)) { + first = false; + + auto storedCurve = splitCurve25519AndOlmSessionId(key).first; + if (storedCurve != curve25519) + break; + auto data = nlohmann::json::parse(pickled_session).get<StoredOlmSession>(); if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts) currentNewest = data; @@ -1086,14 +1196,21 @@ Cache::getOlmSessions(const std::string &curve25519) try { auto txn = ro_txn(env_); - auto db = getOlmSessionsDb(txn, curve25519); - std::string_view session_id, unused; + std::string_view key = curve25519, value; std::vector<std::string> res; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(session_id, unused, MDB_NEXT)) + auto cursor = lmdb::cursor::open(txn, olmSessionDb_); + + bool first = true; + while (cursor.get(key, value, first ? MDB_SET_RANGE : MDB_NEXT)) { + first = false; + + auto [storedCurve, session_id] = splitCurve25519AndOlmSessionId(key); + if (storedCurve != curve25519) + break; res.emplace_back(session_id); + } cursor.close(); return res; @@ -1603,6 +1720,46 @@ Cache::runMigrations() nhlog::db()->info("Successfully updated states key database format."); return true; }}, + {"2023.10.22", + [this]() { + // migrate olm sessions to a single db + try { + auto txn = lmdb::txn::begin(env_, nullptr); + auto mainDb = lmdb::dbi::open(txn); + auto dbNames = lmdb::cursor::open(txn, mainDb); + + std::string_view dbName; + while (dbNames.get(dbName, MDB_NEXT)) { + if (!dbName.starts_with("olm_sessions.v2/")) + continue; + + auto curveKey = dbName; + curveKey.remove_prefix(std::string_view("olm_sessions.v2/").size()); + + auto oldDb = lmdb::dbi::open(txn, std::string(dbName).c_str()); + auto olmCursor = lmdb::cursor::open(txn, oldDb); + + std::string_view session_id, json; + while (olmCursor.get(session_id, json, MDB_NEXT)) { + olmSessionDb_.put( + txn, + combineOlmSessionKeyFromCurveAndSessionId(curveKey, session_id), + json); + } + + oldDb.drop(txn, true); + } + + txn.commit(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("Failed to convert olm sessions database in migration! {}", + e.what()); + return false; + } + + nhlog::db()->info("Successfully updated olm sessions database format."); + return true; + }}, }; nhlog::db()->info("Running migrations, this may take a while!"); @@ -5366,6 +5523,12 @@ from_json(const nlohmann::json &obj, StoredOlmSession &msg) namespace cache { void +setNeedsCompactFlag() +{ + needsCompact = true; +} + +void init(const QString &user_id) { instance_ = std::make_unique<Cache>(user_id); diff --git a/src/Cache.h b/src/Cache.h index 113ee42e..bed4938c 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -27,6 +27,9 @@ struct Notifications; namespace cache { void +setNeedsCompactFlag(); + +void init(const QString &user_id); std::string diff --git a/src/Cache_p.h b/src/Cache_p.h index 8d51c7c4..e59796ed 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -594,11 +594,6 @@ private: 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); - } - lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id) { return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE); @@ -679,16 +674,6 @@ private: return lmdb::dbi::open(txn, "verified", MDB_CREATE); } - //! Retrieves or creates the database that stores the open OLM sessions between our device - //! and the given curve25519 key which represents another device. - //! - //! Each entry is a map from the session_id to the pickled representation of the session. - lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) - { - return lmdb::dbi::open( - txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE); - } - QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) { if (!event.content.display_name.empty()) @@ -718,6 +703,7 @@ private: lmdb::dbi inboundMegolmSessionDb_; lmdb::dbi outboundMegolmSessionDb_; lmdb::dbi megolmSessionDataDb_; + lmdb::dbi olmSessionDb_; lmdb::dbi encryptedRooms_; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 06d88303..90d542dd 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -588,6 +588,10 @@ ChatPage::loadStateFromCache() try { olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret()); + nhlog::db()->info("Removing old cached messages"); + cache::deleteOldData(); + nhlog::db()->info("Message removal done"); + emit initializeEmptyViews(); cache::calculateRoomReadStatus(); @@ -769,14 +773,6 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); emit syncUI(std::move(res)); - - // if we process a lot of syncs (1 every 200ms), this means we clean the - // db every 100s - static int syncCounter = 0; - if (syncCounter++ >= 500) { - cache::deleteOldData(); - syncCounter = 0; - } } catch (const lmdb::map_full_error &e) { nhlog::db()->error("lmdb is full: {}", e.what()); cache::deleteOldData(); @@ -1577,7 +1573,7 @@ ChatPage::handleMatrixUri(QString uri) auto items = uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&', Qt::SkipEmptyParts); - for (QString item : qAsConst(items)) { + for (QString item : std::as_const(items)) { nhlog::ui()->info("item: {}", item.toStdString()); if (item.startsWith(QLatin1String("action="))) { diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 7b49c234..76f37fad 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -18,7 +18,7 @@ InviteesModel::InviteesModel(TimelineModel *room, QObject *parent) void InviteesModel::addUser(QString mxid, QString displayName, QString avatarUrl) { - for (const auto &invitee : qAsConst(invitees_)) + for (const auto &invitee : std::as_const(invitees_)) if (invitee->mxid_ == mxid) return; @@ -79,7 +79,7 @@ InviteesModel::mxids() { QStringList mxidList; mxidList.reserve(invitees_.size()); - for (auto &invitee : qAsConst(invitees_)) + for (auto &invitee : std::as_const(invitees_)) mxidList.push_back(invitee->mxid_); return mxidList; } diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 47e0344f..8f930c1a 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -38,7 +38,7 @@ MxcImageProvider::MxcImageProvider() QDir::Filter::Writable | QDir::Filter::NoDotAndDotDot | QDir::Filter::Files); auto files = dir.entryInfoList(); - for (const auto &fileInfo : qAsConst(files)) { + for (const auto &fileInfo : std::as_const(files)) { if (fileInfo.fileTime(QFile::FileTime::FileAccessTime) .daysTo(QDateTime::currentDateTime()) > 30) { if (QFile::remove(fileInfo.absoluteFilePath())) diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp index f0fd9194..01337f11 100644 --- a/src/PowerlevelsEditModels.cpp +++ b/src/PowerlevelsEditModels.cpp @@ -83,7 +83,7 @@ std::map<std::string, mtx::events::state::power_level_t, std::less<>> PowerlevelsTypeListModel::toEvents() const { std::map<std::string, mtx::events::state::power_level_t, std::less<>> m; - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key.find('.') != std::string::npos) m[key] = pl; return m; @@ -91,7 +91,7 @@ PowerlevelsTypeListModel::toEvents() const mtx::events::state::power_level_t PowerlevelsTypeListModel::kick() const { - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key == "kick") return pl; return powerLevels_.users_default; @@ -99,7 +99,7 @@ PowerlevelsTypeListModel::kick() const mtx::events::state::power_level_t PowerlevelsTypeListModel::invite() const { - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key == "invite") return pl; return powerLevels_.users_default; @@ -107,7 +107,7 @@ PowerlevelsTypeListModel::invite() const mtx::events::state::power_level_t PowerlevelsTypeListModel::ban() const { - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key == "ban") return pl; return powerLevels_.users_default; @@ -115,7 +115,7 @@ PowerlevelsTypeListModel::ban() const mtx::events::state::power_level_t PowerlevelsTypeListModel::eventsDefault() const { - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key == "zdefault_events") return pl; return powerLevels_.users_default; @@ -123,7 +123,7 @@ PowerlevelsTypeListModel::eventsDefault() const mtx::events::state::power_level_t PowerlevelsTypeListModel::stateDefault() const { - for (const auto &[key, pl] : qAsConst(types)) + for (const auto &[key, pl] : std::as_const(types)) if (key == "zdefault_states") return pl; return powerLevels_.users_default; @@ -399,7 +399,7 @@ std::map<std::string, mtx::events::state::power_level_t, std::less<>> PowerlevelsUserListModel::toUsers() const { std::map<std::string, mtx::events::state::power_level_t, std::less<>> m; - for (const auto &[key, pl] : qAsConst(users)) + for (const auto &[key, pl] : std::as_const(users)) if (key.size() > 0 && key.at(0) == '@') m[key] = pl; return m; @@ -407,7 +407,7 @@ PowerlevelsUserListModel::toUsers() const mtx::events::state::power_level_t PowerlevelsUserListModel::usersDefault() const { - for (const auto &[key, pl] : qAsConst(users)) + for (const auto &[key, pl] : std::as_const(users)) if (key == "default") return pl; return powerLevels_.users_default; @@ -635,7 +635,7 @@ PowerlevelEditingModels::updateSpacesModel() void PowerlevelEditingModels::addRole(int pl) { - for (const auto &e : qAsConst(types_.types)) + for (const auto &e : std::as_const(types_.types)) if (pl == int(e.pl)) return; @@ -752,7 +752,7 @@ PowerlevelsSpacesListModel::commit() { std::vector<std::string> spacesToApplyTo; - for (const auto &s : qAsConst(spaces)) + for (const auto &s : std::as_const(spaces)) if (s.apply) spacesToApplyTo.push_back(s.roomid); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 75a6b443..4a25880c 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -152,7 +152,7 @@ UserSettings::load(std::optional<QString> profile) collapsedSpaces_.clear(); auto tempSpaces = settings.value(prefix + "user/collapsed_spaces", QList<QVariant>{}).toList(); - for (const auto &e : qAsConst(tempSpaces)) + for (const auto &e : std::as_const(tempSpaces)) collapsedSpaces_.push_back(e.toStringList()); shareKeysWithTrustedUsers_ = @@ -962,7 +962,7 @@ UserSettings::save() QVariantList v; v.reserve(collapsedSpaces_.size()); - for (const auto &e : qAsConst(collapsedSpaces_)) + for (const auto &e : std::as_const(collapsedSpaces_)) v.push_back(e); settings.setValue(prefix + "user/collapsed_spaces", v); diff --git a/src/Utils.cpp b/src/Utils.cpp index 0ea42a27..ec73c901 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -428,7 +428,7 @@ utils::escapeBlacklistedHtml(const QString &rawStr) "blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul", "ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li", "b", "/b", "i", "/i", "u", "/u", "strong", "/strong", - "em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr", + "em", "/em", "strike", "/strike", "code", "/code", "hr", "hr/", "br", "br/", "div", "/div", "table", "/table", "thead", "/thead", "tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td", "caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img", diff --git a/src/encryption/Olm.cpp b/src/encryption/Olm.cpp index 8993f715..7fa176b0 100644 --- a/src/encryption/Olm.cpp +++ b/src/encryption/Olm.cpp @@ -719,7 +719,7 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip nhlog::crypto()->debug("Updated olm session: {}", mtx::crypto::session_id(session->get())); cache::saveOlmSession( - id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch()); + sender_key, std::move(session.value()), QDateTime::currentMSecsSinceEpoch()); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", msg.type, diff --git a/src/main.cpp b/src/main.cpp index 3984f4ba..36326b13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,8 +20,8 @@ #include <QStandardPaths> #include <QTranslator> +#include "Cache.h" #include "ChatPage.h" -#include "Config.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -226,6 +226,10 @@ main(int argc, char *argv[]) "The default is 'file,stderr'. types:{file,stderr,none}"), QObject::tr("type")); parser.addOption(logType); + QCommandLineOption compactDb( + QStringList() << QStringLiteral("C") << QStringLiteral("compact"), + QObject::tr("Recompacts the database which might improve performance.")); + parser.addOption(compactDb); // This option is not actually parsed via Qt due to the need to parse it before the app // name is set. It only exists to keep Qt from complaining about the --profile/-p @@ -240,6 +244,9 @@ main(int argc, char *argv[]) parser.process(app); + if (parser.isSet(compactDb)) + cache::setNeedsCompactFlag(); + // This check needs to happen _after_ process(), so that we actually print help for --help when // Nheko is already running. if (app.isSecondary()) { diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp index a54256ae..ed5c0670 100644 --- a/src/notifications/Manager.cpp +++ b/src/notifications/Manager.cpp @@ -45,7 +45,7 @@ NotificationsManager::removeNotifications(const QString &roomId_, markerPos = std::max(markerPos, cache::getEventIndex(room_id, e.toStdString()).value_or(0)); } - for (const auto &[roomId, eventId] : qAsConst(this->notificationIds)) { + for (const auto &[roomId, eventId] : std::as_const(this->notificationIds)) { if (roomId != roomId_) continue; auto idx = cache::getEventIndex(room_id, eventId.toStdString()); diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index fc92c9ae..e838bb85 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -36,14 +36,14 @@ NotificationsManager::NotificationsManager(QObject *parent) this) , hasMarkup_{std::invoke([this]() -> bool { auto caps = dbus.call("GetCapabilities").arguments(); - for (const auto &x : qAsConst(caps)) + for (const auto &x : std::as_const(caps)) if (x.toStringList().contains("body-markup")) return true; return false; })} , hasImages_{std::invoke([this]() -> bool { auto caps = dbus.call("GetCapabilities").arguments(); - for (const auto &x : qAsConst(caps)) + for (const auto &x : std::as_const(caps)) if (x.toStringList().contains("body-images")) return true; return false; diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp index 3c09d747..e1018f38 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp @@ -581,7 +581,7 @@ CommunitiesModel::setCurrentTagId(const QString &tagId) if (tagId.startsWith(QLatin1String("tag:"))) { auto tag = tagId.mid(4); - for (const auto &t : qAsConst(tags_)) { + for (const auto &t : std::as_const(tags_)) { if (t == tag) { this->currentTagId_ = tagId; UserSettings::instance()->setCurrentTagId(tagId); diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp index 91b2194b..b95116ec 100644 --- a/src/timeline/DelegateChooser.cpp +++ b/src/timeline/DelegateChooser.cpp @@ -96,7 +96,7 @@ DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p) void DelegateChooser::recalcChild() { - for (const auto choice : qAsConst(choices_)) { + for (const auto choice : std::as_const(choices_)) { const auto &choiceValue = choice->roleValueRef(); if (choiceValue == roleValue_ || (!choiceValue.isValid() && !roleValue_.isValid())) { if (child_) { @@ -134,7 +134,7 @@ DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) } else if (status == QQmlIncubator::Error) { auto errors_ = errors(); - for (const auto &e : qAsConst(errors_)) + for (const auto &e : std::as_const(errors_)) nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); } } diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp new file mode 100644 index 00000000..99a4cf3a --- /dev/null +++ b/src/timeline/EventDelegateChooser.cpp @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "EventDelegateChooser.h" +#include "TimelineModel.h" + +#include "Logging.h" + +#include <QQmlEngine> +#include <QtGlobal> + +#include <ranges> + +// privat qt headers to access required properties +#include <QtQml/private/qqmlincubator_p.h> +#include <QtQml/private/qqmlobjectcreator_p.h> + +QQmlComponent * +EventDelegateChoice::delegate() const +{ + return delegate_; +} + +void +EventDelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QList<int> +EventDelegateChoice::roleValues() const +{ + return roleValues_; +} + +void +EventDelegateChoice::setRoleValues(const QList<int> &value) +{ + if (value != roleValues_) { + roleValues_ = value; + emit roleValuesChanged(); + emit changed(); + } +} + +QQmlListProperty<EventDelegateChoice> +EventDelegateChooser::choices() +{ + return QQmlListProperty<EventDelegateChoice>(this, + this, + &EventDelegateChooser::appendChoice, + &EventDelegateChooser::choiceCount, + &EventDelegateChooser::choice, + &EventDelegateChooser::clearChoices); +} + +void +EventDelegateChooser::appendChoice(QQmlListProperty<EventDelegateChoice> *p, EventDelegateChoice *c) +{ + EventDelegateChooser *dc = static_cast<EventDelegateChooser *>(p->object); + dc->choices_.append(c); +} + +qsizetype +EventDelegateChooser::choiceCount(QQmlListProperty<EventDelegateChoice> *p) +{ + return static_cast<EventDelegateChooser *>(p->object)->choices_.count(); +} +EventDelegateChoice * +EventDelegateChooser::choice(QQmlListProperty<EventDelegateChoice> *p, qsizetype index) +{ + return static_cast<EventDelegateChooser *>(p->object)->choices_.at(index); +} +void +EventDelegateChooser::clearChoices(QQmlListProperty<EventDelegateChoice> *p) +{ + static_cast<EventDelegateChooser *>(p->object)->choices_.clear(); +} + +void +EventDelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + // eventIncubator.forceCompletion(); +} + +void +EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) +{ + auto item = qobject_cast<QQuickItem *>(obj); + if (!item) + return; + + item->setParentItem(&chooser); + item->setParent(&chooser); + + auto roleNames = chooser.room_->roleNames(); + QHash<QByteArray, int> nameToRole; + for (const auto &[k, v] : roleNames.asKeyValueRange()) { + nameToRole.insert(v, k); + } + + QHash<int, int> roleToPropIdx; + std::vector<QModelRoleData> roles; + // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 + QHash<QString, RequiredPropertyKey> requiredProperties; + for (const auto &[propKey, prop] : + QQmlIncubatorPrivate::get(this)->requiredProperties()->asKeyValueRange()) { + requiredProperties.insert(prop.propertyName, propKey); + } + + // collect required properties + auto mo = obj->metaObject(); + for (int i = 0; i < mo->propertyCount(); i++) { + auto prop = mo->property(i); + // nhlog::ui()->critical("Found prop {}", prop.name()); + // See https://bugreports.qt.io/browse/QTBUG-98846 + if (!prop.isRequired() && !requiredProperties.contains(prop.name())) + continue; + + if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { + roleToPropIdx.insert(*role, i); + roles.emplace_back(*role); + + // nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role); + } else { + nhlog::ui()->critical("Required property {} not found in model!", prop.name()); + } + } + + // nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); + chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); + + Qt::beginPropertyUpdateGroup(); + auto attached = qobject_cast<EventDelegateChooserAttachedType *>( + qmlAttachedPropertiesObject<EventDelegateChooser>(obj)); + Q_ASSERT(attached != nullptr); + attached->setIsReply(this->forReply); + + for (const auto &role : roles) { + const auto &roleName = roleNames[role.role()]; + // nhlog::ui()->critical("Setting role {}, {} to {}", + // role.role(), + // roleName.toStdString(), + // role.data().toString().toStdString()); + + // nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name()); + mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); + + if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) + QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); + } + + Qt::endPropertyUpdateGroup(); + + // setInitialProperties(rolesToSet); + + auto update = + [this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList<int> &changedRoles) { + if (changedRoles.empty() || changedRoles.contains(TimelineModel::Roles::Type)) { + int type = chooser.room_ + ->dataById(currentId, + TimelineModel::Roles::Type, + forReply ? chooser.eventId_ : QString()) + .toInt(); + if (type != oldType) { + // nhlog::ui()->debug("Type changed!"); + reset(currentId); + return; + } + } + + std::vector<QModelRoleData> rolesToRequest; + + if (changedRoles.empty()) { + for (const auto role : + std::ranges::subrange(roleToPropIdx.keyBegin(), roleToPropIdx.keyEnd())) + rolesToRequest.emplace_back(role); + } else { + for (auto role : changedRoles) { + if (roleToPropIdx.contains(role)) { + rolesToRequest.emplace_back(role); + } + } + } + + if (rolesToRequest.empty()) + return; + + auto mo = obj->metaObject(); + chooser.room_->multiData( + currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest); + + Qt::beginPropertyUpdateGroup(); + for (const auto &role : rolesToRequest) { + mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); + } + Qt::endPropertyUpdateGroup(); + }; + + if (!forReply) { + auto row = chooser.room_->idToIndex(currentId); + auto connection = connect( + chooser.room_, + &QAbstractItemModel::dataChanged, + obj, + [row, update](const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QList<int> &changedRoles) { + if (row < topLeft.row() || row > bottomRight.row()) + return; + + update(changedRoles); + }, + Qt::QueuedConnection); + connect(&this->chooser, &EventDelegateChooser::destroyed, obj, [connection]() { + QObject::disconnect(connection); + }); + } +} + +void +EventDelegateChooser::DelegateIncubator::reset(QString id) +{ + if (!chooser.room_ || id.isEmpty()) + return; + + // nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply); + + this->currentId = id; + + auto role = + chooser.room_ + ->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString()) + .toInt(); + this->oldType = role; + + for (const auto choice : std::as_const(chooser.choices_)) { + const auto &choiceValue = choice->roleValues(); + if (choiceValue.contains(role) || choiceValue.empty()) { + // nhlog::ui()->debug( + // "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role)); + + if (auto child = qobject_cast<QQuickItem *>(object())) { + child->setParentItem(nullptr); + } + + choice->delegate()->create(*this, QQmlEngine::contextForObject(&chooser)); + return; + } + } +} + +void +EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + auto child = qobject_cast<QQuickItem *>(object()); + if (child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + child->setParentItem(&chooser); + QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::CppOwnership); + + // connect(child, &QQuickItem::parentChanged, child, [child](QQuickItem *) { + // // QTBUG-115687 + // if (child->flags().testFlag(QQuickItem::ItemObservesViewport)) { + // nhlog::ui()->critical("SETTING OBSERVES VIEWPORT"); + // // Re-trigger the parent traversal to get subtreeTransformChangedEnabled turned + // on child->setFlag(QQuickItem::ItemObservesViewport); + // } + // }); + + if (forReply) + emit chooser.replyChanged(); + else + emit chooser.mainChanged(); + + chooser.polish(); + } else if (status == QQmlIncubator::Error) { + auto errors_ = errors(); + for (const auto &e : std::as_const(errors_)) + nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); + } +} + +void +EventDelegateChooser::updatePolish() +{ + auto mainChild = qobject_cast<QQuickItem *>(eventIncubator.object()); + auto replyChild = qobject_cast<QQuickItem *>(replyIncubator.object()); + + // nhlog::ui()->trace("POLISHING {}", (void *)this); + + auto layoutItem = [this](QQuickItem *item, int inset) { + if (item) { + QObject::disconnect(item, &QQuickItem::implicitWidthChanged, this, &QQuickItem::polish); + + auto attached = qobject_cast<EventDelegateChooserAttachedType *>( + qmlAttachedPropertiesObject<EventDelegateChooser>(item)); + Q_ASSERT(attached != nullptr); + + int maxWidth = maxWidth_ - inset; + + // in theory we could also reset the width, but that doesn't seem to work nicely for + // text areas because of how they cache it. + if (attached->maxWidth() > 0) + item->setWidth(attached->maxWidth()); + else + item->setWidth(maxWidth); + item->ensurePolished(); + auto width = item->implicitWidth(); + + if (width < 1 || width > maxWidth) + width = maxWidth; + + if (attached->maxWidth() > 0 && width > attached->maxWidth()) + width = attached->maxWidth(); + + if (attached->keepAspectRatio()) { + auto height = width * attached->aspectRatio(); + if (attached->maxHeight() && height > attached->maxHeight()) { + height = attached->maxHeight(); + width = height / attached->aspectRatio(); + } + + item->setHeight(height); + } + + item->setWidth(width); + item->ensurePolished(); + + QObject::connect(item, &QQuickItem::implicitWidthChanged, this, &QQuickItem::polish); + } + }; + + layoutItem(mainChild, mainInset_); + layoutItem(replyChild, replyInset_); +} + +void +EventDelegateChooserAttachedType::polishChooser() +{ + auto p = parent(); + if (p) { + auto chooser = qobject_cast<EventDelegateChooser *>(p->parent()); + if (chooser) { + chooser->polish(); + } + } +} diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h new file mode 100644 index 00000000..df1953ab --- /dev/null +++ b/src/timeline/EventDelegateChooser.h @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractItemModel> +#include <QQmlComponent> +#include <QQmlIncubator> +#include <QQmlListProperty> +#include <QQuickItem> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +#include "TimelineModel.h" + +class EventDelegateChooserAttachedType : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool keepAspectRatio READ keepAspectRatio WRITE setKeepAspectRatio NOTIFY + keepAspectRatioChanged) + Q_PROPERTY(double aspectRatio READ aspectRatio WRITE setAspectRatio NOTIFY aspectRatioChanged) + Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) + Q_PROPERTY(int maxHeight READ maxHeight WRITE setMaxHeight NOTIFY maxHeightChanged) + Q_PROPERTY(bool isReply READ isReply WRITE setIsReply NOTIFY isReplyChanged) + + QML_ANONYMOUS +public: + EventDelegateChooserAttachedType(QObject *parent) + : QObject(parent) + { + } + + bool keepAspectRatio() const { return keepAspectRatio_; } + void setKeepAspectRatio(bool fill) + { + if (fill != keepAspectRatio_) { + keepAspectRatio_ = fill; + emit keepAspectRatioChanged(); + polishChooser(); + } + } + + double aspectRatio() const { return aspectRatio_; } + void setAspectRatio(double fill) + { + aspectRatio_ = fill; + emit aspectRatioChanged(); + polishChooser(); + } + + int maxWidth() const { return maxWidth_; } + void setMaxWidth(int fill) + { + maxWidth_ = fill; + emit maxWidthChanged(); + polishChooser(); + } + + int maxHeight() const { return maxHeight_; } + void setMaxHeight(int fill) + { + maxHeight_ = fill; + emit maxHeightChanged(); + } + + bool isReply() const { return isReply_; } + void setIsReply(bool fill) + { + if (fill != isReply_) { + isReply_ = fill; + emit isReplyChanged(); + polishChooser(); + } + } + +signals: + void keepAspectRatioChanged(); + void aspectRatioChanged(); + void maxWidthChanged(); + void maxHeightChanged(); + void isReplyChanged(); + +private: + void polishChooser(); + + double aspectRatio_ = 1.; + int maxWidth_ = -1; + int maxHeight_ = -1; + bool keepAspectRatio_ = false; + bool isReply_ = false; +}; + +class EventDelegateChoice : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QList<int> roleValues READ roleValues WRITE setRoleValues NOTIFY roleValuesChanged + REQUIRED FINAL) + Q_PROPERTY( + QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged REQUIRED FINAL) + + [[nodiscard]] QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + [[nodiscard]] QList<int> roleValues() const; + void setRoleValues(const QList<int> &value); + +signals: + void delegateChanged(); + void roleValuesChanged(); + void changed(); + +private: + QList<int> roleValues_; + QQmlComponent *delegate_ = nullptr; +}; + +class EventDelegateChooser : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "choices") + + QML_ATTACHED(EventDelegateChooserAttachedType) + + Q_PROPERTY(QQmlListProperty<EventDelegateChoice> choices READ choices CONSTANT FINAL) + Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) + Q_PROPERTY(QQuickItem *reply READ reply NOTIFY replyChanged FINAL) + Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) + Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) + Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) + Q_PROPERTY(bool sameWidth READ sameWidth WRITE setSameWidth NOTIFY sameWidthChanged) + Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) + Q_PROPERTY(int replyInset READ replyInset WRITE setReplyInset NOTIFY replyInsetChanged) + Q_PROPERTY(int mainInset READ mainInset WRITE setMainInset NOTIFY mainInsetChanged) + +public: + QQmlListProperty<EventDelegateChoice> choices(); + + [[nodiscard]] QQuickItem *main() const + { + return qobject_cast<QQuickItem *>(eventIncubator.object()); + } + [[nodiscard]] QQuickItem *reply() const + { + return qobject_cast<QQuickItem *>(replyIncubator.object()); + } + + bool sameWidth() const { return sameWidth_; } + void setSameWidth(bool width) + { + sameWidth_ = width; + emit sameWidthChanged(); + } + int maxWidth() const { return maxWidth_; } + void setMaxWidth(int width) + { + maxWidth_ = width; + emit maxWidthChanged(); + polish(); + } + + int replyInset() const { return replyInset_; } + void setReplyInset(int width) + { + replyInset_ = width; + emit replyInsetChanged(); + polish(); + } + + int mainInset() const { return mainInset_; } + void setMainInset(int width) + { + mainInset_ = width; + emit mainInsetChanged(); + polish(); + } + + void setRoom(TimelineModel *m) + { + if (m != room_) { + room_ = m; + emit roomChanged(); + + if (isComponentComplete()) { + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + } + } + } + [[nodiscard]] TimelineModel *room() { return room_; } + + void setEventId(QString idx) + { + eventId_ = idx; + emit eventIdChanged(); + + if (isComponentComplete()) + eventIncubator.reset(eventId_); + } + [[nodiscard]] QString eventId() const { return eventId_; } + void setReplyTo(QString id) + { + replyId = id; + emit replyToChanged(); + + if (isComponentComplete()) + replyIncubator.reset(replyId); + } + [[nodiscard]] QString replyTo() const { return replyId; } + + void componentComplete() override; + + static EventDelegateChooserAttachedType *qmlAttachedProperties(QObject *object) + { + return new EventDelegateChooserAttachedType(object); + } + + void updatePolish() override; + +signals: + void mainChanged(); + void replyChanged(); + void roomChanged(); + void eventIdChanged(); + void replyToChanged(); + void sameWidthChanged(); + void maxWidthChanged(); + void replyInsetChanged(); + void mainInsetChanged(); + +private: + struct DelegateIncubator final : public QQmlIncubator + { + DelegateIncubator(EventDelegateChooser &parent, bool forReply) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + , forReply(forReply) + { + } + void setInitialState(QObject *object) override; + void statusChanged(QQmlIncubator::Status status) override; + + void reset(QString id); + + EventDelegateChooser &chooser; + bool forReply; + QString currentId; + + QString instantiatedId; + int instantiatedRole = -1; + QAbstractItemModel *instantiatedModel = nullptr; + int oldType = -1; + }; + + QVariant roleValue_; + QList<EventDelegateChoice *> choices_; + DelegateIncubator eventIncubator{*this, false}; + DelegateIncubator replyIncubator{*this, true}; + TimelineModel *room_{nullptr}; + QString eventId_; + QString replyId; + bool sameWidth_ = false; + int maxWidth_ = 400; + int replyInset_ = 0; + int mainInset_ = 0; + + static void appendChoice(QQmlListProperty<EventDelegateChoice> *, EventDelegateChoice *); + static qsizetype choiceCount(QQmlListProperty<EventDelegateChoice> *); + static EventDelegateChoice *choice(QQmlListProperty<EventDelegateChoice> *, qsizetype index); + static void clearChoices(QQmlListProperty<EventDelegateChoice> *); +}; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 63b67474..3db70f77 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -800,7 +800,7 @@ EventStore::enableKeyRequests(bool suppressKeyRequests_) { if (!suppressKeyRequests_) { auto keys = decryptedEvents_.keys(); - for (const auto &key : qAsConst(keys)) + for (const auto &key : std::as_const(keys)) if (key.room == this->room_id_) decryptedEvents_.remove(key); suppressKeyRequests = false; @@ -843,8 +843,8 @@ EventStore::get(const std::string &id, nhlog::net()->error( "Failed to retrieve event with id {}, which was " "requested to show the replyTo for event {}", - relatedTo, - id); + id, + relatedTo); return; } emit eventFetched(id, relatedTo, timeline); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index a371e2b4..fcec8e9c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -491,7 +491,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow QString body; bool firstLine = true; auto lines = QStringView(related.quoted_body).split(u'\n'); - for (auto line : qAsConst(lines)) { + for (auto line : std::as_const(lines)) { if (firstLine) { firstLine = false; body = QStringLiteral("> <%1> %2\n").arg(related.quoted_user, line); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 8d8d2977..5ea6f8c8 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -340,7 +340,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) int total_unread_msgs = 0; - for (const auto &room : qAsConst(models)) { + for (const auto &room : std::as_const(models)) { if (!room.isNull() && !room->isSpace()) total_unread_msgs += room->notificationCount(); } @@ -541,7 +541,7 @@ RoomlistModel::sync(const mtx::responses::Sync &sync_) if (auto t = std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( &ev)) { - std::vector<QString> typing; + QStringList typing; typing.reserve(t->content.user_ids.size()); for (const auto &user : t->content.user_ids) { if (user != http::client()->user_id().to_string()) @@ -948,7 +948,7 @@ FilteredRoomlistModel::updateHiddenTagsAndSpaces() hideDMs = false; auto hidden = UserSettings::instance()->hiddenTags(); - for (const auto &t : qAsConst(hidden)) { + for (const auto &t : std::as_const(hidden)) { if (t.startsWith(u"tag:")) hiddenTags.push_back(t.mid(4)); else if (t.startsWith(u"space:")) diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp index 6f2f9e7a..c2d9e31b 100644 --- a/src/timeline/TimelineFilter.cpp +++ b/src/timeline/TimelineFilter.cpp @@ -163,14 +163,20 @@ TimelineFilter::setSource(TimelineModel *s) this->setSourceModel(s); - connect(s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged); - connect( - s, &TimelineModel::fetchedMore, this, &TimelineFilter::fetchAgain, Qt::QueuedConnection); - connect(s, - &TimelineModel::dataChanged, - this, - &TimelineFilter::sourceDataChanged, - Qt::QueuedConnection); + if (s) { + connect( + s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged); + connect(s, + &TimelineModel::fetchedMore, + this, + &TimelineFilter::fetchAgain, + Qt::QueuedConnection); + connect(s, + &TimelineModel::dataChanged, + this, + &TimelineFilter::sourceDataChanged, + Qt::QueuedConnection); + } // reset the search index a second time just to be safe. incrementalSearchIndex = 0; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index e13b56d7..d85a9516 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -534,6 +534,7 @@ TimelineModel::roleNames() const {IsOnlyEmoji, "isOnlyEmoji"}, {Body, "body"}, {FormattedBody, "formattedBody"}, + {FormattedStateEvent, "formattedStateEvent"}, {IsSender, "isSender"}, {UserId, "userId"}, {UserName, "userName"}, @@ -562,6 +563,7 @@ TimelineModel::roleNames() const {ReplyTo, "replyTo"}, {ThreadId, "threadId"}, {Reactions, "reactions"}, + {Room, "room"}, {RoomId, "roomId"}, {RoomName, "roomName"}, {RoomTopic, "roomTopic"}, @@ -601,12 +603,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case UserName: return QVariant(displayName(QString::fromStdString(acc::sender(event)))); case UserPowerlevel: { - return static_cast<qlonglong>(mtx::events::state::PowerLevels{ - cache::client() - ->getStateEvent<mtx::events::state::PowerLevels>(room_id_.toStdString()) - .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) - .content} - .user_level(acc::sender(event))); + return static_cast<qlonglong>( + permissions_.powerlevelEvent().user_level(acc::sender(event))); } case Day: { @@ -694,8 +692,90 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r formattedBody_.replace(curImg, imgReplacement); } + if (auto effectMessage = + std::get_if<mtx::events::RoomEvent<mtx::events::msg::ElementEffect>>(&event)) { + if (effectMessage->content.msgtype == std::string_view("nic.custom.confetti")) { + formattedBody_.append(QUtf8StringView(u8"🎊")); + } else if (effectMessage->content.msgtype == + std::string_view("io.element.effect.rainfall")) { + formattedBody_.append(QUtf8StringView(u8"🌧️")); + } + } + return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_))); } + case FormattedStateEvent: { + if (mtx::accessors::is_state_event(event)) { + return std::visit( + [this](const auto &e) { + constexpr auto t = mtx::events::state_content_to_type<decltype(e.content)>; + if constexpr (t == mtx::events::EventType::RoomServerAcl) + return tr("%1 changed which servers are allowed in this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::RoomName) { + if (e.content.name.empty()) + return tr("%1 removed the room name.") + .arg(displayName(QString::fromStdString(e.sender))); + else + return tr("%1 changed the room name to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(e.content.name).toHtmlEscaped()); + } else if constexpr (t == mtx::events::EventType::RoomTopic) { + if (e.content.topic.empty()) + return tr("%1 removed the topic.") + .arg(displayName(QString::fromStdString(e.sender))); + else + return tr("%1 changed the topic to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(e.content.topic).toHtmlEscaped()); + } else if constexpr (t == mtx::events::EventType::RoomAvatar) { + if (e.content.url.starts_with("mxc://")) + return tr("%1 changed the room avatar to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QStringLiteral("<img height=\"32\" src=\"%1\">") + .arg(QUrl::toPercentEncoding( + QString::fromStdString(e.content.url)))); + else + return tr("%1 removed the room avatar.") + .arg(displayName(QString::fromStdString(e.sender))); + } else if constexpr (t == mtx::events::EventType::RoomPinnedEvents) + return tr("%1 changed the pinned messages.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::ImagePackInRoom) + formatImagePackEvent(e); + else if constexpr (t == mtx::events::EventType::RoomCanonicalAlias) + return tr("%1 changed the addresses for this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::SpaceParent) + return tr("%1 changed the parent communities for this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::RoomCreate) + return tr("%1 created and configured room: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(room_id_); + else if constexpr (t == mtx::events::EventType::RoomPowerLevels) + return formatPowerLevelEvent(e); + else if constexpr (t == mtx::events::EventType::PolicyRuleRoom) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::PolicyRuleUser) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::PolicyRuleServer) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::RoomHistoryVisibility) + return formatHistoryVisibilityEvent(e); + else if constexpr (t == mtx::events::EventType::RoomGuestAccess) + return formatGuestAccessEvent(e); + else if constexpr (t == mtx::events::EventType::RoomMember) + return formatMemberEvent(e); + + return tr("%1 changed unknown state event %2.") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(to_string(e.type))); + }, + event); + } + return QString(); + } case Url: return QVariant(QString::fromStdString(url(event))); case ThumbnailUrl: @@ -830,6 +910,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r auto id = relations(event).replaces().value_or(event_id(event)); return QVariant::fromValue(events.reactions(id)); } + case Room: + return QVariant::fromValue(this); case RoomId: return QVariant(room_id_); case RoomName: @@ -909,8 +991,11 @@ TimelineModel::data(const QModelIndex &index, int role) const void TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const { - if (index.row() < 0 && index.row() >= rowCount()) + if (index.row() < 0 && index.row() >= rowCount()) { + for (QModelRoleData &roleData : roleDataSpan) + roleData.clearData(); return; + } // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems if (index.row() + 1 == rowCount() && !m_paginationInProgress) @@ -918,8 +1003,35 @@ TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSp auto event = events.get(rowCount() - index.row() - 1); - if (!event) + if (!event) { + for (QModelRoleData &roleData : roleDataSpan) + roleData.clearData(); + return; + } + + for (QModelRoleData &roleData : roleDataSpan) { + roleData.setData(data(*event, roleData.role())); + } +} + +void +TimelineModel::multiData(const QString &id, + const QString &relatedTo, + QModelRoleDataSpan roleDataSpan) const +{ + if (id.isEmpty()) { + for (QModelRoleData &roleData : roleDataSpan) + roleData.clearData(); + return; + } + + auto event = events.get(id.toStdString(), relatedTo.toStdString()); + + if (!event) { + for (QModelRoleData &roleData : roleDataSpan) + roleData.clearData(); return; + } for (QModelRoleData &roleData : roleDataSpan) { int role = roleData.role(); @@ -2208,7 +2320,7 @@ TimelineModel::markSpecialEffectsDone() } QString -TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor &bg) +TimelineModel::formatTypingUsers(const QStringList &users, const QColor &bg) { QString temp = tr("%1 and %2 are typing.", @@ -2255,7 +2367,7 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor }; uidWithoutLast.reserve(static_cast<int>(users.size())); - for (size_t i = 0; i + 1 < users.size(); i++) { + for (qsizetype i = 0; i + 1 < users.size(); i++) { uidWithoutLast.append(formatUser(users[i])); } @@ -2300,20 +2412,13 @@ TimelineModel::formatJoinRuleEvent(const QString &id) } QString -TimelineModel::formatGuestAccessEvent(const QString &id) +TimelineModel::formatGuestAccessEvent( + const mtx::events::StateEvent<mtx::events::state::GuestAccess> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); - if (!event) - return {}; - - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.guest_access) { + switch (event.content.guest_access) { case mtx::events::state::AccessState::CanJoin: return tr("%1 made the room open to guests.").arg(name); case mtx::events::state::AccessState::Forbidden: @@ -2324,21 +2429,13 @@ TimelineModel::formatGuestAccessEvent(const QString &id) } QString -TimelineModel::formatHistoryVisibilityEvent(const QString &id) +TimelineModel::formatHistoryVisibilityEvent( + const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); - - if (!event) - return {}; - - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.history_visibility) { + switch (event.content.history_visibility) { case mtx::events::state::Visibility::WorldReadable: return tr("%1 made the room history world readable. Events may be now read by " "non-joined people.") @@ -2356,32 +2453,25 @@ TimelineModel::formatHistoryVisibilityEvent(const QString &id) } QString -TimelineModel::formatPowerLevelEvent(const QString &id) +TimelineModel::formatPowerLevelEvent( + const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); - if (!event) - return QString(); - mtx::events::StateEvent<mtx::events::state::PowerLevels> const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(tempPrevEvent); } } - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString sender_name = utils::replaceEmoji(displayName(user)); // Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and // "Moderator" powerlevels. - auto administrator_power_level = event->content.state_level("m.room.power_levels"); - auto moderator_power_level = event->content.redact; - auto default_powerlevel = event->content.users_default; + auto administrator_power_level = event.content.state_level("m.room.power_levels"); + auto moderator_power_level = event.content.redact; + auto default_powerlevel = event.content.users_default; if (!prevEvent) return tr("%1 has changed the room's permissions.").arg(sender_name); @@ -2391,7 +2481,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id) auto numberOfAffected = 0; // We do only compare to people with explicit PL. Usually others are not going to be // affected either way and this is cheaper to iterate over. - for (auto const &[mxid, currentPowerlevel] : event->content.users) { + for (auto const &[mxid, currentPowerlevel] : event.content.users) { if (currentPowerlevel == newPowerlevelSetting && prevEvent->content.user_level(mxid) < newPowerlevelSetting) { numberOfAffected++; @@ -2405,16 +2495,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) QStringList resultingMessage{}; // These affect only a few people. Therefor we can print who is affected. - if (event->content.kick != prevEvent->content.kick) { + if (event.content.kick != prevEvent->content.kick) { auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.kick) - .arg(event->content.kick); + .arg(event.content.kick); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.kick > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.kick); + if (event.content.kick > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.kick); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2436,16 +2526,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.redact != prevEvent->content.redact) { + if (event.content.redact != prevEvent->content.redact) { auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.redact) - .arg(event->content.redact); + .arg(event.content.redact); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.redact > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.redact); + if (event.content.redact > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.redact); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2468,16 +2558,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.ban != prevEvent->content.ban) { + if (event.content.ban != prevEvent->content.ban) { auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.ban) - .arg(event->content.ban); + .arg(event.content.ban); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.ban > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.ban); + if (event.content.ban > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.ban); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2499,17 +2589,17 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.state_default != prevEvent->content.state_default) { + if (event.content.state_default != prevEvent->content.state_default) { auto default_message = tr("%1 has changed the room's state_default powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.state_default) - .arg(event->content.state_default); + .arg(event.content.state_default); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.state_default > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.kick); + if (event.content.state_default > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.kick); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2533,42 +2623,42 @@ TimelineModel::formatPowerLevelEvent(const QString &id) // These affect potentially the whole room. We there for do not calculate who gets affected // by this to prevent huge lists of people. - if (event->content.invite != prevEvent->content.invite) { + if (event.content.invite != prevEvent->content.invite) { resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.") .arg(sender_name, QString::number(prevEvent->content.invite), - QString::number(event->content.invite))); + QString::number(event.content.invite))); } - if (event->content.events_default != prevEvent->content.events_default) { - if ((event->content.events_default > default_powerlevel) && + if (event.content.events_default != prevEvent->content.events_default) { + if ((event.content.events_default > default_powerlevel) && prevEvent->content.events_default <= default_powerlevel) { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " "users can now not send any events.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); - } else if ((event->content.events_default < prevEvent->content.events_default) && - (event->content.events_default < default_powerlevel) && + QString::number(event.content.events_default))); + } else if ((event.content.events_default < prevEvent->content.events_default) && + (event.content.events_default < default_powerlevel) && (prevEvent->content.events_default > default_powerlevel)) { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " "users can now send events that are not otherwise restricted.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); + QString::number(event.content.events_default))); } else { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); + QString::number(event.content.events_default))); } } // Compare if a Powerlevel of a user changed - for (auto const &[mxid, powerlevel] : event->content.users) { + for (auto const &[mxid, powerlevel] : event.content.users) { auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid))); if (prevEvent->content.user_level(mxid) != powerlevel) { if (powerlevel >= administrator_power_level) { @@ -2593,7 +2683,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } // Handle added/removed/changed event type - for (auto const &[event_type, powerlevel] : event->content.events) { + for (auto const &[event_type, powerlevel] : event.content.events) { auto prev_not_present = prevEvent->content.events.find(event_type) == prevEvent->content.events.end(); @@ -2632,26 +2722,19 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } QString -TimelineModel::formatImagePackEvent(const QString &id) +TimelineModel::formatImagePackEvent( + const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(e); - if (!event) - return {}; - mtx::events::StateEvent<mtx::events::msc2545::ImagePack> const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(tempPrevEvent); } } - const auto &newImages = event->content.images; + const auto &newImages = event.content.images; const auto oldImages = prevEvent ? prevEvent->content.images : decltype(newImages){}; auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); @@ -2674,12 +2757,12 @@ TimelineModel::formatImagePackEvent(const QString &id) auto added = calcChange(newImages, oldImages); auto removed = calcChange(oldImages, newImages); - auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event->sender))); + auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event.sender))); const auto packId = [&event]() -> QString { - if (event->content.pack && !event->content.pack->display_name.empty()) { - return event->content.pack->display_name.c_str(); - } else if (!event->state_key.empty()) { - return event->state_key.c_str(); + if (event.content.pack && !event.content.pack->display_name.empty()) { + return event.content.pack->display_name.c_str(); + } else if (!event.state_key.empty()) { + return event.state_key.c_str(); } return tr("(empty)"); }(); @@ -2704,7 +2787,7 @@ TimelineModel::formatImagePackEvent(const QString &id) } QString -TimelineModel::formatPolicyRule(const QString &id) +TimelineModel::formatPolicyRule(const QString &id) const { auto idStr = id.toStdString(); auto e = events.get(idStr, ""); @@ -2905,34 +2988,27 @@ TimelineModel::joinReplacementRoom(const QString &id) } QString -TimelineModel::formatMemberEvent(const QString &id) +TimelineModel::formatMemberEvent( + const mtx::events::StateEvent<mtx::events::state::Member> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return {}; - mtx::events::StateEvent<mtx::events::state::Member> const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent); } } - QString user = QString::fromStdString(event->state_key); + QString user = QString::fromStdString(event.state_key); QString name = utils::replaceEmoji(displayName(user)); QString rendered; - QString sender = QString::fromStdString(event->sender); + QString sender = QString::fromStdString(event.sender); QString senderName = utils::replaceEmoji(displayName(sender)); // see table https://matrix.org/docs/spec/client_server/latest#m-room-member using namespace mtx::events::state; - switch (event->content.membership) { + switch (event.content.membership) { case Membership::Invite: rendered = tr("%1 invited %2.").arg(senderName, name); break; @@ -2941,9 +3017,8 @@ TimelineModel::formatMemberEvent(const QString &id) QString oldName = utils::replaceEmoji( QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); - bool displayNameChanged = - prevEvent->content.display_name != event->content.display_name; - bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url; + bool displayNameChanged = prevEvent->content.display_name != event.content.display_name; + bool avatarChanged = prevEvent->content.avatar_url != event.content.avatar_url; if (displayNameChanged && avatarChanged) rendered = tr("%1 has changed their avatar and changed their " @@ -2958,30 +3033,30 @@ TimelineModel::formatMemberEvent(const QString &id) // the case of nothing changed but join follows join shouldn't happen, so // just show it as join } else { - if (event->content.join_authorised_via_users_server.empty()) + if (event.content.join_authorised_via_users_server.empty()) rendered = tr("%1 joined.").arg(name); else rendered = tr("%1 joined via authorisation from %2's server.") .arg(name, - QString::fromStdString(event->content.join_authorised_via_users_server)); + QString::fromStdString(event.content.join_authorised_via_users_server)); } break; case Membership::Leave: if (!prevEvent || prevEvent->content.membership == Membership::Join) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 left the room.").arg(name); else rendered = tr("%2 kicked %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Invite) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 rejected their invite.").arg(name); else rendered = tr("%2 revoked the invite to %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Ban) { rendered = tr("%2 unbanned %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Knock) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 redacted their knock.").arg(name); else rendered = tr("%2 rejected the knock from %1.").arg(name, senderName); @@ -3000,8 +3075,8 @@ TimelineModel::formatMemberEvent(const QString &id) break; } - if (event->content.reason != "") { - rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); + if (event.content.reason != "") { + rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event.content.reason)); } return rendered; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index c8947891..eefe921f 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -199,8 +199,8 @@ class TimelineModel final : public QAbstractListModel QML_UNCREATABLE("") Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY - typingUsersChanged) + Q_PROPERTY( + QStringList typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged) Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged) Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) @@ -238,6 +238,7 @@ public: IsOnlyEmoji, Body, FormattedBody, + FormattedStateEvent, IsSender, UserId, UserName, @@ -266,6 +267,7 @@ public: ReplyTo, ThreadId, Reactions, + Room, RoomId, RoomName, RoomTopic, @@ -286,6 +288,8 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override; + void + multiData(const QString &id, const QString &relatedTo, QModelRoleDataSpan roleDataSpan) const; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo); Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const @@ -302,17 +306,22 @@ public: Q_INVOKABLE QString displayName(const QString &id) const; Q_INVOKABLE QString avatarUrl(const QString &id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; - Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, const QColor &bg); + Q_INVOKABLE QString formatTypingUsers(const QStringList &users, const QColor &bg); Q_INVOKABLE bool showAcceptKnockButton(const QString &id); Q_INVOKABLE void acceptKnock(const QString &id); Q_INVOKABLE void joinReplacementRoom(const QString &id); - Q_INVOKABLE QString formatMemberEvent(const QString &id); + Q_INVOKABLE QString + formatMemberEvent(const mtx::events::StateEvent<mtx::events::state::Member> &event) const; Q_INVOKABLE QString formatJoinRuleEvent(const QString &id); - Q_INVOKABLE QString formatHistoryVisibilityEvent(const QString &id); - Q_INVOKABLE QString formatGuestAccessEvent(const QString &id); - Q_INVOKABLE QString formatPowerLevelEvent(const QString &id); - Q_INVOKABLE QString formatImagePackEvent(const QString &id); - Q_INVOKABLE QString formatPolicyRule(const QString &id); + QString formatHistoryVisibilityEvent( + const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const; + QString + formatGuestAccessEvent(const mtx::events::StateEvent<mtx::events::state::GuestAccess> &) const; + QString formatPowerLevelEvent( + const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const; + QString formatImagePackEvent( + const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const; + Q_INVOKABLE QString formatPolicyRule(const QString &id) const; Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); Q_INVOKABLE void viewRawMessage(const QString &id); @@ -396,14 +405,14 @@ public slots: void lastReadIdOnWindowFocus(); void checkAfterFetch(); QVariantMap getDump(const QString &eventId, const QString &relatedTo) const; - void updateTypingUsers(const std::vector<QString> &users) + void updateTypingUsers(const QStringList &users) { if (this->typingUsers_ != users) { this->typingUsers_ = users; emit typingUsersChanged(typingUsers_); } } - std::vector<QString> typingUsers() const { return typingUsers_; } + QStringList typingUsers() const { return typingUsers_; } bool paginationInProgress() const { return m_paginationInProgress; } QString reply() const { return reply_; } void setReply(const QString &newReply); @@ -462,7 +471,7 @@ signals: void redactionFailed(QString id); void mediaCached(QString mxcUrl, QString cacheUrl); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); - void typingUsersChanged(std::vector<QString> users); + void typingUsersChanged(QStringList users); void replyChanged(QString reply); void editChanged(QString reply); void threadChanged(QString id); @@ -523,7 +532,7 @@ private: QString currentId, currentReadId; QString reply_, edit_, thread_; QString textBeforeEdit, replyBeforeEdit; - std::vector<QString> typingUsers_; + QStringList typingUsers_; TimelineViewManager *manager_; diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp index 14f5dbd8..ffe54c71 100644 --- a/src/ui/MxcAnimatedImage.cpp +++ b/src/ui/MxcAnimatedImage.cpp @@ -102,10 +102,12 @@ MxcAnimatedImage::startDownload() if (buffer.bytesAvailable() < 4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM movie.setCacheMode(QMovie::CacheAll); - if (play_) + if (play_ && movie.frameCount() > 1) movie.start(); - else + else { movie.jumpToFrame(0); + movie.setPaused(true); + } emit loadedChanged(); update(); }); @@ -173,6 +175,9 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD if (!imageDirty) return oldNode; + if (clipRect().isEmpty()) + return oldNode; + imageDirty = false; QSGImageNode *n = static_cast<QSGImageNode *>(oldNode); if (!n) { diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h index c9f89764..1f2c0b74 100644 --- a/src/ui/MxcAnimatedImage.h +++ b/src/ui/MxcAnimatedImage.h @@ -29,6 +29,7 @@ public: connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload); connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame); setFlag(QQuickItem::ItemHasContents); + setFlag(QQuickItem::ItemObservesViewport); // setAcceptHoverEvents(true); } @@ -55,7 +56,12 @@ public: { if (play_ != newPlay) { play_ = newPlay; - movie.setPaused(!play_); + if (movie.frameCount() > 1) + movie.setPaused(!play_); + else { + movie.jumpToFrame(0); + movie.setPaused(true); + } emit playChanged(); } } @@ -77,7 +83,8 @@ private slots: { currentFrame = frame; imageDirty = true; - update(); + if (!clipRect().isEmpty()) + update(); } private: diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp index 63c9aa6f..348ef5d8 100644 --- a/src/ui/NhekoDropArea.cpp +++ b/src/ui/NhekoDropArea.cpp @@ -38,6 +38,7 @@ NhekoDropArea::dropEvent(QDropEvent *event) auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_); if (model) { model->input()->insertMimeData(event->mimeData()); + ChatPage::instance()->timelineManager()->focusMessageInput(); } } } diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp index 769f2c8d..5f4184b3 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp @@ -728,7 +728,7 @@ RoomSettingsAllowedRoomsModel::RoomSettingsAllowedRoomsModel(RoomSettings *paren this->listedRoomIds = QStringList(parentSpaces.begin(), parentSpaces.end()); - for (const auto &e : qAsConst(this->allowedRoomIds)) { + for (const auto &e : std::as_const(this->allowedRoomIds)) { if (!this->parentSpaces.count(e)) this->listedRoomIds.push_back(e); } diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp index 5479ba31..46679e71 100644 --- a/src/voip/CallManager.cpp +++ b/src/voip/CallManager.cpp @@ -92,7 +92,8 @@ CallManager::CallManager(QObject *parent) if (QGuiApplication::platformName() != QStringLiteral("wayland")) { // Selected by default screenShareType_ = ScreenShareType::X11; - std::swap(screenShareTypes_[0], screenShareTypes_[1]); + if (screenShareTypes_.size() >= 2) + std::swap(screenShareTypes_[0], screenShareTypes_[1]); } } #endif |