diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000000000000000000000000000000000000..1a5279993de34a287779282866750468991ca80d
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1 @@
+Checks: '-*,analyzer-cplusplus.NewDeleteLeaks'
diff --git a/src/app/accountadapter.cpp b/src/app/accountadapter.cpp
index 95cb761120df8a32ddf714980cf97b64c05fd334..e022e70d34890c2796933eb593218659ab50068b 100644
--- a/src/app/accountadapter.cpp
+++ b/src/app/accountadapter.cpp
@@ -22,7 +22,7 @@
 
 #include "appsettingsmanager.h"
 #include "qtutils.h"
-#include "qmlregister.h"
+#include "accountlistmodel.h"
 
 #include <QtConcurrent/QtConcurrent>
 
@@ -33,14 +33,7 @@ AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager,
     : QmlAdapterBase(instance, parent)
     , settingsManager_(settingsManager)
     , systemTray_(systemTray)
-    , accountListModel_(new AccountListModel(instance))
-    , deviceItemListModel_(new DeviceItemListModel(instance, parent))
-    , moderatorListModel_(new ModeratorListModel(instance, parent))
 {
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, accountListModel_.get(), "AccountListModel");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, deviceItemListModel_.get(), "DeviceItemListModel");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, moderatorListModel_.get(), "ModeratorListModel");
-
     connect(&lrcInstance_->accountModel(),
             &AccountModel::accountStatusChanged,
             this,
@@ -53,8 +46,13 @@ AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager,
 
     connect(systemTray_,
             &SystemTray::countChanged,
-            accountListModel_.get(),
+            qApp->property("AccountListModel").value<AccountListModel*>(),
             &AccountListModel::updateNotifications);
+
+    // Switch account to the specified index when an account is added.
+    connect(this, &AccountAdapter::accountAdded, this, [this](const QString&, int index) {
+        changeAccount(index);
+    });
 }
 
 AccountModel*
diff --git a/src/app/accountadapter.h b/src/app/accountadapter.h
index 0407fc95af3e01296efaeaf428b82d025b383491..8ff685db73dcee5944273417d3ead86e0508cbf5 100644
--- a/src/app/accountadapter.h
+++ b/src/app/accountadapter.h
@@ -20,21 +20,20 @@
 
 #include "qmladapterbase.h"
 
-#include "accountlistmodel.h"
-#include "deviceitemlistmodel.h"
-#include "moderatorlistmodel.h"
 #include "systemtray.h"
 #include "lrcinstance.h"
-#include "utils.h"
 
 #include <QSettings>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class AppSettingsManager;
 
 class AccountAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
 
     Q_PROPERTY(lrc::api::AccountModel* model READ getModel NOTIFY modelChanged)
 
@@ -45,6 +44,13 @@ Q_SIGNALS:
     void modelChanged();
 
 public:
+    static AccountAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new AccountAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
+                                  qApp->property("SystemTray").value<SystemTray*>(),
+                                  qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit AccountAdapter(AppSettingsManager* settingsManager,
                             SystemTray* systemTray,
                             LRCInstance* instance,
@@ -100,9 +106,5 @@ private:
 
     AppSettingsManager* settingsManager_;
     SystemTray* systemTray_;
-
-    QScopedPointer<AccountListModel> accountListModel_;
-    QScopedPointer<DeviceItemListModel> deviceItemListModel_;
-    QScopedPointer<ModeratorListModel> moderatorListModel_;
 };
 Q_DECLARE_METATYPE(AccountAdapter*)
diff --git a/src/app/accountlistmodel.cpp b/src/app/accountlistmodel.cpp
index d64760a0fddae9130156959ee755f9e0028bbcf7..b4a60e1aacbb011c3877e5844d3b0813309f5107 100644
--- a/src/app/accountlistmodel.cpp
+++ b/src/app/accountlistmodel.cpp
@@ -20,11 +20,8 @@
 #include "accountlistmodel.h"
 
 #include "lrcinstance.h"
-#include "utils.h"
 
 #include "api/account.h"
-#include "api/contact.h"
-#include "api/conversation.h"
 
 #include <QDateTime>
 
diff --git a/src/app/accountlistmodel.h b/src/app/accountlistmodel.h
index 67921a523a84477c38343690f30c26be41295361..9ffd94efc40315ce1db9674019d8d1b83ff1e515 100644
--- a/src/app/accountlistmodel.h
+++ b/src/app/accountlistmodel.h
@@ -51,11 +51,9 @@ public:
     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
     QHash<int, QByteArray> roleNames() const override;
 
-    // reset the model when there's new account added
     Q_INVOKABLE void reset();
-
     void updateNotifications();
 
-protected:
+private:
     using Role = AccountList::Role;
 };
diff --git a/src/app/avadapter.h b/src/app/avadapter.h
index 306ce46dd4ef2b4cbab33f2abcccd800456e8cb2..ce5427fb9f7cc5524b5b4be578df58e9f21e9947 100644
--- a/src/app/avadapter.h
+++ b/src/app/avadapter.h
@@ -20,23 +20,31 @@
 
 #include "qmladapterbase.h"
 #include "lrcinstance.h"
+#include "qtutils.h"
+#include "rendererinformationlistmodel.h"
 
 #include <QObject>
 #include <QVariant>
 #include <QString>
-#include <qtutils.h>
-
-#include "rendererinformationlistmodel.h"
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class AvAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(bool, muteCamera)
     QML_RO_PROPERTY(QStringList, windowsNames)
     QML_RO_PROPERTY(QList<QVariant>, windowsIds)
     QML_RO_PROPERTY(QVariant, renderersInfoList)
 
 public:
+    static AvAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new AvAdapter(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit AvAdapter(LRCInstance* instance, QObject* parent = nullptr);
     ~AvAdapter() = default;
 
diff --git a/src/app/avatarregistry.h b/src/app/avatarregistry.h
index 2133f6c36a7850028e48350dd7260644e3f69f65..b224e20f1ed7aaa73ed4acbd2c485c9d23b50daf 100644
--- a/src/app/avatarregistry.h
+++ b/src/app/avatarregistry.h
@@ -20,13 +20,22 @@
 
 #include <QObject>
 #include <QMap>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class LRCInstance;
 
 class AvatarRegistry : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
+
 public:
+    static AvatarRegistry* create(QQmlEngine*, QJSEngine*)
+    {
+        return new AvatarRegistry(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit AvatarRegistry(LRCInstance* instance, QObject* parent = nullptr);
     ~AvatarRegistry() = default;
 
diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp
index af49bdef7521a440ae8bfebe310e132a0c1f1ec5..6ac9b3d5519dcf291e9a8a47ce13b72706895d98 100644
--- a/src/app/calladapter.cpp
+++ b/src/app/calladapter.cpp
@@ -26,8 +26,8 @@
 #include "calladapter.h"
 
 #include "systemtray.h"
-#include "qmlregister.h"
 #include "appsettingsmanager.h"
+#include "pttlistener.h"
 
 #include <api/callmodel.h>
 #include <api/callparticipantsmodel.h>
@@ -45,19 +45,15 @@ CallAdapter::CallAdapter(AppSettingsManager* settingsManager,
     : QmlAdapterBase(instance, parent)
     , systemTray_(systemTray)
     , callInformationListModel_(std::make_unique<CallInformationListModel>())
-    , listener_(new PTTListener(settingsManager, this))
 {
-    // Expose the Push-to-talk listener to QML as a singleton
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, listener_, "PttListener");
+    // Get the PTTListener instance.
+    listener_ = qApp->property("PTTListener").value<PTTListener*>();
 
     set_callInformationList(QVariant::fromValue(callInformationListModel_.get()));
 
     timer = new QTimer(this);
     connect(timer, &QTimer::timeout, this, &CallAdapter::updateAdvancedInformation);
 
-    overlayModel_.reset(new CallOverlayModel(lrcInstance_, listener_, this));
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, overlayModel_.get(), "CallOverlayModel");
-
     accountId_ = lrcInstance_->get_currentAccountId();
     connectCallModel(accountId_);
 
diff --git a/src/app/calladapter.h b/src/app/calladapter.h
index 0b00bc788196ae96bf2da916b3d3cf4f96d36a3b..cefeee01429981c68c8394f9801c85487b3a7a73 100644
--- a/src/app/calladapter.h
+++ b/src/app/calladapter.h
@@ -23,32 +23,34 @@
 #include "lrcinstance.h"
 #include "qmladapterbase.h"
 #include "screensaver.h"
-#include "calloverlaymodel.h"
-
-#ifdef HAVE_GLOBAL_PTT
-#include "pttlistener.h"
-#endif
+#include "callInformationListModel.h"
 
 #include <QObject>
 #include <QString>
 #include <QVariant>
 #include <QSystemTrayIcon>
-
-#include "callInformationListModel.h"
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class SystemTray;
 class AppSettingsManager;
+class PTTListener;
 
 class CallAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(bool, hasCall)
     QML_RO_PROPERTY(QVariant, callInformationList)
 
 public:
-    QTimer* timer;
-    enum MuteStates { UNMUTED, LOCAL_MUTED, MODERATOR_MUTED, BOTH_MUTED };
-    Q_ENUM(MuteStates)
+    static CallAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new CallAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
+                               qApp->property("SystemTray").value<SystemTray*>(),
+                               qApp->property("LRCInstance").value<LRCInstance*>());
+    }
 
     explicit CallAdapter(AppSettingsManager* settingsManager,
                          SystemTray* systemTray,
@@ -56,7 +58,10 @@ public:
                          QObject* parent = nullptr);
     ~CallAdapter();
 
-public:
+    QTimer* timer;
+    enum MuteStates { UNMUTED, LOCAL_MUTED, MODERATOR_MUTED, BOTH_MUTED };
+    Q_ENUM(MuteStates)
+
     Q_INVOKABLE void startTimerInformation();
     Q_INVOKABLE void stopTimerInformation();
     Q_INVOKABLE void placeAudioOnlyCall();
@@ -131,7 +136,6 @@ private:
 
     ScreenSaver screenSaver;
     SystemTray* systemTray_;
-    QScopedPointer<CallOverlayModel> overlayModel_;
     VectorString currentConfSubcalls_;
     std::unique_ptr<CallInformationListModel> callInformationListModel_;
 
diff --git a/src/app/calloverlaymodel.h b/src/app/calloverlaymodel.h
index a93145f9538c96ee2f7a67b4dcb60acdb70bca27..934802bc228e72893be7a14fdc4b54f3e2504e82 100644
--- a/src/app/calloverlaymodel.h
+++ b/src/app/calloverlaymodel.h
@@ -21,8 +21,6 @@
 
 #include "lrcinstance.h"
 #include "qtutils.h"
-#include "mainapplication.h"
-
 #include "pttlistener.h"
 
 #include <QAbstractListModel>
diff --git a/src/app/commoncomponents/ChangePttKeyPopup.qml b/src/app/commoncomponents/ChangePttKeyPopup.qml
index 9cdd0bad8b988e49780a394f799b08004d287285..f5c17bf4ea129a64a2293e778ac961c0ce8bbe14 100644
--- a/src/app/commoncomponents/ChangePttKeyPopup.qml
+++ b/src/app/commoncomponents/ChangePttKeyPopup.qml
@@ -37,7 +37,7 @@ BaseModalDialog {
     button2Role: DialogButtonBox.RejectRole
     button1.onClicked: {
         if (!(pressedKey === Qt.Key_unknown)){
-            PttListener.setPttKey(pressedKey);
+            PTTListener.setPttKey(pressedKey);
             choiceMade(pressedKey);
         }
         close();
@@ -102,7 +102,7 @@ BaseModalDialog {
             id: keyItem
 
             Keys.onPressed: (event)=>{
-                keyLabel.text = PttListener.keyToString(event.key);
+                keyLabel.text = PTTListener.keyToString(event.key);
                 pressedKey = event.key;
             }
         }
diff --git a/src/app/connectioninfolistmodel.cpp b/src/app/connectioninfolistmodel.cpp
index 1d904c735d1b399ec5ab20232b5006c0cbefc47c..9ca1503ef6bd0a1c15b528432831b8421afcc7f1 100644
--- a/src/app/connectioninfolistmodel.cpp
+++ b/src/app/connectioninfolistmodel.cpp
@@ -126,10 +126,6 @@ ConnectionInfoListModel::roleNames() const
 void
 ConnectionInfoListModel::update()
 {
-    const auto accountId = lrcInstance_->get_currentAccountId();
-    if (accountId.isEmpty()) {
-        return;
-    }
     aggregateData();
 }
 
@@ -224,4 +220,4 @@ ConnectionInfoListModel::resetData()
     peerIds_.clear();
     peerData_.clear();
     endResetModel();
-}
\ No newline at end of file
+}
diff --git a/src/app/connectioninfolistmodel.h b/src/app/connectioninfolistmodel.h
index 4b049af1942d30beda994159932a47edf8ab6210..47deb1225d53aff2f525f80240988a9a095c1079 100644
--- a/src/app/connectioninfolistmodel.h
+++ b/src/app/connectioninfolistmodel.h
@@ -41,8 +41,10 @@ enum Role {
 Q_ENUM_NS(Role)
 } // namespace ConnectionInfoList
 
-class ConnectionInfoListModel : public AbstractListModelBase
+class ConnectionInfoListModel final : public AbstractListModelBase
 {
+    Q_OBJECT
+
 public:
     explicit ConnectionInfoListModel(LRCInstance* instance, QObject* parent = nullptr);
 
@@ -56,7 +58,6 @@ private:
     using Role = ConnectionInfoList::Role;
 
     VectorMapStringString connectionInfoList_;
-
     QVector<QString> peerIds_;
     QMap<QString, QMap<QString, QMap<QString, QVariant>>> peerData_;
     void aggregateData();
diff --git a/src/app/contactadapter.cpp b/src/app/contactadapter.cpp
index 91d6995633f41ef8496dc65f99fe1105ed4117e9..b2e6f6b71d35797f013242b8fd17b3a8c5185e97 100644
--- a/src/app/contactadapter.cpp
+++ b/src/app/contactadapter.cpp
@@ -21,16 +21,10 @@
 #include "contactadapter.h"
 
 #include "lrcinstance.h"
-#include "qmlregister.h"
 
 ContactAdapter::ContactAdapter(LRCInstance* instance, QObject* parent)
     : QmlAdapterBase(instance, parent)
-    , connectionInfoListModel_(new ConnectionInfoListModel(lrcInstance_, this))
 {
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS,
-                                      connectionInfoListModel_.get(),
-                                      "ConnectionInfoListModel");
-
     selectableProxyModel_.reset(new SelectableProxyModel(this));
     if (lrcInstance_) {
         connect(lrcInstance_,
@@ -252,12 +246,6 @@ ContactAdapter::removeContact(const QString& peerUri, bool banContact)
     accInfo.contactModel->removeContact(peerUri, banContact);
 }
 
-void
-ContactAdapter::updateConnectionInfo()
-{
-    connectionInfoListModel_->update();
-}
-
 void
 ContactAdapter::connectSignals()
 {
diff --git a/src/app/contactadapter.h b/src/app/contactadapter.h
index 80b67be408ebec05d2b9c5f9fe490c5b1a44b397..b8931f3c238893ea47e736a7645d821aa4e42161 100644
--- a/src/app/contactadapter.h
+++ b/src/app/contactadapter.h
@@ -20,12 +20,12 @@
 
 #include "qmladapterbase.h"
 #include "smartlistmodel.h"
-#include "conversationlistmodel.h"
-#include "connectioninfolistmodel.h"
 
 #include <QObject>
 #include <QSortFilterProxyModel>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class LRCInstance;
 
@@ -80,8 +80,14 @@ private:
 class ContactAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
 
 public:
+    static ContactAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new ContactAdapter(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit ContactAdapter(LRCInstance* instance, QObject* parent = nullptr);
     ~ContactAdapter() = default;
 
@@ -91,7 +97,6 @@ public:
     Q_INVOKABLE void setSearchFilter(const QString& filter);
     Q_INVOKABLE void contactSelected(int index);
     Q_INVOKABLE void removeContact(const QString& peerUri, bool banContact);
-    Q_INVOKABLE void updateConnectionInfo();
 
     void connectSignals();
 
@@ -106,7 +111,6 @@ private:
     SmartListModel::Type listModeltype_;
     QScopedPointer<SmartListModel> smartListModel_;
     QScopedPointer<SelectableProxyModel> selectableProxyModel_;
-    QScopedPointer<ConnectionInfoListModel> connectionInfoListModel_;
 
     QStringList defaultModerators_;
 
diff --git a/src/app/conversationlistmodel.cpp b/src/app/conversationlistmodel.cpp
index ac80946396aa98d5bd619d4e79474b12ba542fec..b914f73187b7a84dc6b60ff9a4632ca2a73f1f93 100644
--- a/src/app/conversationlistmodel.cpp
+++ b/src/app/conversationlistmodel.cpp
@@ -55,10 +55,15 @@ ConversationListModel::ConversationListModel(LRCInstance* instance, QObject* par
             &ConversationListModel::endRemoveRows,
             Qt::DirectConnection);
 
-    connect(model_, &ConversationModel::dataChanged, this, [this](int position) {
-        const auto index = createIndex(position, 0);
-        Q_EMIT ConversationListModel::dataChanged(index, index);
-    }, Qt::QueuedConnection);
+    connect(
+        model_,
+        &ConversationModel::dataChanged,
+        this,
+        [this](int position) {
+            const auto index = createIndex(position, 0);
+            Q_EMIT ConversationListModel::dataChanged(index, index);
+        },
+        Qt::QueuedConnection);
 }
 
 int
diff --git a/src/app/conversationsadapter.cpp b/src/app/conversationsadapter.cpp
index 857ed5ee1b250020136f2c7260995c78f39e3648..8e9ecaaa017bfb84076ef7924bd652a13b50a8c2 100644
--- a/src/app/conversationsadapter.cpp
+++ b/src/app/conversationsadapter.cpp
@@ -24,7 +24,9 @@
 #include "qmlregister.h"
 #include "qtutils.h"
 
+#ifdef Q_OS_LINUX
 #include "namedirectory.h"
+#endif
 
 #include <QApplication>
 #include <QJsonObject>
@@ -33,18 +35,21 @@ using namespace lrc::api;
 
 ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
                                            LRCInstance* instance,
+                                           ConversationListProxyModel* convProxyModel,
+                                           SelectableListProxyModel* searchProxyModel,
                                            QObject* parent)
     : QmlAdapterBase(instance, parent)
     , systemTray_(systemTray)
     , convSrcModel_(new ConversationListModel(lrcInstance_))
-    , convModel_(new ConversationListProxyModel(convSrcModel_.get()))
+    , convModel_(convProxyModel)
     , searchSrcModel_(new SearchResultsListModel(lrcInstance_))
-    , searchModel_(new SelectableListProxyModel(searchSrcModel_.get()))
+    , searchModel_(searchProxyModel)
 {
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, convModel_.get(), "ConversationListModel");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, searchModel_.get(), "SearchResultsListModel");
+    convModel_->bindSourceModel(convSrcModel_.get());
+    searchModel_->bindSourceModel(searchSrcModel_.get());
 
-    new SelectableListProxyGroupModel({convModel_.data(), searchModel_.data()}, this);
+    set_convListProxyModel(QVariant::fromValue(convModel_));
+    set_searchListProxyModel(QVariant::fromValue(searchModel_));
 
     // this will trigger when the invite filter tab is selected
     connect(this, &ConversationsAdapter::filterRequestsChanged, [this]() {
@@ -112,32 +117,27 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newUnreadInteraction,
             this,
-            &ConversationsAdapter::onNewUnreadInteraction,
-            Qt::UniqueConnection);
+            &ConversationsAdapter::onNewUnreadInteraction);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newReadInteraction,
             this,
-            &ConversationsAdapter::onNewReadInteraction,
-            Qt::UniqueConnection);
+            &ConversationsAdapter::onNewReadInteraction);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::newTrustRequest,
             this,
-            &ConversationsAdapter::onNewTrustRequest,
-            Qt::UniqueConnection);
+            &ConversationsAdapter::onNewTrustRequest);
 
     connect(&lrcInstance_->behaviorController(),
             &BehaviorController::trustRequestTreated,
             this,
-            &ConversationsAdapter::onTrustRequestTreated,
-            Qt::UniqueConnection);
+            &ConversationsAdapter::onTrustRequestTreated);
 
     connect(lrcInstance_,
             &LRCInstance::currentAccountIdChanged,
             this,
-            &ConversationsAdapter::onCurrentAccountIdChanged,
-            Qt::UniqueConnection);
+            &ConversationsAdapter::onCurrentAccountIdChanged);
 
     connectConversationModel();
 }
@@ -168,7 +168,6 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
         if (interaction.authorUri == accountInfo.profileInfo.uri)
             return;
         auto from = accountInfo.contactModel->bestNameForContact(interaction.authorUri);
-        auto to = lrcInstance_->accountModel().bestNameForAccount(accountId);
         auto body_ = interaction.body;
 
         if (interaction.type == interaction::Type::DATA_TRANSFER) {
@@ -180,6 +179,7 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
         if (preferences["ignoreNotifications"] == "true")
             return;
 #ifdef Q_OS_LINUX
+        auto to = lrcInstance_->accountModel().bestNameForAccount(accountId);
         auto contactPhoto = Utils::contactPhoto(lrcInstance_,
                                                 interaction.authorUri,
                                                 QSize(50, 50),
@@ -599,75 +599,49 @@ void
 ConversationsAdapter::connectConversationModel()
 {
     // Signal connections
-    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
-    if (currentConversationModel == nullptr) {
+    auto model = lrcInstance_->getCurrentConversationModel();
+    if (!model) {
         return;
     }
 
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::modelChanged,
-                     this,
-                     &ConversationsAdapter::onModelChanged,
-                     Qt::UniqueConnection);
-
-    QObject::connect(lrcInstance_->getCurrentContactModel(),
-                     &ContactModel::profileUpdated,
-                     this,
-                     &ConversationsAdapter::onProfileUpdated,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::conversationUpdated,
-                     this,
-                     &ConversationsAdapter::onConversationUpdated,
-                     Qt::UniqueConnection);
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::conversationRemoved,
-                     this,
-                     &ConversationsAdapter::onConversationRemoved,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::filterChanged,
-                     this,
-                     &ConversationsAdapter::onFilterChanged,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::conversationCleared,
-                     this,
-                     &ConversationsAdapter::onConversationCleared,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::searchStatusChanged,
-                     this,
-                     &ConversationsAdapter::onSearchStatusChanged,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::searchResultUpdated,
-                     this,
-                     &ConversationsAdapter::onSearchResultUpdated,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::searchResultEnded,
-                     this,
-                     &ConversationsAdapter::onSearchResultEnded,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::conversationReady,
-                     this,
-                     &ConversationsAdapter::onConversationReady,
-                     Qt::UniqueConnection);
-
-    QObject::connect(lrcInstance_->getCurrentContactModel(),
-                     &ContactModel::bannedStatusChanged,
-                     this,
-                     &ConversationsAdapter::onBannedStatusChanged,
-                     Qt::UniqueConnection);
+    auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
+        connect(obj, signal, this, slot, Qt::UniqueConnection);
+    };
+
+    connectObjectSignal(model,
+                        &ConversationModel::modelChanged,
+                        &ConversationsAdapter::onModelChanged);
+    connectObjectSignal(model,
+                        &ConversationModel::profileUpdated,
+                        &ConversationsAdapter::onProfileUpdated);
+    connectObjectSignal(model,
+                        &ConversationModel::conversationUpdated,
+                        &ConversationsAdapter::onConversationUpdated);
+    connectObjectSignal(model,
+                        &ConversationModel::conversationRemoved,
+                        &ConversationsAdapter::onConversationRemoved);
+    connectObjectSignal(model,
+                        &ConversationModel::filterChanged,
+                        &ConversationsAdapter::onFilterChanged);
+    connectObjectSignal(model,
+                        &ConversationModel::conversationCleared,
+                        &ConversationsAdapter::onConversationCleared);
+    connectObjectSignal(model,
+                        &ConversationModel::searchStatusChanged,
+                        &ConversationsAdapter::onSearchStatusChanged);
+    connectObjectSignal(model,
+                        &ConversationModel::searchResultUpdated,
+                        &ConversationsAdapter::onSearchResultUpdated);
+    connectObjectSignal(model,
+                        &ConversationModel::searchResultEnded,
+                        &ConversationsAdapter::onSearchResultEnded);
+    connectObjectSignal(model,
+                        &ConversationModel::conversationReady,
+                        &ConversationsAdapter::onConversationReady);
+
+    connectObjectSignal(lrcInstance_->getCurrentContactModel(),
+                        &ContactModel::bannedStatusChanged,
+                        &ConversationsAdapter::onBannedStatusChanged);
 
     convSrcModel_.reset(new ConversationListModel(lrcInstance_));
     convModel_->bindSourceModel(convSrcModel_.get());
diff --git a/src/app/conversationsadapter.h b/src/app/conversationsadapter.h
index 59d970d520a11510e0622d5c0bfaded8e7867275..0a1114f3be99cf67460f1038a95ff6cebe13e2d7 100644
--- a/src/app/conversationsadapter.h
+++ b/src/app/conversationsadapter.h
@@ -26,19 +26,36 @@
 
 #include <QObject>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class SystemTray;
 
 class ConversationsAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(bool, filterRequests)
     QML_PROPERTY(int, totalUnreadMessageCount)
     QML_PROPERTY(int, pendingRequestCount)
+    QML_RO_PROPERTY(QVariant, convListProxyModel)
+    QML_RO_PROPERTY(QVariant, searchListProxyModel)
 
 public:
+    static ConversationsAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new ConversationsAdapter(
+            qApp->property("SystemTray").value<SystemTray*>(),
+            qApp->property("LRCInstance").value<LRCInstance*>(),
+            qApp->property("ConvListProxyModel").value<ConversationListProxyModel*>(),
+            qApp->property("ConvSearchListProxyModel").value<SelectableListProxyModel*>());
+    }
+
     explicit ConversationsAdapter(SystemTray* systemTray,
                                   LRCInstance* instance,
+                                  ConversationListProxyModel* convProxyModel,
+                                  SelectableListProxyModel* searchProxyModel,
                                   QObject* parent = nullptr);
     ~ConversationsAdapter() = default;
 
@@ -46,9 +63,9 @@ public:
     void connectConversationModel();
 
     Q_INVOKABLE QString createSwarm(const QString& title,
-                                 const QString& description,
-                                 const QString& avatar,
-                                 const VectorString& participants);
+                                    const QString& description,
+                                    const QString& avatar,
+                                    const VectorString& participants);
     Q_INVOKABLE void setFilter(const QString& filterString);
     Q_INVOKABLE void setFilterAndSelect(const QString& filterString);
     Q_INVOKABLE void ignoreFiltering(const QVariant& hightlighted);
@@ -107,9 +124,9 @@ private:
     SystemTray* systemTray_;
 
     QScopedPointer<ConversationListModel> convSrcModel_;
-    QScopedPointer<ConversationListProxyModel> convModel_;
+    ConversationListProxyModel* convModel_;
     QScopedPointer<SearchResultsListModel> searchSrcModel_;
-    QScopedPointer<SelectableListProxyModel> searchModel_;
+    SelectableListProxyModel* searchModel_;
 
     std::atomic_bool selectFirst_ {false};
 };
diff --git a/src/app/currentaccount.h b/src/app/currentaccount.h
index c3f97b6bbf9131a283c12de87b4ae3745a42beca..f2f5e7337c7f8684f25e4f26d3c72f5244c8533c 100644
--- a/src/app/currentaccount.h
+++ b/src/app/currentaccount.h
@@ -24,6 +24,8 @@
 
 #include <QObject>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 #define ACCOUNT_CONFIG_SETTINGS_PROPERTY_BASE(type, prop) \
     PROPERTY_GETTER_BASE(type, prop) \
@@ -97,7 +99,7 @@ private: \
 class CurrentAccount final : public QObject
 {
     Q_OBJECT
-    // Basic settings
+    QML_SINGLETON
 
     QML_RO_PROPERTY(QString, id)
     QML_RO_PROPERTY(QString, uri)
@@ -194,6 +196,12 @@ class CurrentAccount final : public QObject
     QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(QJsonObject, uiCustomization)
 
 public:
+    static CurrentAccount* create(QQmlEngine*, QJSEngine*)
+    {
+        return new CurrentAccount(qApp->property("LRCInstance").value<LRCInstance*>(),
+                                  qApp->property("AppSettingsManager").value<AppSettingsManager*>());
+    }
+
     explicit CurrentAccount(LRCInstance* lrcInstance,
                             AppSettingsManager* settingsManager,
                             QObject* parent = nullptr);
diff --git a/src/app/currentaccounttomigrate.cpp b/src/app/currentaccounttomigrate.cpp
index 21c01223ca266158a7e8e63d8bb1f15fad3ec7f1..e670d72d5967d5c5359debb00a4664925dcc29e9 100644
--- a/src/app/currentaccounttomigrate.cpp
+++ b/src/app/currentaccounttomigrate.cpp
@@ -58,8 +58,6 @@ CurrentAccountToMigrate::CurrentAccountToMigrate(LRCInstance* instance, QObject*
             &CurrentAccountToMigrate::slotAccountRemoved);
 }
 
-CurrentAccountToMigrate::~CurrentAccountToMigrate() {}
-
 void
 CurrentAccountToMigrate::removeCurrentAccountToMigrate()
 {
@@ -133,4 +131,4 @@ CurrentAccountToMigrate::slotAccountRemoved(const QString& accountId)
         Q_EMIT allMigrationsFinished();
     else
         Q_EMIT currentAccountToMigrateRemoved();
-}
\ No newline at end of file
+}
diff --git a/src/app/currentaccounttomigrate.h b/src/app/currentaccounttomigrate.h
index 3d05720a5bc234a6b57eddcdc992a94b8998c17a..b1b075c788e6fff83d4159a3f896e6d5b9bc79d6 100644
--- a/src/app/currentaccounttomigrate.h
+++ b/src/app/currentaccounttomigrate.h
@@ -19,15 +19,19 @@
 
 #pragma once
 
-#include <QObject>
-
 #include "qtutils.h"
 
+#include <QObject>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
+
 class LRCInstance;
 
 class CurrentAccountToMigrate : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_RO_PROPERTY(int, accountToMigrateListSize)
     QML_RO_PROPERTY(QString, accountId)
     QML_RO_PROPERTY(QString, managerUsername)
@@ -36,8 +40,13 @@ class CurrentAccountToMigrate : public QObject
     QML_RO_PROPERTY(QString, alias)
 
 public:
+    static CurrentAccountToMigrate* create(QQmlEngine*, QJSEngine*)
+    {
+        return new CurrentAccountToMigrate(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit CurrentAccountToMigrate(LRCInstance* lrcInstance, QObject* parent = nullptr);
-    ~CurrentAccountToMigrate();
+    ~CurrentAccountToMigrate() = default;
 
     Q_INVOKABLE void removeCurrentAccountToMigrate();
 
diff --git a/src/app/currentcall.cpp b/src/app/currentcall.cpp
index 2b5ecae882bbe8d8f4355c6fbbc3977af1e379bf..7ef2af43b124352df5115993e87ce4801ecc0207 100644
--- a/src/app/currentcall.cpp
+++ b/src/app/currentcall.cpp
@@ -16,7 +16,8 @@
  */
 
 #include "currentcall.h"
-#include "qmlregister.h"
+
+#include "callparticipantsmodel.h"
 
 #include <api/callparticipantsmodel.h>
 #include <api/devicemodel.h>
@@ -25,8 +26,7 @@ CurrentCall::CurrentCall(LRCInstance* lrcInstance, QObject* parent)
     : QObject(parent)
     , lrcInstance_(lrcInstance)
 {
-    participantsModel_.reset(new CallParticipantsModel(lrcInstance_, this));
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, participantsModel_.get(), "CallParticipantsModel");
+    participantsModel_ = qApp->property("CallParticipantsModel").value<CallParticipantsModel*>();
 
     connect(lrcInstance_,
             &LRCInstance::currentAccountIdChanged,
diff --git a/src/app/currentcall.h b/src/app/currentcall.h
index 921e3a918d93e6292713f867ba3f929bec447a07..f9688747244c0dd397f09ca468429fdfebe499ec 100644
--- a/src/app/currentcall.h
+++ b/src/app/currentcall.h
@@ -19,14 +19,18 @@
 
 #include "lrcinstance.h"
 #include "qtutils.h"
-#include "callparticipantsmodel.h"
 
 #include <QObject>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
+
+class CallParticipantsModel;
 
 class CurrentCall final : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
 
     QML_RO_PROPERTY(QString, id)
     QML_RO_PROPERTY(QStringList, uris)
@@ -55,6 +59,11 @@ class CurrentCall final : public QObject
     QML_PROPERTY(bool, flipSelf)
 
 public:
+    static CurrentCall* create(QQmlEngine*, QJSEngine*)
+    {
+        return new CurrentCall(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit CurrentCall(LRCInstance* lrcInstance, QObject* parent = nullptr);
     ~CurrentCall() = default;
     Q_INVOKABLE QVariantList getConferencesInfos() const;
@@ -85,5 +94,5 @@ private Q_SLOTS:
 
 private:
     LRCInstance* lrcInstance_;
-    QScopedPointer<CallParticipantsModel> participantsModel_;
+    CallParticipantsModel* participantsModel_;
 };
diff --git a/src/app/currentconversation.cpp b/src/app/currentconversation.cpp
index 1b2354c2f838336c9e828969f3b819e25fd16f7f..8fa2b74cecfd3e5aebeaec0483d3ed49ac7d08bc 100644
--- a/src/app/currentconversation.cpp
+++ b/src/app/currentconversation.cpp
@@ -24,7 +24,9 @@ CurrentConversation::CurrentConversation(LRCInstance* lrcInstance, QObject* pare
     : QObject(parent)
     , lrcInstance_(lrcInstance)
 {
-    uris_ = new CurrentConversationMembers(lrcInstance, this);
+    membersModel_ = new CurrentConversationMembers(lrcInstance, this);
+    set_members(QVariant::fromValue(membersModel_));
+
     // whenever the account changes, reconnect the new conversation model
     // for updates to the conversation and call state/id
     connect(lrcInstance_,
@@ -55,7 +57,7 @@ CurrentConversation::updateData()
     // If the conversation is empty, clear the id and return.
     if (convId.isEmpty()) {
         set_id();
-        uris_->setMembers({}, {}, {});
+        membersModel_->setMembers({}, {}, {});
         return;
     }
 
@@ -95,7 +97,7 @@ CurrentConversation::updateData()
             for (const auto& banned : bannedUris)
                 uris.push_back(banned);
         }
-        uris_->setMembers(accountId, convId, uris);
+        membersModel_->setMembers(accountId, convId, uris);
         set_isSwarm(convInfo.isSwarm());
         set_isLegacy(convInfo.isLegacy());
         set_isCoreDialog(convInfo.isCoreDialog());
@@ -202,12 +204,6 @@ CurrentConversation::setInfo(const QString& key, const QString& value)
     accInfo.conversationModel->updateConversationInfos(convId, infos);
 }
 
-CurrentConversationMembers*
-CurrentConversation::uris() const
-{
-    return uris_;
-}
-
 void
 CurrentConversation::onConversationUpdated(const QString& convId)
 {
@@ -266,16 +262,22 @@ CurrentConversation::updateConversationPreferences(const QString& convId)
 void
 CurrentConversation::connectModel()
 {
-    uris_->setMembers({}, {}, {});
+    membersModel_->setMembers({}, {}, {});
     auto convModel = lrcInstance_->getCurrentConversationModel();
     if (!convModel)
         return;
 
-    connect(lrcInstance_->getCurrentConversationModel(),
-            &ConversationModel::conversationUpdated,
-            this,
-            &CurrentConversation::onConversationUpdated,
-            Qt::UniqueConnection);
+    auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
+        connect(obj, signal, this, slot, Qt::UniqueConnection);
+    };
+
+    connectObjectSignal(convModel,
+                        &ConversationModel::conversationUpdated,
+                        &CurrentConversation::onConversationUpdated);
+    connectObjectSignal(convModel,
+                        &ConversationModel::profileUpdated,
+                        &CurrentConversation::updateProfile);
+
     connect(lrcInstance_->getCurrentConversationModel(),
             &ConversationModel::profileUpdated,
             this,
diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h
index b42b9c7f01f70530016a240dc4df10650ccbfdd5..44eb3234f8209927c5bf90cf964188796f2f0d9a 100644
--- a/src/app/currentconversation.h
+++ b/src/app/currentconversation.h
@@ -23,6 +23,8 @@
 
 #include <QObject>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 // an adapter object to expose a conversation::Info struct
 // as a group of observable properties
@@ -30,6 +32,8 @@
 class CurrentConversation final : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(QString, id)
     QML_PROPERTY(QString, title)
     QML_PROPERTY(QString, description)
@@ -56,10 +60,17 @@ class CurrentConversation final : public QObject
     QML_PROPERTY(QStringList, backendErrors)
     QML_PROPERTY(QString, lastSelfMessageId)
     QML_RO_PROPERTY(bool, hasCall)
+    QML_RO_PROPERTY(QVariant, members)
 
 public:
+    static CurrentConversation* create(QQmlEngine*, QJSEngine*)
+    {
+        return new CurrentConversation(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);
     ~CurrentConversation() = default;
+
     Q_INVOKABLE void scrollToMsg(const QString& msgId);
     Q_INVOKABLE void setPreference(const QString& key, const QString& value);
     Q_INVOKABLE QString getPreference(const QString& key) const;
@@ -88,7 +99,7 @@ Q_SIGNALS:
 
 private:
     LRCInstance* lrcInstance_;
-    CurrentConversationMembers* uris_;
+    CurrentConversationMembers* membersModel_;
 
     void connectModel();
 };
diff --git a/src/app/currentconversationmembers.h b/src/app/currentconversationmembers.h
index e0485c23b154333b5c9fb5a26b71908255684873..ea8bb61eb8d7bb01962403a0417beada26e89df6 100644
--- a/src/app/currentconversationmembers.h
+++ b/src/app/currentconversationmembers.h
@@ -41,7 +41,7 @@ Q_ENUM_NS(Role)
 class CurrentConversationMembers : public QAbstractListModel
 {
     Q_OBJECT
-    QML_PROPERTY(int, count)
+    QML_RO_PROPERTY(int, count)
 
 public:
     explicit CurrentConversationMembers(LRCInstance* lrcInstance, QObject* parent = nullptr);
@@ -56,4 +56,4 @@ private:
     QString accountId_;
     QString convId_;
     QStringList members_;
-};
\ No newline at end of file
+};
diff --git a/src/app/imagedownloader.h b/src/app/imagedownloader.h
index f5b71741cc460a81f2b72ed62382b1c27d1edeb4..e0cf725f66792197a37433c5fea48e96fdf2484a 100644
--- a/src/app/imagedownloader.h
+++ b/src/app/imagedownloader.h
@@ -18,16 +18,28 @@
 #pragma once
 
 #include "networkmanager.h"
+#include "connectivitymonitor.h"
 #include "qtutils.h"
 
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
+
 class ImageDownloader : public NetworkManager
 {
     Q_OBJECT
+    QML_SINGLETON
 
     QML_PROPERTY(QString, cachePath)
 
 public:
+    static ImageDownloader* create(QQmlEngine*, QJSEngine*)
+    {
+        return new ImageDownloader(
+            qApp->property("ConnectivityMonitor").value<ConnectivityMonitor*>());
+    }
+
     explicit ImageDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr);
+    ~ImageDownloader() = default;
 
     // Download an image and call onDownloadImageFinished when done
     Q_INVOKABLE void downloadImage(const QUrl& url, const QString& localPath);
@@ -39,4 +51,4 @@ Q_SIGNALS:
 private Q_SLOTS:
     // Saves the image to the localPath and emits the appropriate signal
     void onDownloadImageFinished(const QByteArray& reply, const QString& localPath);
-};
\ No newline at end of file
+};
diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp
index 144bfbc62edd4ae68e9c10060c085d496e377a64..330412dccb2110a4ea17bf8738f2d5dc340b5ed6 100644
--- a/src/app/mainapplication.cpp
+++ b/src/app/mainapplication.cpp
@@ -26,6 +26,8 @@
 #include "connectivitymonitor.h"
 #include "systemtray.h"
 #include "videoprovider.h"
+#include "previewengine.h"
+#include "conversationlistmodel.h"
 
 #include <QWKQuick/qwkquickglobal.h>
 
@@ -42,7 +44,6 @@
 #include <QQuickWindow>
 #include <QLoggingCategory>
 
-#include <locale.h>
 #include <thread>
 
 #ifdef Q_OS_WIN
@@ -188,6 +189,7 @@ MainApplication::init()
     connectivityMonitor_ = new ConnectivityMonitor(this);
     settingsManager_ = new AppSettingsManager(this);
     systemTray_ = new SystemTray(settingsManager_, this);
+    previewEngine_ = new PreviewEngine(connectivityMonitor_, this);
 
     // These should should be QueuedConnection to ensure that the
     // they are executed after the QML engine has been initialized,
@@ -407,6 +409,7 @@ MainApplication::initQmlLayer()
                          systemTray_,
                          settingsManager_,
                          connectivityMonitor_,
+                         previewEngine_,
                          &screenInfo_,
                          this);
 
diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h
index 5c039e882f733bc0d5584f0621116fa9ea80c3fb..3bbba00a8758031f10ffadb84023df31cbd79224 100644
--- a/src/app/mainapplication.h
+++ b/src/app/mainapplication.h
@@ -35,6 +35,7 @@
 class ConnectivityMonitor;
 class AppSettingsManager;
 class SystemTray;
+class PreviewEngine;
 
 // Provides information about the screen the app is displayed on
 class ScreenInfo : public QObject
@@ -120,6 +121,7 @@ private:
     ConnectivityMonitor* connectivityMonitor_;
     SystemTray* systemTray_;
     AppSettingsManager* settingsManager_;
+    PreviewEngine* previewEngine_;
 
     ScreenInfo screenInfo_;
 };
diff --git a/src/app/mainview/components/AddMemberPanel.qml b/src/app/mainview/components/AddMemberPanel.qml
index 27826eb6cae02a37580f9da286596f1877971b8e..2874dc2796cc2aa8728d1112c1c46923bb52bc5b 100644
--- a/src/app/mainview/components/AddMemberPanel.qml
+++ b/src/app/mainview/components/AddMemberPanel.qml
@@ -57,8 +57,8 @@ Rectangle {
             Layout.leftMargin: 4
             Layout.rightMargin: 4
 
-            // Reset the model if visible or the CurrentConversationMembers.count changes (0 or greater)
-            model: visible && CurrentConversationMembers.count >= 0 ? ContactAdapter.getContactSelectableModel(type) : null
+            // Reset the model if visible or the current conv member count changes (0 or greater)
+            model: visible && CurrentConversation.members.count >= 0 ? ContactAdapter.getContactSelectableModel(type) : null
 
             delegate: ContactPickerItemDelegate {
                 id: contactPickerItemDelegate
diff --git a/src/app/mainview/components/ConversationExtrasPanel.qml b/src/app/mainview/components/ConversationExtrasPanel.qml
index e319f4e7eeb517e47dada52154996d79ee6d5d52..5fa4d1be3f60413c1362b539b898e66388ba6110 100644
--- a/src/app/mainview/components/ConversationExtrasPanel.qml
+++ b/src/app/mainview/components/ConversationExtrasPanel.qml
@@ -65,12 +65,12 @@ StackLayout {
     }
 
     Connections {
-        target: CurrentConversationMembers
+        target: CurrentConversation.members
 
         function onCountChanged() {
             // Close the panel if there are 8 or more members in the
             // conversation AND the "Add Member" panel is currently open.
-            if (CurrentConversationMembers.count >= 8 && isOpen(ChatView.AddMemberPanel)) {
+            if (CurrentConversation.members.count >= 8 && isOpen(ChatView.AddMemberPanel)) {
                 closePanel();
             }
         }
diff --git a/src/app/mainview/components/ConversationListView.qml b/src/app/mainview/components/ConversationListView.qml
index 9a23671c3a79ded0d5f58ea05a4fc8a752ca78ac..d0b1fdcd225cab36ba9bf2beea44d7692e39f130 100644
--- a/src/app/mainview/components/ConversationListView.qml
+++ b/src/app/mainview/components/ConversationListView.qml
@@ -70,28 +70,6 @@ JamiListView {
 
     onCountChanged: positionViewAtBeginning()
 
-    add: Transition {
-        NumberAnimation {
-            property: "opacity"
-            from: 0
-            to: 1.0
-            duration: JamiTheme.smartListTransitionDuration
-        }
-    }
-
-    displaced: Transition {
-        NumberAnimation {
-            properties: "x,y"
-            easing.type: Easing.OutCubic
-            duration: JamiTheme.smartListTransitionDuration
-        }
-        NumberAnimation {
-            property: "opacity"
-            to: 1.0
-            duration: JamiTheme.smartListTransitionDuration * (1 - from)
-        }
-    }
-
     Behavior on opacity  {
         NumberAnimation {
             easing.type: Easing.OutCubic
diff --git a/src/app/mainview/components/PluginHandlerPicker.qml b/src/app/mainview/components/PluginHandlerPicker.qml
index ce21b7e87d1b378f8730b6623dc9fb6d26d257e3..c0a4e4066b2462b2cb0c84f49be503a1074e36d3 100644
--- a/src/app/mainview/components/PluginHandlerPicker.qml
+++ b/src/app/mainview/components/PluginHandlerPicker.qml
@@ -57,7 +57,7 @@ Popup {
                     if (isCall) {
                         pluginhandlerPickerListView.model = PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id);
                     } else {
-                        var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0];
+                        var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.members[0];
                         pluginhandlerPickerListView.model = PluginAdapter.getChatHandlerSelectableModel(LRCInstance.currentAccountId, peerId);
                     }
                 }
@@ -69,7 +69,7 @@ Popup {
                     pluginhandlerPickerListView.model = PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id);
                 } else {
                     var accountId = LRCInstance.currentAccountId;
-                    var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0];
+                    var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.members[0];
                     PluginModel.toggleChatHandler(handlerId, accountId, peerId, !isLoaded);
                     pluginhandlerPickerListView.model = PluginAdapter.getChatHandlerSelectableModel(accountId, peerId);
                 }
@@ -124,7 +124,7 @@ Popup {
                         if (isCall) {
                             return PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id);
                         } else {
-                            var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0];
+                            var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.members[0];
                             return PluginAdapter.getChatHandlerSelectableModel(LRCInstance.currentAccountId, peerId);
                         }
                     }
diff --git a/src/app/mainview/components/SidePanel.qml b/src/app/mainview/components/SidePanel.qml
index 0037f7ebd270139dcdc643e6dbca4e3d0aafb8e3..01ce34680ba19ff29d6d408f398729573af113f8 100644
--- a/src/app/mainview/components/SidePanel.qml
+++ b/src/app/mainview/components/SidePanel.qml
@@ -378,7 +378,7 @@ SidePanelBase {
                             return parent.height;
                     }
 
-                    model: SearchResultsListModel
+                    model: ConversationsAdapter.searchListProxyModel
                     headerLabel: JamiStrings.searchResults
                     headerVisible: true
                 }
@@ -389,7 +389,7 @@ SidePanelBase {
                     Layout.fillWidth: true
                     Layout.fillHeight: true
 
-                    model: ConversationListModel
+                    model: ConversationsAdapter.convListProxyModel
                     headerLabel: JamiStrings.conversations
                     headerVisible: count && searchResultsListView.visible
                 }
@@ -411,7 +411,7 @@ SidePanelBase {
                     Layout.preferredWidth: parent.width
                     Layout.fillHeight: true
 
-                    model: ConversationListModel
+                    model: ConversationsAdapter.convListProxyModel
                     delegate: SmartListItemDelegate {
                         interactive: false
 
diff --git a/src/app/mainview/components/SwarmDetailsPanel.qml b/src/app/mainview/components/SwarmDetailsPanel.qml
index 2f0d5a597427d1b07be97a67286ccab27c9e3cc3..ef439fe160445c3252754e150aacbb6d767f6fdf 100644
--- a/src/app/mainview/components/SwarmDetailsPanel.qml
+++ b/src/app/mainview/components/SwarmDetailsPanel.qml
@@ -205,7 +205,7 @@ Rectangle {
                         objectName: "members"
                         visible: !CurrentConversation.isCoreDialog
                         labelText: {
-                            var membersNb = CurrentConversationMembers.count;
+                            var membersNb = CurrentConversation.members.count;
                             if (membersNb > 1)
                                 return JamiStrings.members.arg(membersNb);
                             return JamiStrings.member;
@@ -581,7 +581,7 @@ Rectangle {
                     }
                 }
 
-                model: CurrentConversationMembers
+                model: CurrentConversation.members
                 delegate: ItemDelegate {
                     id: member
 
diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp
index 823300e046d731fbbb27b9eccb0c98b88f1a8b52..9bc034e6ccd77bdaf89789e6ba8e335eaae43408 100644
--- a/src/app/messagesadapter.cpp
+++ b/src/app/messagesadapter.cpp
@@ -26,6 +26,7 @@
 #include "appsettingsmanager.h"
 #include "qtutils.h"
 #include "messageparser.h"
+#include "previewengine.h"
 
 #include <api/datatransfermodel.h>
 
diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h
index e333e66ba763401162662a552409227706126c94..1deb16089e9fdeedc24ed7063c2917a884af41e4 100644
--- a/src/app/messagesadapter.h
+++ b/src/app/messagesadapter.h
@@ -20,16 +20,18 @@
 
 #include "lrcinstance.h"
 #include "qmladapterbase.h"
+
 #include "previewengine.h"
+#include "messageparser.h"
+#include "appsettingsmanager.h"
 
 #include <QObject>
 #include <QString>
 #include <QTimer>
 
 #include <QSortFilterProxyModel>
-
-class AppSettingsManager;
-class MessageParser;
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class FilteredMsgListModel final : public QSortFilterProxyModel
 {
@@ -63,6 +65,8 @@ public:
 class MessagesAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_RO_PROPERTY(QVariant, messageListModel)
     QML_PROPERTY(QString, replyToId)
     QML_PROPERTY(QString, editId)
@@ -71,23 +75,19 @@ class MessagesAdapter final : public QmlAdapterBase
     QML_PROPERTY(QString, searchbarPrompt)
 
 public:
+    static MessagesAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new MessagesAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
+                                   qApp->property("PreviewEngine").value<PreviewEngine*>(),
+                                   qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit MessagesAdapter(AppSettingsManager* settingsManager,
                              PreviewEngine* previewEngine,
                              LRCInstance* instance,
                              QObject* parent = nullptr);
     ~MessagesAdapter() = default;
 
-Q_SIGNALS:
-    void newInteraction(const QString& id, int type);
-    void newMessageBarPlaceholderText(QString& placeholderText);
-    void newFilePasted(QString filePath);
-    void newTextPasted();
-    void moreMessagesLoaded(qint32 loadingRequestId);
-    void timestampUpdated();
-    void fileCopied(const QString& dest);
-    void messageParsed(const QString& msgId, const QString& msg);
-
-protected:
     Q_INVOKABLE bool isDocument(const interaction::Type& type);
     Q_INVOKABLE void loadMoreMessages();
     Q_INVOKABLE void connectConversationModel();
@@ -149,6 +149,15 @@ protected:
 
     inline MessageListModel* getMsgListSourceModel() const;
 
+Q_SIGNALS:
+    void newInteraction(const QString& id, int type);
+    void newFilePasted(const QString& filePath);
+    void newTextPasted();
+    void moreMessagesLoaded(qint32 loadingRequestId);
+    void timestampUpdated();
+    void fileCopied(const QString& dest);
+    void messageParsed(const QString& msgId, const QString& msg);
+
 private Q_SLOTS:
     void onNewInteraction(const QString& convUid,
                           const QString& interactionId,
diff --git a/src/app/pluginadapter.cpp b/src/app/pluginadapter.cpp
index bcdd53c55151e3e652cb7242a0d4311c906f06dd..ab0864e0f1bedb8a01ea27996074716be4560d71 100644
--- a/src/app/pluginadapter.cpp
+++ b/src/app/pluginadapter.cpp
@@ -24,7 +24,8 @@
 #include "networkmanager.h"
 #include "lrcinstance.h"
 #include "appsettingsmanager.h"
-#include "qmlregister.h"
+
+#include "api/pluginmodel.h"
 
 #include <QJsonArray>
 #include <QJsonDocument>
@@ -35,8 +36,7 @@
 
 PluginAdapter::PluginAdapter(LRCInstance* instance,
                              AppSettingsManager* settingsManager,
-                             QObject* parent,
-                             QString baseUrl)
+                             QObject* parent)
     : QmlAdapterBase(instance, parent)
     , pluginStoreListModel_(new PluginStoreListModel(instance, this))
     , pluginVersionManager_(new PluginVersionManager(instance, settingsManager, this))
@@ -44,8 +44,9 @@ PluginAdapter::PluginAdapter(LRCInstance* instance,
     , lrcInstance_(instance)
     , settingsManager_(settingsManager)
 {
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, pluginStoreListModel_, "PluginStoreListModel");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, pluginListModel_, "PluginListModel")
+    pluginListModel_ = qApp->property("PluginListModel").value<PluginListModel*>();
+    pluginStoreListModel_ = qApp->property("PluginStoreListModel").value<PluginStoreListModel*>();
+
     updateHandlersListCount();
     connect(&lrcInstance_->pluginModel(),
             &lrc::api::PluginModel::modelUpdated,
diff --git a/src/app/pluginadapter.h b/src/app/pluginadapter.h
index da93da9537bf9c70ecae03cd7538cba4c925aebf..38e8c62a0f6406cdbb7264ec63482aa10cd92fd0 100644
--- a/src/app/pluginadapter.h
+++ b/src/app/pluginadapter.h
@@ -19,33 +19,37 @@
 #pragma once
 
 #include "qmladapterbase.h"
-#include "pluginlistmodel.h"
 #include "pluginhandlerlistmodel.h"
-#include "pluginlistpreferencemodel.h"
-#include "appsettingsmanager.h"
-#include "pluginversionmanager.h"
-#include "pluginstorelistmodel.h"
-#include "preferenceitemlistmodel.h"
 
 #include <QObject>
 #include <QSortFilterProxyModel>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
-class PluginVersionManager;
+class PluginListModel;
 class PluginStoreListModel;
+class PluginVersionManager;
 class AppSettingsManager;
 
 class PluginAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(int, callMediaHandlersListCount)
     QML_PROPERTY(int, chatHandlersListCount)
 
 public:
+    static PluginAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new PluginAdapter(qApp->property("LRCInstance").value<LRCInstance*>(),
+                                 qApp->property("AppSettingsManager").value<AppSettingsManager*>());
+    }
+
     explicit PluginAdapter(LRCInstance* instance,
                            AppSettingsManager* settingsManager,
-                           QObject* parent = nullptr,
-                           QString baseUrl = "https://plugins.jami.net");
+                           QObject* parent = nullptr);
     ~PluginAdapter() = default;
 
     Q_INVOKABLE void getPluginsFromStore();
@@ -73,9 +77,9 @@ Q_SIGNALS:
 private:
     void updateHandlersListCount();
 
+    PluginListModel* pluginListModel_;
     PluginStoreListModel* pluginStoreListModel_;
     PluginVersionManager* pluginVersionManager_;
-    PluginListModel* pluginListModel_;
 
     std::unique_ptr<PluginHandlerListModel> pluginHandlerListModel_;
 
diff --git a/src/app/positionmanager.h b/src/app/positionmanager.h
index 42c9ca35e9ab1211a9c482570fb1c6c799c4c7e2..88cc821cee28e797dab13cf3f86fdb12375ac440 100644
--- a/src/app/positionmanager.h
+++ b/src/app/positionmanager.h
@@ -27,16 +27,27 @@
 #include <QMutex>
 #include <QObject>
 #include <QString>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class PositionManager : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     // map of elements : map key and isUnpin
     QML_PROPERTY(QVariantMap, mapStatus)
     QML_PROPERTY(bool, mapAutoOpening)
     QML_PROPERTY(int, positionShareConvIdsCount)
     QML_PROPERTY(int, sharingUrisCount)
 public:
+    static PositionManager* create(QQmlEngine*, QJSEngine*)
+    {
+        return new PositionManager(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
+                                   qApp->property("SystemTray").value<SystemTray*>(),
+                                   qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit PositionManager(AppSettingsManager* settingsManager,
                              SystemTray* systemTray,
                              LRCInstance* instance,
diff --git a/src/app/previewengine.cpp b/src/app/previewengine.cpp
index 8aa570eb21b6e09163a76719540a9379ed4e29c2..6d3310261a9cfcf4bd7f56df0cb23cf600cae8f5 100644
--- a/src/app/previewengine.cpp
+++ b/src/app/previewengine.cpp
@@ -17,32 +17,72 @@
 
 #include "previewengine.h"
 
+#include "htmlparser.h"
+
 #include <QRegularExpression>
 #include <QThread>
 
-const QRegularExpression PreviewEngine::newlineRe("\\r?\\n");
+class PreviewEngine::Parser : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit Parser(QObject* parent = nullptr);
+    ~Parser() = default;
+
+    Q_SIGNAL void infoReady(const QString& id, const QVariantMap& info);
+
+public:
+    Q_SLOT void processHTML(const QString& id, const QString& link, const QString& data);
+
+private:
+    // An instance of HtmlParser used to parse HTML.
+    HtmlParser* htmlParser_;
+
+    QString getTagContent(const QList<QString>& tags, const QString& value);
+    QString getTitle(const QList<QString>& metaTags);
+    QString getDescription(const QList<QString>& metaTags);
+    QString getImage(const QList<QString>& metaTags);
+
+    static const QRegularExpression newlineRe;
+};
+
+const QRegularExpression PreviewEngine::Parser::newlineRe("\\r?\\n");
 
 PreviewEngine::PreviewEngine(ConnectivityMonitor* cm, QObject* parent)
     : NetworkManager(cm, parent)
-    , htmlParser_(new HtmlParser(this))
+    , parser_(new PreviewEngine::Parser)
 {
-    // Run this object in a separate thread.
-    thread_ = new QThread();
-    moveToThread(thread_);
-    thread_->start();
+    parserThread_ = new QThread();
+    parser_->moveToThread(parserThread_);
+
+    connect(this, &PreviewEngine::parseLink, this, &PreviewEngine::onParseLink);
+    connect(this, &PreviewEngine::htmlReady, parser_.get(), &Parser::processHTML);
+    connect(parser_.get(), &Parser::infoReady, this, &PreviewEngine::infoReady);
 
-    // Connect on a queued connection to avoid blocking caller thread.
-    connect(this, &PreviewEngine::parseLink, this, &PreviewEngine::onParseLink, Qt::QueuedConnection);
+    parserThread_->start();
 }
 
 PreviewEngine::~PreviewEngine()
 {
-    thread_->quit();
-    thread_->wait();
+    parserThread_->quit();
+    parserThread_->wait();
 }
 
+void
+PreviewEngine::onParseLink(const QString& id, const QString& link)
+{
+    sendGetRequest(link,
+                   [this, id, link](const QByteArray& html) { Q_EMIT htmlReady(id, link, html); });
+}
+
+PreviewEngine::Parser::Parser(QObject* parent)
+    : QObject(parent)
+    , htmlParser_(new HtmlParser(this))
+{}
+
 QString
-PreviewEngine::getTagContent(const QList<QString>& tags, const QString& value)
+PreviewEngine::Parser::getTagContent(const QList<QString>& tags, const QString& value)
 {
     Q_FOREACH (auto tag, tags) {
         const QRegularExpression re("(property|name)=\"(og:|twitter:|)" + value
@@ -56,7 +96,7 @@ PreviewEngine::getTagContent(const QList<QString>& tags, const QString& value)
 }
 
 QString
-PreviewEngine::getTitle(const QList<QString>& metaTags)
+PreviewEngine::Parser::getTitle(const QList<QString>& metaTags)
 {
     // Try with opengraph/twitter props
     QString title = getTagContent(metaTags, "title");
@@ -73,7 +113,7 @@ PreviewEngine::getTitle(const QList<QString>& metaTags)
 }
 
 QString
-PreviewEngine::getDescription(const QList<QString>& metaTags)
+PreviewEngine::Parser::getDescription(const QList<QString>& metaTags)
 {
     // Try with og/twitter props
     QString desc = getTagContent(metaTags, "description");
@@ -84,7 +124,7 @@ PreviewEngine::getDescription(const QList<QString>& metaTags)
 }
 
 QString
-PreviewEngine::getImage(const QList<QString>& metaTags)
+PreviewEngine::Parser::getImage(const QList<QString>& metaTags)
 {
     // Try with og/twitter props
     QString image = getTagContent(metaTags, "image");
@@ -101,25 +141,25 @@ PreviewEngine::getImage(const QList<QString>& metaTags)
 }
 
 void
-PreviewEngine::onParseLink(const QString& messageId, const QString& link)
+PreviewEngine::Parser::processHTML(const QString& id, const QString& link, const QString& data)
 {
-    sendGetRequest(QUrl(link), [this, messageId, link](const QByteArray& html) {
-        htmlParser_->parseHtmlString(html);
-        auto tagsNodes = htmlParser_->getTagsNodes({TidyTag_META});
-        auto metaTagNodes = tagsNodes[TidyTag_META];
-        QList<QString> metaTags;
-        Q_FOREACH (auto tag, metaTagNodes) {
-            metaTags.append(htmlParser_->getNodeText(tag));
-        }
-        QString domain = QUrl(link).host();
-        if (domain.isEmpty()) {
-            domain = link;
-        }
-        Q_EMIT infoReady(messageId,
-                         {{"title", getTitle(metaTags)},
-                          {"description", getDescription(metaTags)},
-                          {"image", getImage(metaTags)},
-                          {"url", link},
-                          {"domain", domain}});
-    });
+    htmlParser_->parseHtmlString(data);
+    auto tagsNodes = htmlParser_->getTagsNodes({TidyTag_META});
+    auto metaTagNodes = tagsNodes[TidyTag_META];
+    QList<QString> metaTags;
+    Q_FOREACH (auto tag, metaTagNodes) {
+        metaTags.append(htmlParser_->getNodeText(tag));
+    }
+    QString domain = QUrl(link).host();
+    if (domain.isEmpty()) {
+        domain = link;
+    }
+    Q_EMIT infoReady(id,
+                     {{"title", getTitle(metaTags)},
+                      {"description", getDescription(metaTags)},
+                      {"image", getImage(metaTags)},
+                      {"url", link},
+                      {"domain", domain}});
 }
+
+#include "previewengine.moc"
diff --git a/src/app/previewengine.h b/src/app/previewengine.h
index 0ba009e192deed0d2195f3fc5ab19dbd04c1ecbb..660a960cf839d9ccae3b301c34456afafe11fc3e 100644
--- a/src/app/previewengine.h
+++ b/src/app/previewengine.h
@@ -19,8 +19,6 @@
 
 #include "networkmanager.h"
 
-#include "htmlparser.h"
-
 class PreviewEngine final : public NetworkManager
 {
     Q_OBJECT
@@ -30,21 +28,14 @@ public:
     ~PreviewEngine();
 
 Q_SIGNALS:
-    void parseLink(const QString& messageId, const QString& link);
-    void infoReady(const QString& messageId, const QVariantMap& info);
+    void parseLink(const QString& id, const QString& link);
+    void infoReady(const QString& id, const QVariantMap& info);
 
 private:
-    Q_SLOT void onParseLink(const QString& messageId, const QString& link);
-
-    // An instance of HtmlParser used to parse HTML.
-    HtmlParser* htmlParser_;
-
-    QString getTagContent(const QList<QString>& tags, const QString& value);
-    QString getTitle(const QList<QString>& metaTags);
-    QString getDescription(const QList<QString>& metaTags);
-    QString getImage(const QList<QString>& metaTags);
-
-    static const QRegularExpression newlineRe;
+    Q_SLOT void onParseLink(const QString& id, const QString& link);
+    Q_SIGNAL void htmlReady(const QString& id, const QString& link, const QByteArray& data);
 
-    QThread* thread_;
+    class Parser;
+    QScopedPointer<Parser> parser_;
+    QThread* parserThread_;
 };
diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp
index a0d1c816e9e4659222c5481d51e4025b2df206f7..96480c2ba072bdf82451efd2cdd213bea0c25a5c 100644
--- a/src/app/qmlregister.cpp
+++ b/src/app/qmlregister.cpp
@@ -27,7 +27,6 @@
 #include "positionmanager.h"
 #include "tipsmodel.h"
 #include "connectivitymonitor.h"
-#include "previewengine.h"
 #include "imagedownloader.h"
 #include "utilsadapter.h"
 #include "conversationsadapter.h"
@@ -36,7 +35,8 @@
 #include "currentaccount.h"
 #include "videodevices.h"
 #include "currentaccounttomigrate.h"
-
+#include "pttlistener.h"
+#include "calloverlaymodel.h"
 #include "accountlistmodel.h"
 #include "mediacodeclistmodel.h"
 #include "audiodevicemodel.h"
@@ -47,7 +47,10 @@
 #include "smartlistmodel.h"
 #include "filestosendlistmodel.h"
 #include "callInformationListModel.h"
-#include "rendererinformationlistmodel.h"
+#include "connectioninfolistmodel.h"
+#include "callparticipantsmodel.h"
+#include "pluginlistmodel.h"
+#include "pluginstorelistmodel.h"
 
 #include "qrimageprovider.h"
 #include "avatarimageprovider.h"
@@ -75,6 +78,7 @@
 // clang-format off
 // TODO: remove this
 #define QML_REGISTERSINGLETONTYPE_WITH_INSTANCE(T) \
+    QQmlEngine::setObjectOwnership(&T::instance(), QQmlEngine::CppOwnership); \
     qmlRegisterSingletonType<T>(NS_MODELS, MODULE_VER_MAJ, MODULE_VER_MIN, #T, \
                                 [](QQmlEngine* e, QJSEngine* se) -> QObject* { \
                                     Q_UNUSED(e); Q_UNUSED(se); \
@@ -97,6 +101,10 @@
                                              MODULE_VER_MAJ, MODULE_VER_MIN, #T, \
                                              "Don't try to add to a qml definition of " #T);
 
+#define REG_QML_SINGLETON qmlRegisterSingletonType
+#define REG_MODEL NS_MODELS, MODULE_VER_MAJ, MODULE_VER_MIN
+#define CREATE(Obj) [=](QQmlEngine*, QJSEngine*) -> QObject* { return Obj; }
+
 namespace Utils {
 
 /*!
@@ -108,65 +116,103 @@ registerTypes(QQmlEngine* engine,
               SystemTray* systemTray,
               AppSettingsManager* settingsManager,
               ConnectivityMonitor* connectivityMonitor,
+              PreviewEngine* previewEngine,
               ScreenInfo* screenInfo,
               QObject* app)
 {
-    // setup the adapters (their lifetimes are that of MainApplication)
-    auto avatarRegistry = new AvatarRegistry(lrcInstance, engine);
-    auto callAdapter = new CallAdapter(settingsManager, systemTray, lrcInstance, engine);
-    auto previewEngine = new PreviewEngine(connectivityMonitor, engine);
-    auto imageDownloader = new ImageDownloader(connectivityMonitor, engine);
-    auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, engine);
-    auto positionManager = new PositionManager(settingsManager, systemTray, lrcInstance, engine);
-    auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, engine);
-    auto avAdapter = new AvAdapter(lrcInstance, engine);
-    auto contactAdapter = new ContactAdapter(lrcInstance, engine);
-    auto accountAdapter = new AccountAdapter(settingsManager, systemTray, lrcInstance, engine);
-    auto utilsAdapter = new UtilsAdapter(settingsManager, systemTray, lrcInstance, engine);
-    auto pluginAdapter = new PluginAdapter(lrcInstance, settingsManager, engine);
-    auto currentCall = new CurrentCall(lrcInstance, engine);
-    auto currentConversation = new CurrentConversation(lrcInstance, engine);
-    auto currentAccount = new CurrentAccount(lrcInstance, settingsManager, engine);
-    auto tipsModel = new TipsModel(settingsManager, engine);
-    auto videoDevices = new VideoDevices(lrcInstance, engine);
-    auto currentAccountToMigrate = new CurrentAccountToMigrate(lrcInstance, engine);
-    auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, accountAdapter, settingsManager, engine);
+    /* Used in ContactAdapter */
+    auto connectionInfoListModel = new ConnectionInfoListModel(lrcInstance, app);
+    qApp->setProperty("ConnectionInfoListModel", QVariant::fromValue(connectionInfoListModel));
+    QQmlEngine::setObjectOwnership(connectionInfoListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<ConnectionInfoListModel>(REG_MODEL, "ConnectionInfoListModel", CREATE(connectionInfoListModel));
+
+    /* Used in AccountAdapter */
+    auto accountListModel = new AccountListModel(lrcInstance, app);
+    qApp->setProperty("AccountListModel", QVariant::fromValue(accountListModel));
+    QQmlEngine::setObjectOwnership(accountListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<AccountListModel>(REG_MODEL, "AccountListModel", CREATE(accountListModel));
+
+    auto deviceItemListModel = new DeviceItemListModel(lrcInstance, app);
+    qApp->setProperty("DeviceItemListModel", QVariant::fromValue(deviceItemListModel));
+    QQmlEngine::setObjectOwnership(deviceItemListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<DeviceItemListModel>(REG_MODEL, "DeviceItemListModel", CREATE(deviceItemListModel));
+
+    auto moderatorListModel = new ModeratorListModel(lrcInstance, app);
+    qApp->setProperty("ModeratorListModel", QVariant::fromValue(moderatorListModel));
+    QQmlEngine::setObjectOwnership(moderatorListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<ModeratorListModel>(REG_MODEL, "ModeratorListModel", CREATE(moderatorListModel));
+
+    /* Used in CallAdapter */
+    auto pttListener = new PTTListener(settingsManager, app);
+    qApp->setProperty("PTTListener", QVariant::fromValue(pttListener));
+    QQmlEngine::setObjectOwnership(pttListener, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<PTTListener>(REG_MODEL, "PTTListener", CREATE(pttListener));
+
+    auto callOverlayModel = new CallOverlayModel(lrcInstance, pttListener, app);
+    qApp->setProperty("CallOverlayModel", QVariant::fromValue(callOverlayModel));
+    QQmlEngine::setObjectOwnership(callOverlayModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<CallOverlayModel>(REG_MODEL, "CallOverlayModel", CREATE(callOverlayModel));
+
+    /* Used in CurrentCall */
+    auto callParticipantsModel = new CallParticipantsModel(lrcInstance, app);
+    qApp->setProperty("CallParticipantsModel", QVariant::fromValue(callParticipantsModel));
+    QQmlEngine::setObjectOwnership(callParticipantsModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<CallParticipantsModel>(REG_MODEL, "CallParticipantsModel", CREATE(callParticipantsModel));
+
+    /* Used in ConversationsAdapter */
+    auto convListProxyModel = new ConversationListProxyModel(nullptr, app);
+    qApp->setProperty("ConvListProxyModel", QVariant::fromValue(convListProxyModel));
+    auto searchProxyListModel = new SelectableListProxyModel(nullptr, app);
+    qApp->setProperty("ConvSearchListProxyModel", QVariant::fromValue(searchProxyListModel));
+
+    // This causes mutually exclusive selection between the two proxy models.
+    new SelectableListProxyGroupModel({convListProxyModel, searchProxyListModel}, app);
+
+    /* Used in PluginManager */
+    auto pluginListModel = new PluginListModel(lrcInstance, app);
+    qApp->setProperty("PluginListModel", QVariant::fromValue(pluginListModel));
+    QQmlEngine::setObjectOwnership(pluginListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<PluginListModel>(REG_MODEL, "PluginListModel", CREATE(pluginListModel));
+
+    auto pluginStoreListModel = new PluginStoreListModel(lrcInstance, app);
+    qApp->setProperty("PluginStoreListModel", QVariant::fromValue(pluginStoreListModel));
+    QQmlEngine::setObjectOwnership(pluginStoreListModel, QQmlEngine::CppOwnership);
+    REG_QML_SINGLETON<PluginStoreListModel>(REG_MODEL, "PluginStoreListModel", CREATE(pluginStoreListModel));
+
+    // Register app-level objects that are used by QML created objects.
+    // These MUST be set prior to loading the initial QML file, in order to
+    // be available to the QML adapter class factory creation methods.
+    qApp->setProperty("LRCInstance", QVariant::fromValue(lrcInstance));
+    qApp->setProperty("SystemTray", QVariant::fromValue(systemTray));
+    qApp->setProperty("AppSettingsManager", QVariant::fromValue(settingsManager));
+    qApp->setProperty("ConnectivityMonitor", QVariant::fromValue(connectivityMonitor));
+    qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
 
     // qml adapter registration
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, avatarRegistry, "AvatarRegistry");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, callAdapter, "CallAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, tipsModel, "TipsModel");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, messagesAdapter, "MessagesAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, positionManager, "PositionManager");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, conversationsAdapter, "ConversationsAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, avAdapter, "AvAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, contactAdapter, "ContactAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, accountAdapter, "AccountAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, utilsAdapter, "UtilsAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, pluginAdapter, "PluginAdapter");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentCall, "CurrentCall");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentConversation, "CurrentConversation");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentConversation->uris(), "CurrentConversationMembers");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccount, "CurrentAccount");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, videoDevices, "VideoDevices");
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccountToMigrate, "CurrentAccountToMigrate")
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, wizardViewStepModel, "WizardViewStepModel")
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, imageDownloader, "ImageDownloader")
+    QML_REGISTERSINGLETON_TYPE(NS_HELPERS, AvatarRegistry);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, AccountAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CallAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, MessagesAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, ConversationsAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, ContactAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, UtilsAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, PositionManager);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, AvAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, PluginAdapter);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccount);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentConversation);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentCall);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
+    QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
+    QML_REGISTERSINGLETON_TYPE(NS_MODELS, WizardViewStepModel);
+    QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader);
 
     // TODO: remove these
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, PluginModel, &lrcInstance->pluginModel())
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_HELPERS, AppVersionManager, lrcInstance->getAppVersionManager())
-
-    // Hack for QtCreator autocomplete (part 2)
-    // https://bugreports.qt.io/browse/QTCREATORBUG-20569
-    // Use a dummy object to register the import namespace.
-    // This occurs when we register from within MainApplication
-    QML_REGISTERNAMESPACE(NS_MODELS, dummy::staticMetaObject, "");
-    QML_REGISTERNAMESPACE(NS_ADAPTERS, dummy::staticMetaObject, "");
-    QML_REGISTERNAMESPACE(NS_CONSTANTS, dummy::staticMetaObject, "");
-    QML_REGISTERNAMESPACE(NS_HELPERS, dummy::staticMetaObject, "");
-    QML_REGISTERNAMESPACE(NS_ENUMS, dummy::staticMetaObject, "");
+    QML_REGISTERSINGLETONTYPE_WITH_INSTANCE(NameDirectory); // C++ singleton
 
     // QAbstractListModels
     QML_REGISTERTYPE(NS_MODELS, BannedListModel);
@@ -176,10 +222,7 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERTYPE(NS_MODELS, PreferenceItemListModel);
     QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
     QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
-    QML_REGISTERTYPE(NS_MODELS, SmartListModel);
-    QML_REGISTERTYPE(NS_MODELS, MessageListModel);
     QML_REGISTERTYPE(NS_MODELS, CallInformationListModel);
-    QML_REGISTERTYPE(NS_MODELS, RendererInformationListModel);
 
     // Roles & type enums for models
     QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
@@ -201,10 +244,6 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
 
-    // C++ singletons
-    // TODO: remove this
-    QML_REGISTERSINGLETONTYPE_WITH_INSTANCE(NameDirectory);
-
     // Lrc namespaces, models, and singletons
     QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::staticMetaObject, "Lrc");
     QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::account::staticMetaObject, "Account");
@@ -239,13 +278,7 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatFpsModel)
 
     engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance));
-    engine->addImageProvider(QLatin1String("avatarimage"),
-                              new AvatarImageProvider(lrcInstance));
-
-    engine->setObjectOwnership(&lrcInstance->avModel(), QQmlEngine::CppOwnership);
-    engine->setObjectOwnership(&lrcInstance->pluginModel(), QQmlEngine::CppOwnership);
-    engine->setObjectOwnership(lrcInstance->getAppVersionManager(), QQmlEngine::CppOwnership);
-    engine->setObjectOwnership(&NameDirectory::instance(), QQmlEngine::CppOwnership);
+    engine->addImageProvider(QLatin1String("avatarimage"), new AvatarImageProvider(lrcInstance));
 }
 // clang-format on
 } // namespace Utils
diff --git a/src/app/qmlregister.h b/src/app/qmlregister.h
index 510a010a131038a0ea152f4add3eb7735646fd68..3e0bc38a234242317e1c4517a74ec5f4385c3884 100644
--- a/src/app/qmlregister.h
+++ b/src/app/qmlregister.h
@@ -46,6 +46,9 @@ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
 } // namespace dummy
 
 // clang-format off
+#define QML_REGISTERSINGLETON_TYPE(NS, T) \
+    qmlRegisterSingletonType<T>(NS, MODULE_VER_MAJ, MODULE_VER_MIN, #T, T::create);
+
 #define QML_REGISTERSINGLETONTYPE_POBJECT(NS, I, N) \
     QQmlEngine::setObjectOwnership(I, QQmlEngine::CppOwnership); \
     { using T = std::remove_reference<decltype(*I)>::type; \
@@ -67,6 +70,7 @@ void registerTypes(QQmlEngine* engine,
                    SystemTray* systemTray,
                    AppSettingsManager* appSettingsManager,
                    ConnectivityMonitor* connectivityMonitor,
+                   PreviewEngine* previewEngine,
                    ScreenInfo* screenInfo,
                    QObject* app);
 }
diff --git a/src/app/settingsview/components/CallSettingsPage.qml b/src/app/settingsview/components/CallSettingsPage.qml
index b637c979bab3684504234b0146e35d47a81a922f..431d554f42f066b71fdb7eecad683c358de2bc97 100644
--- a/src/app/settingsview/components/CallSettingsPage.qml
+++ b/src/app/settingsview/components/CallSettingsPage.qml
@@ -34,7 +34,7 @@ SettingsPageBase {
 
     property bool isSIP: CurrentAccount.type === Profile.Type.SIP
     property int itemWidth: 132
-    property string key: PttListener.keyToString(PttListener.getCurrentKey())
+    property string key: PTTListener.keyToString(PTTListener.getCurrentKey())
     title: JamiStrings.callSettingsTitle
 
     function updateAndShowModeratorsSlot() {
@@ -437,7 +437,7 @@ SettingsPageBase {
                     onClicked: {
                         var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ChangePttKeyPopup.qml");
                         dlg.choiceMade.connect(function (chosenKey) {
-                             keyLabel.text = PttListener.keyToString(chosenKey);
+                             keyLabel.text = PTTListener.keyToString(chosenKey);
                         });
                     }
                 }
diff --git a/src/app/settingsview/components/ConnectionMonitoringTable.qml b/src/app/settingsview/components/ConnectionMonitoringTable.qml
index 1337a74a184e6a4c5c5c86d038e8e880ec943647..9819cc46bf25d01dca8bed02973f92d9890ac67e 100644
--- a/src/app/settingsview/components/ConnectionMonitoringTable.qml
+++ b/src/app/settingsview/components/ConnectionMonitoringTable.qml
@@ -14,13 +14,16 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
 import QtQuick
 import QtQuick.Controls
 import QtQuick.Layouts
+
 import net.jami.Adapters 1.1
 import net.jami.Constants 1.1
 import net.jami.Enums 1.1
 import net.jami.Models 1.1
+
 import "../../commoncomponents"
 import "../js/logviewwindowcreation.js" as LogViewWindowCreation
 
@@ -109,16 +112,14 @@ ListView {
 
     model: ConnectionInfoListModel
 
-    Component.onCompleted: {
-        ContactAdapter.updateConnectionInfo();
-    }
+    Component.onCompleted: ConnectionInfoListModel.update()
 
     Timer {
         interval: 1000
         running: root.visible
         repeat: true
         onTriggered: {
-            ContactAdapter.updateConnectionInfo();
+            ConnectionInfoListModel.update();
             listview.rota = listview.rota + 5;
         }
     }
diff --git a/src/app/tipsmodel.h b/src/app/tipsmodel.h
index 4cac64bca8e548171f2862b66263caa427fcc77c..fa3af4356a019e9c674ba622579ad1aa75511132 100644
--- a/src/app/tipsmodel.h
+++ b/src/app/tipsmodel.h
@@ -17,12 +17,14 @@
  */
 #pragma once
 
-#include "lrcinstance.h"
 #include "appsettingsmanager.h"
-#include "qtutils.h"
+
+#include "typedefs.h"
 
 #include <QAbstractListModel>
 #include <QObject>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 #define TIPS_ROLES \
     X(TipId) \
@@ -44,8 +46,14 @@ Q_ENUM_NS(Role)
 class TipsModel : public QAbstractListModel
 {
     Q_OBJECT
+    QML_SINGLETON
 
 public:
+    static TipsModel* create(QQmlEngine*, QJSEngine*)
+    {
+        return new TipsModel(qApp->property("AppSettingsManager").value<AppSettingsManager*>());
+    }
+
     TipsModel(AppSettingsManager* sm, QObject* parent = nullptr);
 
     int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@@ -58,4 +66,4 @@ public Q_SLOTS:
 private:
     VectorMapStringString tips_;
     AppSettingsManager* settingsManager_;
-};
\ No newline at end of file
+};
diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h
index 2f1b6abb7ecae093bb4511afd65ae839996b22e0..5cb5c5a89a7476bcf087d68f66baa1ef257226bc 100644
--- a/src/app/utilsadapter.h
+++ b/src/app/utilsadapter.h
@@ -30,12 +30,15 @@
 
 #include <QApplication>
 #include <QObject>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 #if __has_include(<gio/gio.h>)
 #include <gio/gio.h>
 #endif
 
 #if defined(WIN32) && __has_include(<winrt/Windows.Foundation.h>)
+#define _SILENCE_CLANG_COROUTINE_MESSAGE
 #include <winrt/Windows.Foundation.h>
 
 #define WATCHSYSTEMTHEME __has_include(<winrt/Windows.UI.ViewManagement.h>)
@@ -67,9 +70,18 @@ bool readAppsUseLightThemeRegistry(bool getValue);
 class UtilsAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_PROPERTY(QStringList, logList)
     QML_RO_PROPERTY(bool, isRTL)
 public:
+    static UtilsAdapter* create(QQmlEngine*, QJSEngine*)
+    {
+        return new UtilsAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
+                                qApp->property("SystemTray").value<SystemTray*>(),
+                                qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit UtilsAdapter(AppSettingsManager* settingsManager,
                           SystemTray* systemTray,
                           LRCInstance* instance,
diff --git a/src/app/videodevices.cpp b/src/app/videodevices.cpp
index 133b11727380aec9ab30522247384f73db9bfd0f..a986c4f4b058e9269eb843f10fcc40a8db58ae6f 100644
--- a/src/app/videodevices.cpp
+++ b/src/app/videodevices.cpp
@@ -18,7 +18,8 @@
 
 #include "videodevices.h"
 
-// VideoInputDeviceModel
+#include "api/devicemodel.h"
+
 VideoInputDeviceModel::VideoInputDeviceModel(LRCInstance* lrcInstance,
                                              VideoDevices* videoDeviceInstance)
     : QAbstractListModel(videoDeviceInstance)
@@ -124,7 +125,6 @@ VideoFormatResolutionModel::getCurrentIndex() const
     return resultList.size() > 0 ? resultList[0].row() : 0;
 }
 
-// VideoFormatFpsModel
 VideoFormatFpsModel::VideoFormatFpsModel(LRCInstance* lrcInstance, VideoDevices* videoDeviceInstance)
     : QAbstractListModel(videoDeviceInstance)
     , lrcInstance_(lrcInstance)
@@ -177,7 +177,6 @@ VideoFormatFpsModel::getCurrentIndex() const
     return resultList.size() > 0 ? resultList[0].row() : 0;
 }
 
-// VideoDevices
 VideoDevices::VideoDevices(LRCInstance* lrcInstance, QObject* parent)
     : QObject(parent)
     , lrcInstance_(lrcInstance)
diff --git a/src/app/videodevices.h b/src/app/videodevices.h
index 5232e927031439a47e200871d946820c5e99fc4f..546381d77ce1ada78950f12cb361a540e2f182ab 100644
--- a/src/app/videodevices.h
+++ b/src/app/videodevices.h
@@ -21,9 +21,9 @@
 #include "lrcinstance.h"
 #include "qtutils.h"
 
-#include "api/devicemodel.h"
-
 #include <QObject>
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class VideoDevices;
 
@@ -115,6 +115,8 @@ private:
 class VideoDevices : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
+
     QML_RO_PROPERTY(int, listSize)
 
     QML_RO_PROPERTY(QString, defaultChannel)
@@ -130,6 +132,11 @@ class VideoDevices : public QObject
     QML_RO_PROPERTY(QVariant, sharingFpsSourceModel)
 
 public:
+    static VideoDevices* create(QQmlEngine*, QJSEngine*)
+    {
+        return new VideoDevices(qApp->property("LRCInstance").value<LRCInstance*>());
+    }
+
     explicit VideoDevices(LRCInstance* lrcInstance, QObject* parent = nullptr);
     ~VideoDevices() = default;
 
diff --git a/src/app/wizardview/WizardView.qml b/src/app/wizardview/WizardView.qml
index 2a1f9b738c0f33fc972699d84ef89d83888ff241..995de99198e10120d89fee940008eca61c4bff41 100644
--- a/src/app/wizardview/WizardView.qml
+++ b/src/app/wizardview/WizardView.qml
@@ -50,6 +50,31 @@ BaseView {
         }
     }
 
+    // Handle the end of the wizard account creation process.
+    Connections {
+        target: WizardViewStepModel
+        function onCreateAccountRequested(creationOption) {
+            switch (creationOption) {
+            case WizardViewStepModel.AccountCreationOption.CreateJamiAccount:
+            case WizardViewStepModel.AccountCreationOption.CreateRendezVous:
+            case WizardViewStepModel.AccountCreationOption.ImportFromBackup:
+            case WizardViewStepModel.AccountCreationOption.ImportFromDevice:
+                AccountAdapter.createJamiAccount(WizardViewStepModel.accountCreationInfo);
+                break;
+            case WizardViewStepModel.AccountCreationOption.ConnectToAccountManager:
+                AccountAdapter.createJAMSAccount(WizardViewStepModel.accountCreationInfo);
+                break;
+            case WizardViewStepModel.AccountCreationOption.CreateSipAccount:
+                AccountAdapter.createSIPAccount(WizardViewStepModel.accountCreationInfo);
+                break;
+            default:
+                print("Bad account creation option: " + creationOption);
+                WizardViewStepModel.closeWizardView();
+                break;
+            }
+        }
+    }
+
     Connections {
         target: WizardViewStepModel
 
diff --git a/src/app/wizardviewstepmodel.cpp b/src/app/wizardviewstepmodel.cpp
index fc48c1ad338f636195af28c8d2ed90609109da64..08038f614342391fe95a9bf3bcbd087e970e11ee 100644
--- a/src/app/wizardviewstepmodel.cpp
+++ b/src/app/wizardviewstepmodel.cpp
@@ -18,26 +18,23 @@
 
 #include "wizardviewstepmodel.h"
 
-#include "accountadapter.h"
 #include "appsettingsmanager.h"
+#include "lrcinstance.h"
+
+#include "api/accountmodel.h"
 
 WizardViewStepModel::WizardViewStepModel(LRCInstance* lrcInstance,
-                                         AccountAdapter* accountAdapter,
                                          AppSettingsManager* appSettingsManager,
                                          QObject* parent)
     : QObject(parent)
     , lrcInstance_(lrcInstance)
-    , accountAdapter_(accountAdapter)
     , appSettingsManager_(appSettingsManager)
 {
     reset();
-
-    connect(accountAdapter_,
-            &AccountAdapter::accountAdded,
+    connect(&lrcInstance_->accountModel(),
+            &AccountModel::accountAdded,
             this,
-            [this](QString accountId, int index) {
-                accountAdapter_->changeAccount(index);
-
+            [this](const QString& accountId) {
                 auto accountCreationOption = get_accountCreationOption();
                 if (accountCreationOption == AccountCreationOption::ConnectToAccountManager
                     || accountCreationOption == AccountCreationOption::CreateSipAccount) {
@@ -66,30 +63,8 @@ WizardViewStepModel::startAccountCreationFlow(AccountCreationOption accountCreat
 void
 WizardViewStepModel::nextStep()
 {
-    auto accountCreationOption = get_accountCreationOption();
-    if (get_mainStep() == MainSteps::Initial
-        || accountCreationOption == AccountCreationOption::None) {
-        return;
-    }
-
-    switch (accountCreationOption) {
-    case AccountCreationOption::CreateJamiAccount:
-    case AccountCreationOption::CreateRendezVous:
-    case AccountCreationOption::ImportFromBackup:
-    case AccountCreationOption::ImportFromDevice: {
-        accountAdapter_->createJamiAccount(get_accountCreationInfo());
-        break;
-    }
-    case AccountCreationOption::ConnectToAccountManager: {
-        accountAdapter_->createJAMSAccount(get_accountCreationInfo());
-        break;
-    }
-    case AccountCreationOption::CreateSipAccount: {
-        accountAdapter_->createSIPAccount(get_accountCreationInfo());
-        break;
-    }
-    default:
-        return;
+    if (mainStep_ != MainSteps::Initial) {
+        Q_EMIT createAccountRequested(accountCreationOption_);
     }
 }
 
diff --git a/src/app/wizardviewstepmodel.h b/src/app/wizardviewstepmodel.h
index c21decead1c550ae41d118c2c9ebb9333d57d474..4928325e78a3dccbd1a6ab73f80aee3cb6bf1d51 100644
--- a/src/app/wizardviewstepmodel.h
+++ b/src/app/wizardviewstepmodel.h
@@ -18,11 +18,13 @@
 
 #pragma once
 
+#include "qtutils.h"
+
 #include <QObject>
 #include <QVariant>
 #include <QMap>
-
-#include "qtutils.h"
+#include <QQmlEngine>   // QML registration
+#include <QApplication> // QML registration
 
 class AccountAdapter;
 class LRCInstance;
@@ -31,6 +33,7 @@ class AppSettingsManager;
 class WizardViewStepModel : public QObject
 {
     Q_OBJECT
+    QML_SINGLETON
     Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
 
 public:
@@ -54,12 +57,17 @@ public:
 
     QML_PROPERTY(MainSteps, mainStep)
     QML_PROPERTY(AccountCreationOption, accountCreationOption)
-
     QML_PROPERTY(QVariantMap, accountCreationInfo)
 
 public:
+    static WizardViewStepModel* create(QQmlEngine*, QJSEngine*)
+    {
+        return new WizardViewStepModel(qApp->property("LRCInstance").value<LRCInstance*>(),
+                                       qApp->property("AppSettingsManager")
+                                           .value<AppSettingsManager*>());
+    }
+
     explicit WizardViewStepModel(LRCInstance* lrcInstance,
-                                 AccountAdapter* accountAdapter,
                                  AppSettingsManager* appSettingsManager,
                                  QObject* parent = nullptr);
 
@@ -70,11 +78,11 @@ public:
 Q_SIGNALS:
     void accountIsReady(QString accountId);
     void closeWizardView();
+    void createAccountRequested(AccountCreationOption);
 
 private:
     void reset();
 
     LRCInstance* lrcInstance_;
-    AccountAdapter* accountAdapter_;
     AppSettingsManager* appSettingsManager_;
 };
diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp
index a604a5c729277fb55279885777619209d0a17e95..d25e81cebcfca2d46b40bdbfb1359dfbb3472295 100644
--- a/tests/qml/main.cpp
+++ b/tests/qml/main.cpp
@@ -136,6 +136,7 @@ public Q_SLOTS:
                              systemTray_.get(),
                              settingsManager_.get(),
                              connectivityMonitor_.get(),
+                             previewEngine_.get(),
                              &screenInfo_,
                              this);
 
diff --git a/tests/qml/src/TestWrapper.qml b/tests/qml/src/TestWrapper.qml
index 3d22ce6c3bef693dc149ed082872ce8240beba04..d8f4510d4b70aef954429bb585d9c826b5ac92d9 100644
--- a/tests/qml/src/TestWrapper.qml
+++ b/tests/qml/src/TestWrapper.qml
@@ -17,6 +17,7 @@
 
 import QtQuick
 import QtQuick.Controls
+import QtTest
 
 import "../../../src/app/"
 
@@ -24,8 +25,18 @@ import "../../../src/app/"
 // each UUT from having to manage its own top level app management objects
 // (currently ViewManager, ViewCoordinator, and ApplicationWindow).
 Item {
-    // This will be our UUT.
-    required default property var uut
+    id: tw
+
+    width: childrenRect.width
+    height: childrenRect.height
+
+    // A binding to the windowShown property
+    Binding {
+        tw.appWindow: uut.Window.window
+        when: QTestRootObject.windowShown
+    }
+
+    Component.onCompleted: viewCoordinator.init(this)
 
     // These are our fake app management objects. The caveat is that they
     // must be maintained in sync with the actual objects in the app for now.
@@ -33,7 +44,7 @@ Item {
     // sync them.
     property ViewManager viewManager: ViewManager {}
     property ViewCoordinator viewCoordinator: ViewCoordinator {}
-    property ApplicationWindow appWindow: ApplicationWindow {
+    property QtObject appWindow: QtObject {
         property bool useFrameless: false
     }
 }
diff --git a/tests/qml/src/tst_CallMessageDelegate.qml b/tests/qml/src/tst_CallMessageDelegate.qml
index 4d68c32a0ae7b820206cce5ed14ed42e603be5ee..a7b022b75d5958a1aa80f5a4e21fa2c453142998 100644
--- a/tests/qml/src/tst_CallMessageDelegate.qml
+++ b/tests/qml/src/tst_CallMessageDelegate.qml
@@ -28,17 +28,20 @@ import "../../../src/app/mainview"
 import "../../../src/app/mainview/components"
 import "../../../src/app/commoncomponents"
 
-CallMessageDelegate {
-    id: uut
-    type: Interaction.Type.CALL
+TestWrapper {
+    CallMessageDelegate {
+        id: uut
 
-    TestCase {
-        name: "Check basic visibility for header buttons"
-        function test_checkBasicVisibility() {
-            var moreButton = findChild(uut, "more")
-            var replyButton = findChild(uut, "reply")
-            compare(moreButton.visible, false)
-            compare(replyButton.visible, false)
+        type: Interaction.Type.CALL
+
+        TestCase {
+            name: "Check basic visibility for option buttons"
+            function test_checkOptionButtonsVisibility() {
+                var moreButton = findChild(uut, "more")
+                var replyButton = findChild(uut, "reply")
+                compare(moreButton.visible, false)
+                compare(replyButton.visible, false)
+            }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/qml/src/tst_OngoingCallPage.qml b/tests/qml/src/tst_OngoingCallPage.qml
index 62f9a3754f9a93a35c77b2bca16cd69382603797..0485d61c4faf3effa367d51c6d57c6c039e4541e 100644
--- a/tests/qml/src/tst_OngoingCallPage.qml
+++ b/tests/qml/src/tst_OngoingCallPage.qml
@@ -25,67 +25,59 @@ import net.jami.Constants 1.1
 import "../../../src/app/"
 import "../../../src/app/mainview/components"
 
-OngoingCallPage {
-    id: uut
-
-    width: 800
-    height: 600
-
-    property QtObject appWindow
-    property ViewManager viewManager: ViewManager {}
-    property ViewCoordinator viewCoordinator: ViewCoordinator {}
-
-    TestCase {
-        name: "Check basic visibility of action bar during a call"
-        when: windowShown // Mouse events can only be handled
-                          // after the window has been shown.
-
-        property var callOverlay
-        property var mainOverlay
-
-        function initTestCase() {
-            callOverlay = findChild(uut, "callOverlay")
-            mainOverlay = findChild(callOverlay, "mainOverlay")
-
-            // The CallActionBar on the OngoingCallPage starts out invisible and
-            // is made visible whenever the user moves their mouse.
-            // This is implemented via an event filter in the CallOverlayModel
-            // class. The event filter is created when the MainOverlay becomes
-            // visible. In the actual Jami application, this happens when a call
-            // is started, but we need to toggle the visiblity manually here
-            // because the MainOverlay is visible at the beginning of the test.
-            appWindow = uut.Window.window
-            mainOverlay.visible = false
-            mainOverlay.visible = true
-
-            // Calling mouseMove() will generate warnings if we don't call init first.
-            viewCoordinator.init(uut)
-        }
-
-        function test_checkBasicVisibility() {
-            var callActionBar = findChild(mainOverlay, "callActionBar")
-
-            // The primary and secondary actions in the CallActionBar are currently being added
-            // one by one (not using a loop) to CallOverlayModel in the Component.onCompleted
-            // block of CallActionBar.qml. The two lines below are meant as a sanity check
-            // that no action has been forgotten.
-            compare(callActionBar.primaryActions.length, CallOverlayModel.primaryModel().rowCount())
-            compare(callActionBar.secondaryActions.length, CallOverlayModel.secondaryModel().rowCount())
-
-            compare(callActionBar.visible, false)
-            mouseMove(uut)
-
-            // We need to wait for the fade-in animation of the CallActionBar to be completed
-            // before we check that it's visible.
-            var waitTime = JamiTheme.overlayFadeDuration + 100
-            // Make sure we have time to check that the CallActioinBar is visible before it fades out:
-            verify(waitTime + 100 < JamiTheme.overlayFadeDelay)
-            // Note: The CallActionBar is supposed to stay visible for a few seconds. If the above
-            // check fails, then this means that either overlayFadeDuration or overlayFadeDelay
-            // got changed to a value that's way too high/low.
-
-            wait(waitTime)
-            compare(callActionBar.visible, true)
+TestWrapper {
+    OngoingCallPage {
+        id: uut
+
+        width: 800
+        height: 600
+
+        TestCase {
+            name: "Check basic visibility of action bar during a call"
+            when: windowShown // Mouse events can only be handled
+                              // after the window has been shown.
+
+            property var mainOverlay
+
+            function initTestCase() {
+                mainOverlay = findChild(uut, "mainOverlay")
+
+                // The CallActionBar on the OngoingCallPage starts out invisible and
+                // is made visible whenever the user moves their mouse.
+                // This is implemented via an event filter in the CallOverlayModel
+                // class. The event filter is created when the MainOverlay becomes
+                // visible. In the actual Jami application, this happens when a call
+                // is started, but we need to toggle the visiblity manually here
+                // because the MainOverlay is visible at the beginning of the test.
+                mainOverlay.visible = false
+                mainOverlay.visible = true
+            }
+
+            function test_checkCallActionBarVisibility() {
+                var callActionBar = findChild(mainOverlay, "callActionBar")
+
+                // The primary and secondary actions in the CallActionBar are currently being added
+                // one by one (not using a loop) to CallOverlayModel in the Component.onCompleted
+                // block of CallActionBar.qml. The two lines below are meant as a sanity check
+                // that no action has been forgotten.
+                compare(callActionBar.primaryActions.length, CallOverlayModel.primaryModel().rowCount())
+                compare(callActionBar.secondaryActions.length, CallOverlayModel.secondaryModel().rowCount())
+
+                compare(callActionBar.visible, false)
+                mouseMove(uut)
+
+                // We need to wait for the fade-in animation of the CallActionBar to be completed
+                // before we check that it's visible.
+                var waitTime = JamiTheme.overlayFadeDuration + 100
+                // Make sure we have time to check that the CallActioinBar is visible before it fades out:
+                verify(waitTime + 100 < JamiTheme.overlayFadeDelay)
+                // Note: The CallActionBar is supposed to stay visible for a few seconds. If the above
+                // check fails, then this means that either overlayFadeDuration or overlayFadeDelay
+                // got changed to a value that's way too high/low.
+
+                wait(waitTime)
+                compare(callActionBar.visible, true)
+            }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/qml/src/tst_WelcomePage.qml b/tests/qml/src/tst_WelcomePage.qml
index d7cc95883cda98273236db57f0e90c7e0d52ce19..3acefa89f9cc59c8d79556cc5b67d3036e4e9562 100644
--- a/tests/qml/src/tst_WelcomePage.qml
+++ b/tests/qml/src/tst_WelcomePage.qml
@@ -24,9 +24,12 @@ import "../../../src/app/"
 import "../../../src/app/mainview/components"
 
 TestWrapper {
-    uut: WelcomePage {
+    WelcomePage {
+        id: uut
+
         TestCase {
             name: "Open 'About Jami' popup"
+            when: windowShown
 
             function test_openAboutPopup() {
                 var aboutJamiButton = findChild(uut, "aboutJami")