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
|