summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AliasEditModel.cpp4
-rw-r--r--src/Cache.cpp207
-rw-r--r--src/Cache.h3
-rw-r--r--src/Cache_p.h16
-rw-r--r--src/ChatPage.cpp14
-rw-r--r--src/InviteesModel.cpp4
-rw-r--r--src/MxcImageProvider.cpp2
-rw-r--r--src/PowerlevelsEditModels.cpp20
-rw-r--r--src/UserSettingsPage.cpp4
-rw-r--r--src/Utils.cpp2
-rw-r--r--src/encryption/Olm.cpp2
-rw-r--r--src/main.cpp9
-rw-r--r--src/notifications/Manager.cpp2
-rw-r--r--src/notifications/ManagerLinux.cpp4
-rw-r--r--src/timeline/CommunitiesModel.cpp2
-rw-r--r--src/timeline/DelegateChooser.cpp4
-rw-r--r--src/timeline/EventDelegateChooser.cpp360
-rw-r--r--src/timeline/EventDelegateChooser.h276
-rw-r--r--src/timeline/EventStore.cpp6
-rw-r--r--src/timeline/InputBar.cpp2
-rw-r--r--src/timeline/RoomlistModel.cpp6
-rw-r--r--src/timeline/TimelineFilter.cpp22
-rw-r--r--src/timeline/TimelineModel.cpp311
-rw-r--r--src/timeline/TimelineModel.h35
-rw-r--r--src/ui/MxcAnimatedImage.cpp9
-rw-r--r--src/ui/MxcAnimatedImage.h11
-rw-r--r--src/ui/NhekoDropArea.cpp1
-rw-r--r--src/ui/RoomSettings.cpp2
-rw-r--r--src/voip/CallManager.cpp3
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