summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2023-06-19 01:38:40 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2023-06-19 01:38:40 +0200
commitce1a64bc19ffc21e115bdf2587bb053d7a417f3e (patch)
treea195d127228218772a535448de642f0bb7b6d053
parentRemove explicit link styling (diff)
downloadnheko-ce1a64bc19ffc21e115bdf2587bb053d7a417f3e.tar.xz
Move to automatic type registration
-rw-r--r--CMakeLists.txt136
-rw-r--r--resources/qml/Avatar.qml2
-rw-r--r--resources/qml/ImageButton.qml2
-rw-r--r--resources/qml/MatrixText.qml2
-rw-r--r--resources/qml/MessageView.qml4
-rw-r--r--resources/qml/Root.qml8
-rw-r--r--resources/qml/TimelineRow.qml2
-rw-r--r--resources/qml/TimelineView.qml19
-rw-r--r--resources/qml/TopBar.qml2
-rw-r--r--resources/qml/components/AdaptiveLayout.qml2
-rw-r--r--resources/qml/components/TextButton.qml2
-rw-r--r--resources/qml/delegates/FileMessage.qml2
-rw-r--r--resources/qml/delegates/Reply.qml2
-rw-r--r--resources/qml/delegates/TextMessage.qml2
-rw-r--r--resources/qml/dialogs/InviteDialog.qml2
-rw-r--r--resources/qml/dialogs/ReadReceipts.qml2
-rw-r--r--resources/qml/dialogs/RoomMembers.qml2
-rw-r--r--resources/qml/dialogs/RoomSettings.qml2
-rw-r--r--resources/qml/emoji/StickerPicker.qml1
-rw-r--r--resources/qml/ui/animations/qmldir2
-rw-r--r--resources/qml/ui/media/qmldir3
-rw-r--r--resources/qml/ui/qmldir4
-rw-r--r--resources/qml/voip/ActiveCallBar.qml24
-rw-r--r--resources/qml/voip/CallDevices.qml2
-rw-r--r--resources/qml/voip/CallInvite.qml8
-rw-r--r--resources/qml/voip/CallInviteBar.qml8
-rw-r--r--resources/qml/voip/PlaceCall.qml4
-rw-r--r--resources/qml/voip/ScreenShare.qml8
-rw-r--r--resources/res.qrc100
-rw-r--r--src/AliasEditModel.h4
-rw-r--r--src/CacheCryptoStructs.h3
-rw-r--r--src/Clipboard.h4
-rw-r--r--src/LoginPage.h8
-rw-r--r--src/MainWindow.cpp215
-rw-r--r--src/MainWindow.h25
-rw-r--r--src/PowerlevelsEditModels.h4
-rw-r--r--src/ReadReceiptsModel.h4
-rw-r--r--src/RegisterPage.h2
-rw-r--r--src/RoomDirectoryModel.h2
-rw-r--r--src/UserDirectoryModel.h2
-rw-r--r--src/UserSettingsPage.h24
-rw-r--r--src/encryption/Olm.h3
-rw-r--r--src/encryption/SelfVerificationStatus.cpp5
-rw-r--r--src/encryption/SelfVerificationStatus.h4
-rw-r--r--src/encryption/VerificationManager.cpp1
-rw-r--r--src/encryption/VerificationManager.h27
-rw-r--r--src/main.cpp2
-rw-r--r--src/timeline/CommunitiesModel.cpp1
-rw-r--r--src/timeline/CommunitiesModel.h28
-rw-r--r--src/timeline/DelegateChooser.h2
-rw-r--r--src/timeline/InputBar.h6
-rw-r--r--src/timeline/PresenceEmitter.h27
-rw-r--r--src/timeline/RoomlistModel.cpp2
-rw-r--r--src/timeline/RoomlistModel.h26
-rw-r--r--src/timeline/TimelineFilter.h2
-rw-r--r--src/timeline/TimelineModel.h5
-rw-r--r--src/timeline/TimelineViewManager.cpp30
-rw-r--r--src/timeline/TimelineViewManager.h25
-rw-r--r--src/ui/HiddenEvents.h2
-rw-r--r--src/ui/MxcAnimatedImage.h1
-rw-r--r--src/ui/MxcMediaProxy.h3
-rw-r--r--src/ui/NhekoCursorShape.h2
-rw-r--r--src/ui/NhekoDropArea.h1
-rw-r--r--src/ui/NhekoEventObserver.cpp60
-rw-r--r--src/ui/NhekoEventObserver.h27
-rw-r--r--src/ui/NhekoGlobalObject.h4
-rw-r--r--src/ui/RoomSummary.h4
-rw-r--r--src/ui/UIA.h22
-rw-r--r--src/ui/UserProfile.h2
-rw-r--r--src/voip/CallManager.cpp21
-rw-r--r--src/voip/CallManager.h7
-rw-r--r--src/voip/WebRTCSession.cpp40
-rw-r--r--src/voip/WebRTCSession.h2
73 files changed, 574 insertions, 476 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3feb49f5..50940246 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.13)
+cmake_minimum_required(VERSION 3.13..3.21)
 
 option(APPVEYOR_BUILD "Build on appveyor" OFF)
 option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF)
@@ -395,8 +395,6 @@ set(SRC_FILES
 	src/ui/NhekoCursorShape.h
 	src/ui/NhekoDropArea.cpp
 	src/ui/NhekoDropArea.h
-	src/ui/NhekoEventObserver.cpp
-	src/ui/NhekoEventObserver.h
 	src/ui/NhekoGlobalObject.cpp
 	src/ui/NhekoGlobalObject.h
 	src/ui/RoomSettings.cpp
@@ -499,8 +497,6 @@ set(SRC_FILES
 	src/SingleImagePackModel.h
 	src/TrayIcon.cpp
 	src/TrayIcon.h
-	src/UserDirectoryModel.cpp
-	src/UserDirectoryModel.h
 	src/UserSettingsPage.cpp
 	src/UserSettingsPage.h
 	src/UsersModel.cpp
@@ -679,10 +675,10 @@ if(ASAN)
 endif()
 
 if(WIN32)
-	add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS})
+	qt_add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS})
 	target_compile_definitions(nheko PRIVATE _WIN32_WINNT=0x0601 NOMINMAX WIN32_LEAN_AND_MEAN STRICT)
 else()
-	add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
+	qt_add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
 
 	if (HAVE_BACKTRACE_SYMBOLS_FD AND NOT CMAKE_BUILD_TYPE STREQUAL "Release")
 		set_target_properties(nheko PROPERTIES ENABLE_EXPORTS ON)
@@ -700,6 +696,130 @@ set_target_properties(nheko
 file(GLOB LANG_TS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/resources/langs/*.ts")
 qt_add_translations(nheko RESOURCE_PREFIX "/translations" TS_FILES ${LANG_TS_SRC})
 
+
+#
+# Add qml files
+#
+
+set(QML_SOURCES
+        resources/qml/Root.qml
+        resources/qml/ChatPage.qml
+        resources/qml/CommunitiesList.qml
+        resources/qml/RoomList.qml
+        resources/qml/TimelineView.qml
+        resources/qml/Avatar.qml
+        resources/qml/Completer.qml
+        resources/qml/EncryptionIndicator.qml
+        resources/qml/ImageButton.qml
+        resources/qml/ElidedLabel.qml
+        resources/qml/MatrixText.qml
+        resources/qml/MatrixTextField.qml
+        resources/qml/ToggleButton.qml
+        resources/qml/UploadBox.qml
+        resources/qml/MessageInput.qml
+        resources/qml/MessageView.qml
+        resources/qml/PrivacyScreen.qml
+        resources/qml/Reactions.qml
+        resources/qml/ReplyPopup.qml
+        resources/qml/StatusIndicator.qml
+        resources/qml/TimelineRow.qml
+        resources/qml/TopBar.qml
+        resources/qml/QuickSwitcher.qml
+        resources/qml/ForwardCompleter.qml
+        resources/qml/SelfVerificationCheck.qml
+        resources/qml/TypingIndicator.qml
+        resources/qml/MessageInputWarning.qml
+        resources/qml/components/AdaptiveLayout.qml
+        resources/qml/components/AdaptiveLayoutElement.qml
+        resources/qml/components/AvatarListTile.qml
+        resources/qml/components/FlatButton.qml
+        resources/qml/components/MainWindowDialog.qml
+        resources/qml/components/NhekoTabButton.qml
+        resources/qml/components/NotificationBubble.qml
+        resources/qml/components/ReorderableListview.qml
+        resources/qml/components/SpaceMenuLevel.qml
+        resources/qml/components/TextButton.qml
+        resources/qml/components/UserListRow.qml
+        resources/qml/delegates/Encrypted.qml
+        resources/qml/delegates/FileMessage.qml
+        resources/qml/delegates/ImageMessage.qml
+        resources/qml/delegates/MessageDelegate.qml
+        resources/qml/delegates/NoticeMessage.qml
+        resources/qml/delegates/Pill.qml
+        resources/qml/delegates/Placeholder.qml
+        resources/qml/delegates/PlayableMediaMessage.qml
+        resources/qml/delegates/Redacted.qml
+        resources/qml/delegates/Reply.qml
+        resources/qml/delegates/TextMessage.qml
+        resources/qml/device-verification/DeviceVerification.qml
+        resources/qml/device-verification/DigitVerification.qml
+        resources/qml/device-verification/EmojiVerification.qml
+        resources/qml/device-verification/Failed.qml
+        resources/qml/device-verification/NewVerificationRequest.qml
+        resources/qml/device-verification/Success.qml
+        resources/qml/device-verification/Waiting.qml
+        resources/qml/dialogs/AliasEditor.qml
+        resources/qml/dialogs/ConfirmJoinRoomDialog.qml
+        resources/qml/dialogs/CreateDirect.qml
+        resources/qml/dialogs/CreateRoom.qml
+        resources/qml/dialogs/HiddenEventsDialog.qml
+        resources/qml/dialogs/ImageOverlay.qml
+        resources/qml/dialogs/ImagePackEditorDialog.qml
+        resources/qml/dialogs/ImagePackSettingsDialog.qml
+        resources/qml/dialogs/InputDialog.qml
+        resources/qml/dialogs/InviteDialog.qml
+        resources/qml/dialogs/JoinRoomDialog.qml
+        resources/qml/dialogs/LeaveRoomDialog.qml
+        resources/qml/dialogs/LogoutDialog.qml
+        resources/qml/dialogs/PhoneNumberInputDialog.qml
+        resources/qml/dialogs/PowerLevelEditor.qml
+        resources/qml/dialogs/PowerLevelSpacesApplyDialog.qml
+        resources/qml/dialogs/RawMessageDialog.qml
+        resources/qml/dialogs/ReadReceipts.qml
+        resources/qml/dialogs/RoomDirectory.qml
+        resources/qml/dialogs/RoomMembers.qml
+        resources/qml/dialogs/AllowedRoomsSettingsDialog.qml
+        resources/qml/dialogs/RoomSettings.qml
+        resources/qml/dialogs/UserProfile.qml
+        resources/qml/emoji/StickerPicker.qml
+        resources/qml/pages/LoginPage.qml
+        resources/qml/pages/RegisterPage.qml
+        resources/qml/pages/UserSettingsPage.qml
+        resources/qml/pages/WelcomePage.qml
+        resources/qml/ui/NhekoSlider.qml
+        resources/qml/ui/Ripple.qml
+        resources/qml/ui/Snackbar.qml
+        resources/qml/ui/Spinner.qml
+        resources/qml/ui/animations/BlinkAnimation.qml
+        resources/qml/ui/media/MediaControls.qml
+        resources/qml/voip/ActiveCallBar.qml
+        resources/qml/voip/CallDevices.qml
+        resources/qml/voip/CallInvite.qml
+        resources/qml/voip/CallInviteBar.qml
+        resources/qml/voip/DeviceError.qml
+        resources/qml/voip/PlaceCall.qml
+        resources/qml/voip/ScreenShare.qml
+        resources/qml/voip/VideoCall.qml
+        resources/qml/delegates/EncryptionEnabled.qml
+        resources/qml/ui/TimelineEffects.qml
+)
+qt_add_qml_module(nheko
+	URI im.nheko
+	NO_RESOURCE_TARGET_PATH
+	RESOURCE_PREFIX "/"
+	VERSION 1.1
+	DEPENDENCIES QtQml QtQuick # https://bugreports.qt.io/browse/QTBUG-102554
+	QML_FILES
+	${QML_SOURCES}
+	SOURCES
+	src/UserDirectoryModel.cpp
+	src/UserDirectoryModel.h
+	)
+	#qt_target_qml_sources(nheko
+	#	#PREFIX "/"
+	#)
+
+
 if(WIN32)
 	target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN)
 	if(MSVC)
@@ -712,7 +832,7 @@ else()
 	endif()
 endif()
 
-target_include_directories(nheko PRIVATE src includes)
+target_include_directories(nheko PRIVATE src includes src/timeline/ src/ui/ src/encryption/ src/voip/)
 
 if (USE_BUNDLED_CPPHTTPLIB)
 	target_include_directories(nheko PRIVATE third_party/cpp-httplib-0.5.12)
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 5a28b5de..39061d2b 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -102,7 +102,7 @@ AbstractButton {
             target: Presence
         }
     }
-    CursorShape {
+    NhekoCursorShape {
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
     }
diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index 4115cd0a..e2e1b914 100644
--- a/resources/qml/ImageButton.qml
+++ b/resources/qml/ImageButton.qml
@@ -31,7 +31,7 @@ AbstractButton {
         sourceSize.height: button.height
         sourceSize.width: button.width
     }
-    CursorShape {
+    NhekoCursorShape {
         id: mouseArea
 
         anchors.fill: parent
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 6d611311..7a71182b 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -44,7 +44,7 @@ TextArea {
     onPressAndHold: (event) => event.accepted = false
     onPressed: (event) => event.accepted = (event.button == Qt.LeftButton)
 
-    CursorShape {
+    NhekoCursorShape {
         id: cs
 
         anchors.fill: parent
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 3eed4c48..af3a3371 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -332,7 +332,7 @@ Item {
                             sourceSize.height: button.height
                             sourceSize.width: button.width
                         }
-                        CursorShape {
+                        NhekoCursorShape {
                             anchors.fill: parent
                             cursorShape: Qt.PointingHandCursor
                         }
@@ -590,7 +590,7 @@ Item {
                             elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3)
                             text: userName
                         }
-                        CursorShape {
+                        NhekoCursorShape {
                             anchors.fill: parent
                             cursorShape: Qt.PointingHandCursor
                         }
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index b869f95a..85c10ddd 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -15,7 +15,6 @@ import QtQuick.Controls
 import QtQuick.Layouts
 import QtQuick.Window
 import im.nheko
-import im.nheko.EmojiModel
 
 Pane {
     id: timelineRoot
@@ -93,14 +92,13 @@ Pane {
         id: fontMetrics
 
     }
-    RoomDirectoryModel {
-        id: publicRooms
-
-    }
     UserDirectoryModel {
         id: userDirectory
 
     }
+    RoomDirectoryModel {
+        id: publicRooms
+    }
     Component {
         id: readReceiptsDialog
 
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 9c8ebdc6..16a31a3c 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -173,7 +173,7 @@ AbstractButton {
                 Layout.row: 0
                 blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? ""
                 body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? ""
-                callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
+                callType: r.relatedEventCacheBuster, fromModel(Room.Voip) ?? ""
                 duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0
                 encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0
                 eventId: fromModel(Room.EventId) ?? ""
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 18085f28..7b89eb4b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -8,14 +8,13 @@ import "./device-verification"
 import "./emoji"
 import "./ui"
 import "./voip"
-import Qt.labs.platform 1.1 as Platform
-import QtQuick 2.15
-import QtQuick.Controls 2.5
-import QtQuick.Layouts 1.3
-import QtQuick.Particles 2.15
-import QtQuick.Window 2.13
-import im.nheko 1.0
-import im.nheko.EmojiModel 1.0
+import Qt.labs.platform as Platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Particles
+import QtQuick.Window
+import im.nheko
 
 Item {
     id: timelineView
@@ -123,7 +122,7 @@ Item {
                         searchString: topBar.searchString
                     }
                     Loader {
-                        source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
+                        source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? "voip/VideoCall.qml" : ""
 
                         onLoaded: TimelineManager.setVideoCallItem()
                     }
@@ -249,7 +248,7 @@ Item {
 
                 onLinkActivated: Nheko.openLink(link)
 
-                CursorShape {
+                NhekoCursorShape {
                     anchors.fill: parent
                     cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
                 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 3f2d8d2a..699595e6 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -369,7 +369,7 @@ Pane {
                 onAccepted: topBar.searchString = text
             }
         }
-        CursorShape {
+        NhekoCursorShape {
             anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0)
             anchors.fill: parent
             cursorShape: Qt.PointingHandCursor
diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml
index 9fc27055..eea74006 100644
--- a/resources/qml/components/AdaptiveLayout.qml
+++ b/resources/qml/components/AdaptiveLayout.qml
@@ -87,7 +87,7 @@ Container {
         x: parent.preferredWidth
         z: 3
 
-        CursorShape {
+        NhekoCursorShape {
             height: parent.height
             width: container.splitterGrabMargin * 2
             x: -container.splitterGrabMargin
diff --git a/resources/qml/components/TextButton.qml b/resources/qml/components/TextButton.qml
index 0b1ac270..b6153f22 100644
--- a/resources/qml/components/TextButton.qml
+++ b/resources/qml/components/TextButton.qml
@@ -32,7 +32,7 @@ AbstractButton {
         horizontalAlignment: Text.AlignHCenter
     }
 
-    CursorShape {
+    NhekoCursorShape {
         id: mouseArea
 
         anchors.fill: parent
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 48546592..82b82c1b 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -49,7 +49,7 @@ Item {
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
-            CursorShape {
+            NhekoCursorShape {
                 anchors.fill: parent
                 cursorShape: Qt.PointingHandCursor
             }
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 94128960..4d4983ac 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -43,7 +43,7 @@ AbstractButton {
     implicitHeight: replyContainer.height
     implicitWidth: visible? colorLine.width+Math.max(replyContainer.implicitWidth,userName_.fullTextWidth) : 0 // visible? seems to be causing issues
 
-    CursorShape {
+    NhekoCursorShape {
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
     }
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index 1237180b..1eb5e2c0 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -46,7 +46,7 @@ MatrixText {
     enabled: !Settings.mobileMode
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
 
-    CursorShape {
+    NhekoCursorShape {
         enabled: isReply
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
diff --git a/resources/qml/dialogs/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml
index f5d467db..58bae7fd 100644
--- a/resources/qml/dialogs/InviteDialog.qml
+++ b/resources/qml/dialogs/InviteDialog.qml
@@ -212,7 +212,7 @@ ApplicationWindow {
                         onClicked: invitees.removeUser(model.mxid)
                     }
 
-                    CursorShape {
+                    NhekoCursorShape {
                         anchors.fill: parent
                         cursorShape: Qt.PointingHandCursor
                     }
diff --git a/resources/qml/dialogs/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
index 523b47cb..d65de73c 100644
--- a/resources/qml/dialogs/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -110,7 +110,7 @@ ApplicationWindow {
 
                     }
 
-                    CursorShape {
+                    NhekoCursorShape {
                         anchors.fill: parent
                         cursorShape: Qt.PointingHandCursor
                     }
diff --git a/resources/qml/dialogs/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml
index 916c6c86..bbf1605d 100644
--- a/resources/qml/dialogs/RoomMembers.qml
+++ b/resources/qml/dialogs/RoomMembers.qml
@@ -220,7 +220,7 @@ ApplicationWindow {
 
                     }
 
-                    CursorShape {
+                    NhekoCursorShape {
                         anchors.fill: parent
                         cursorShape: Qt.PointingHandCursor
                     }
diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml
index 0fa98fff..1f011bf1 100644
--- a/resources/qml/dialogs/RoomSettings.qml
+++ b/resources/qml/dialogs/RoomSettings.qml
@@ -213,7 +213,7 @@ ApplicationWindow {
                 horizontalAlignment: TextEdit.AlignHCenter
                 onLinkActivated: Nheko.openLink(link)
 
-                CursorShape {
+                NhekoCursorShape {
                     anchors.fill: parent
                     cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
                 }
diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml
index c10e57e7..b7721db6 100644
--- a/resources/qml/emoji/StickerPicker.qml
+++ b/resources/qml/emoji/StickerPicker.qml
@@ -7,7 +7,6 @@ import QtQuick
 import QtQuick.Controls
 import QtQuick.Layouts
 import im.nheko
-import im.nheko.EmojiModel
 
 Menu {
     id: stickerPopup
diff --git a/resources/qml/ui/animations/qmldir b/resources/qml/ui/animations/qmldir
deleted file mode 100644
index 14f9ad86..00000000
--- a/resources/qml/ui/animations/qmldir
+++ /dev/null
@@ -1,2 +0,0 @@
-module im.nheko.UI.Animations
-BlinkAnimation 1.0 BlinkAnimation.qml
diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir
deleted file mode 100644
index 143b603d..00000000
--- a/resources/qml/ui/media/qmldir
+++ /dev/null
@@ -1,3 +0,0 @@
-module im.nheko.UI.Media
-VolumeSlider 1.0 VolumeSlider.qml
-MediaControls 1.0 MediaControls.qml
\ No newline at end of file
diff --git a/resources/qml/ui/qmldir b/resources/qml/ui/qmldir
deleted file mode 100644
index a2ce7514..00000000
--- a/resources/qml/ui/qmldir
+++ /dev/null
@@ -1,4 +0,0 @@
-module im.nheko.UI
-NhekoSlider 1.0 NhekoSlider.qml
-Ripple 1.0 Ripple.qml
-Spinner 1.0 Spinner.qml
\ No newline at end of file
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index d055c013..45afedab 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -16,7 +16,7 @@ Rectangle {
     MouseArea {
         anchors.fill: parent
         onClicked: {
-            if (CallManager.callType != CallType.VOICE)
+            if (CallManager.callType != Voip.VOICE)
                 stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
 
         }
@@ -58,7 +58,7 @@ Rectangle {
             states: [
                 State {
                     name: "VOICE"
-                    when: CallManager.callType == CallType.VOICE
+                    when: CallManager.callType == Voip.VOICE
 
                     PropertyChanges {
                         target: callTypeIcon
@@ -68,7 +68,7 @@ Rectangle {
                 },
                 State {
                     name: "VIDEO"
-                    when: CallManager.callType == CallType.VIDEO
+                    when: CallManager.callType == Voip.VIDEO
 
                     PropertyChanges {
                         target: callTypeIcon
@@ -78,7 +78,7 @@ Rectangle {
                 },
                 State {
                     name: "SCREEN"
-                    when: CallManager.callType == CallType.SCREEN
+                    when: CallManager.callType == Voip.SCREEN
 
                     PropertyChanges {
                         target: callTypeIcon
@@ -100,7 +100,7 @@ Rectangle {
             states: [
                 State {
                     name: "OFFERSENT"
-                    when: CallManager.callState == WebRTCState.OFFERSENT
+                    when: CallManager.callState == Voip.OFFERSENT
 
                     PropertyChanges {
                         target: callStateLabel
@@ -110,7 +110,7 @@ Rectangle {
                 },
                 State {
                     name: "CONNECTING"
-                    when: CallManager.callState == WebRTCState.CONNECTING
+                    when: CallManager.callState == Voip.CONNECTING
 
                     PropertyChanges {
                         target: callStateLabel
@@ -120,7 +120,7 @@ Rectangle {
                 },
                 State {
                     name: "ANSWERSENT"
-                    when: CallManager.callState == WebRTCState.ANSWERSENT
+                    when: CallManager.callState == Voip.ANSWERSENT
 
                     PropertyChanges {
                         target: callStateLabel
@@ -130,7 +130,7 @@ Rectangle {
                 },
                 State {
                     name: "CONNECTED"
-                    when: CallManager.callState == WebRTCState.CONNECTED
+                    when: CallManager.callState == Voip.CONNECTED
 
                     PropertyChanges {
                         target: callStateLabel
@@ -144,13 +144,13 @@ Rectangle {
 
                     PropertyChanges {
                         target: stackLayout
-                        currentIndex: CallManager.callType != CallType.VOICE ? 1 : 0
+                        currentIndex: CallManager.callType != Voip.VOICE ? 1 : 0
                     }
 
                 },
                 State {
                     name: "DISCONNECTED"
-                    when: CallManager.callState == WebRTCState.DISCONNECTED
+                    when: CallManager.callState == Voip.DISCONNECTED
 
                     PropertyChanges {
                         target: callStateLabel
@@ -176,7 +176,7 @@ Rectangle {
             }
 
             interval: 1000
-            running: CallManager.callState == WebRTCState.CONNECTED
+            running: CallManager.callState == Voip.CONNECTED
             repeat: true
             onTriggered: {
                 var d = new Date();
@@ -190,7 +190,7 @@ Rectangle {
 
         Label {
             Layout.leftMargin: 16
-            visible: CallManager.callType == CallType.SCREEN && CallManager.callState == WebRTCState.CONNECTED
+            visible: CallManager.callType == Voip.SCREEN && CallManager.callState == Voip.CONNECTED
             text: qsTr("You are screen sharing")
             font.pointSize: fontMetrics.font.pointSize * 1.1
             color: "#000000"
diff --git a/resources/qml/voip/CallDevices.qml b/resources/qml/voip/CallDevices.qml
index d4c554dc..46c8cde3 100644
--- a/resources/qml/voip/CallDevices.qml
+++ b/resources/qml/voip/CallDevices.qml
@@ -43,7 +43,7 @@ Popup {
             }
 
             RowLayout {
-                visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
+                visible: CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0
 
                 Image {
                     Layout.preferredWidth: 22
diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml
index 25aa0818..8a609c32 100644
--- a/resources/qml/voip/CallInvite.qml
+++ b/resources/qml/voip/CallInvite.qml
@@ -62,7 +62,7 @@ Popup {
             Layout.bottomMargin: callInv.height / 25
 
             Image {
-                property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg"
+                property string image: CallManager.callType == Voip.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg"
 
                 Layout.alignment: Qt.AlignCenter
                 Layout.preferredWidth: callInv.height / 10
@@ -72,7 +72,7 @@ Popup {
 
             Label {
                 Layout.alignment: Qt.AlignCenter
-                text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
+                text: CallManager.callType == Voip.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
                 font.pointSize: fontMetrics.font.pointSize * 2
                 color: palette.windowText
             }
@@ -106,7 +106,7 @@ Popup {
             }
 
             RowLayout {
-                visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
+                visible: CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0
                 Layout.alignment: Qt.AlignCenter
 
                 Image {
@@ -169,7 +169,7 @@ Popup {
             RoundButton {
                 id: acceptButton
 
-                property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg"
+                property string image: CallManager.callType == Voip.VIDEO ? ":/icons/icons/ui/video.svg" : ":/icons/icons/ui/place-call.svg"
 
                 implicitWidth: buttonLayout.buttonSize
                 implicitHeight: buttonLayout.buttonSize
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index 3c7426cc..d82bd143 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -57,12 +57,12 @@ Rectangle {
             Layout.leftMargin: 4
             Layout.preferredWidth: 24
             Layout.preferredHeight: 24
-            source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg"
+            source: CallManager.callType == Voip.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg"
         }
 
         Label {
             font.pointSize: fontMetrics.font.pointSize * 1.1
-            text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
+            text: CallManager.callType == Voip.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
             color: "#000000"
         }
 
@@ -88,7 +88,7 @@ Rectangle {
 
         Button {
             Layout.rightMargin: 4
-            icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg"
+            icon.source: CallManager.callType == Voip.VIDEO ? "qrc:/icons/icons/ui/video.svg" : "qrc:/icons/icons/ui/place-call.svg"
             text: qsTr("Accept")
             onClicked: {
                 if (CallManager.mics.length == 0) {
@@ -108,7 +108,7 @@ Rectangle {
             timelineRoot.destroyOnClose(dialog);
                     return ;
                 }
-                if (CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
+                if (CallManager.callType == Voip.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
                     var dialog = deviceError.createObject(timelineRoot, {
                         "errorString": qsTr("Unknown camera: %1").arg(Settings.camera),
                         "image": ":/icons/icons/ui/video.svg"
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 5d9387c3..2d4c46f8 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -81,7 +81,7 @@ Popup {
                 onClicked: {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
-                        CallManager.sendInvite(room.roomId, CallType.VOICE);
+                        CallManager.sendInvite(room.roomId, Voip.VOICE);
                         close();
                     }
                 }
@@ -95,7 +95,7 @@ Popup {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
                         Settings.camera = cameraCombo.currentText;
-                        CallManager.sendInvite(room.roomId, CallType.VIDEO);
+                        CallManager.sendInvite(room.roomId, Voip.VIDEO);
                         close();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index ce998299..2337f6d0 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -47,7 +47,7 @@ Popup {
 
             Layout.fillWidth: true
             model: CallManager.screenShareTypeList()
-            onCurrentIndexChanged: CallManager.setScreenShareType(currentIndex);
+            onCurrentIndexChanged: CallManager.setVoip(currentIndex);
           }
         }
 
@@ -63,7 +63,7 @@ Popup {
             }
 
             ComboBox {
-                visible: CallManager.screenShareType == ScreenShareType.X11
+                visible: CallManager.screenShareType == Voip.X11
                 id: windowCombo
 
                 Layout.fillWidth: true
@@ -71,7 +71,7 @@ Popup {
             }
 
             Button {
-                visible: CallManager.screenShareType == ScreenShareType.XDP
+                visible: CallManager.screenShareType == Voip.XDP
                 highlighted: !CallManager.screenShareReady
                 text: qsTr("Request screencast")
                 onClicked: {
@@ -165,7 +165,7 @@ Popup {
                     Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
                     Settings.screenShareHideCursor = hideCursorCheckBox.checked;
 
-                    CallManager.sendInvite(room.roomId, CallType.SCREEN, windowCombo.currentIndex);
+                    CallManager.sendInvite(room.roomId, Voip.SCREEN, windowCombo.currentIndex);
                     close();
                 }
             }
diff --git a/resources/res.qrc b/resources/res.qrc
index ed32b2bf..fb857d4a 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -92,107 +92,7 @@
     </qresource>
     <qresource prefix="/">
         <file>qtquickcontrols2.conf</file>
-        <file>qml/Root.qml</file>
-        <file>qml/ChatPage.qml</file>
-        <file>qml/CommunitiesList.qml</file>
-        <file>qml/RoomList.qml</file>
-        <file>qml/TimelineView.qml</file>
-        <file>qml/Avatar.qml</file>
-        <file>qml/Completer.qml</file>
-        <file>qml/EncryptionIndicator.qml</file>
-        <file>qml/ImageButton.qml</file>
-        <file>qml/ElidedLabel.qml</file>
-        <file>qml/MatrixText.qml</file>
-        <file>qml/MatrixTextField.qml</file>
-        <file>qml/ToggleButton.qml</file>
-        <file>qml/UploadBox.qml</file>
-        <file>qml/MessageInput.qml</file>
-        <file>qml/MessageView.qml</file>
-        <file>qml/PrivacyScreen.qml</file>
-        <file>qml/Reactions.qml</file>
-        <file>qml/ReplyPopup.qml</file>
-        <file>qml/StatusIndicator.qml</file>
-        <file>qml/TimelineRow.qml</file>
-        <file>qml/TopBar.qml</file>
-        <file>qml/QuickSwitcher.qml</file>
-        <file>qml/ForwardCompleter.qml</file>
-        <file>qml/SelfVerificationCheck.qml</file>
-        <file>qml/TypingIndicator.qml</file>
-        <file>qml/MessageInputWarning.qml</file>
-        <file>qml/components/AdaptiveLayout.qml</file>
-        <file>qml/components/AdaptiveLayoutElement.qml</file>
-        <file>qml/components/AvatarListTile.qml</file>
-        <file>qml/components/FlatButton.qml</file>
-        <file>qml/components/MainWindowDialog.qml</file>
-        <file>qml/components/NhekoTabButton.qml</file>
-        <file>qml/components/NotificationBubble.qml</file>
-        <file>qml/components/ReorderableListview.qml</file>
-        <file>qml/components/SpaceMenuLevel.qml</file>
-        <file>qml/components/TextButton.qml</file>
-        <file>qml/components/UserListRow.qml</file>
-        <file>qml/delegates/Encrypted.qml</file>
-        <file>qml/delegates/FileMessage.qml</file>
-        <file>qml/delegates/ImageMessage.qml</file>
-        <file>qml/delegates/MessageDelegate.qml</file>
-        <file>qml/delegates/NoticeMessage.qml</file>
-        <file>qml/delegates/Pill.qml</file>
-        <file>qml/delegates/Placeholder.qml</file>
-        <file>qml/delegates/PlayableMediaMessage.qml</file>
-        <file>qml/delegates/Redacted.qml</file>
-        <file>qml/delegates/Reply.qml</file>
-        <file>qml/delegates/TextMessage.qml</file>
-        <file>qml/device-verification/DeviceVerification.qml</file>
-        <file>qml/device-verification/DigitVerification.qml</file>
-        <file>qml/device-verification/EmojiVerification.qml</file>
-        <file>qml/device-verification/Failed.qml</file>
-        <file>qml/device-verification/NewVerificationRequest.qml</file>
-        <file>qml/device-verification/Success.qml</file>
-        <file>qml/device-verification/Waiting.qml</file>
-        <file>qml/dialogs/AliasEditor.qml</file>
-        <file>qml/dialogs/ConfirmJoinRoomDialog.qml</file>
-        <file>qml/dialogs/CreateDirect.qml</file>
-        <file>qml/dialogs/CreateRoom.qml</file>
-        <file>qml/dialogs/HiddenEventsDialog.qml</file>
-        <file>qml/dialogs/ImageOverlay.qml</file>
-        <file>qml/dialogs/ImagePackEditorDialog.qml</file>
-        <file>qml/dialogs/ImagePackSettingsDialog.qml</file>
-        <file>qml/dialogs/InputDialog.qml</file>
-        <file>qml/dialogs/InviteDialog.qml</file>
-        <file>qml/dialogs/JoinRoomDialog.qml</file>
-        <file>qml/dialogs/LeaveRoomDialog.qml</file>
-        <file>qml/dialogs/LogoutDialog.qml</file>
-        <file>qml/dialogs/PhoneNumberInputDialog.qml</file>
-        <file>qml/dialogs/PowerLevelEditor.qml</file>
-        <file>qml/dialogs/PowerLevelSpacesApplyDialog.qml</file>
-        <file>qml/dialogs/RawMessageDialog.qml</file>
-        <file>qml/dialogs/ReadReceipts.qml</file>
-        <file>qml/dialogs/RoomDirectory.qml</file>
-        <file>qml/dialogs/RoomMembers.qml</file>
-        <file>qml/dialogs/AllowedRoomsSettingsDialog.qml</file>
-        <file>qml/dialogs/RoomSettings.qml</file>
-        <file>qml/dialogs/UserProfile.qml</file>
-        <file>qml/emoji/StickerPicker.qml</file>
-        <file>qml/pages/LoginPage.qml</file>
-        <file>qml/pages/RegisterPage.qml</file>
-        <file>qml/pages/UserSettingsPage.qml</file>
-        <file>qml/pages/WelcomePage.qml</file>
-        <file>qml/ui/NhekoSlider.qml</file>
-        <file>qml/ui/Ripple.qml</file>
-        <file>qml/ui/Snackbar.qml</file>
-        <file>qml/ui/Spinner.qml</file>
-        <file>qml/ui/animations/BlinkAnimation.qml</file>
-        <file>qml/ui/media/MediaControls.qml</file>
-        <file>qml/voip/ActiveCallBar.qml</file>
-        <file>qml/voip/CallDevices.qml</file>
-        <file>qml/voip/CallInvite.qml</file>
-        <file>qml/voip/CallInviteBar.qml</file>
-        <file>qml/voip/DeviceError.qml</file>
-        <file>qml/voip/PlaceCall.qml</file>
-        <file>qml/voip/ScreenShare.qml</file>
-        <file>qml/voip/VideoCall.qml</file>
         <file>confettiparticle.svg</file>
-        <file>qml/delegates/EncryptionEnabled.qml</file>
-        <file>qml/ui/TimelineEffects.qml</file>
     </qresource>
     <qresource prefix="/media">
         <file>media/ring.ogg</file>
diff --git a/src/AliasEditModel.h b/src/AliasEditModel.h
index 2263659b..04de5016 100644
--- a/src/AliasEditModel.h
+++ b/src/AliasEditModel.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QAbstractListModel>
+#include <QQmlEngine>
 #include <QVector>
 
 #include <mtx/events/canonical_alias.hpp>
@@ -29,6 +30,9 @@ signals:
 class AliasEditingModel final : public QAbstractListModel
 {
     Q_OBJECT
+    QML_ELEMENT
+    QML_UNCREATABLE("Please use editAliases to create the models")
+
     Q_PROPERTY(bool canAdvertize READ canAdvertize CONSTANT)
 
 public:
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 96fc35ec..2a5b895f 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 
 #include <map>
 #include <mutex>
@@ -16,6 +17,8 @@
 
 namespace crypto {
 Q_NAMESPACE
+QML_NAMED_ELEMENT(Crypto)
+
 //! How much a participant is trusted.
 enum Trust
 {
diff --git a/src/Clipboard.h b/src/Clipboard.h
index bad9fd10..8bf89c22 100644
--- a/src/Clipboard.h
+++ b/src/Clipboard.h
@@ -5,11 +5,15 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 #include <QString>
 
 class Clipboard final : public QObject
 {
     Q_OBJECT
+    QML_ELEMENT
+    QML_SINGLETON
+
     Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
 
 public:
diff --git a/src/LoginPage.h b/src/LoginPage.h
index f20ba0c6..e3d0d2e0 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -5,13 +5,10 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 #include <QVariantList>
 
-namespace mtx {
-namespace responses {
-struct Login;
-}
-}
+#include <mtx/responses/login.hpp>
 
 struct SSOProvider
 {
@@ -33,6 +30,7 @@ public:
 class LoginPage : public QObject
 {
     Q_OBJECT
+    QML_ELEMENT
 
     Q_PROPERTY(QString mxid READ mxid WRITE setMxid NOTIFY matrixIdChanged)
     Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 51b23e0f..d06171de 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -25,21 +25,15 @@
 #include "InviteesModel.h"
 #include "JdenticonProvider.h"
 #include "Logging.h"
-#include "LoginPage.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "MemberList.h"
 #include "MxcImageProvider.h"
 #include "PowerlevelsEditModels.h"
-#include "ReadReceiptsModel.h"
-#include "RegisterPage.h"
-#include "RoomDirectoryModel.h"
-#include "RoomsModel.h"
 #include "SingleImagePackModel.h"
 #include "TrayIcon.h"
 #include "UserDirectoryModel.h"
 #include "UserSettingsPage.h"
-#include "UsersModel.h"
 #include "Utils.h"
 #include "dock/Dock.h"
 #include "emoji/Provider.h"
@@ -48,12 +42,6 @@
 #include "timeline/DelegateChooser.h"
 #include "timeline/TimelineFilter.h"
 #include "timeline/TimelineViewManager.h"
-#include "ui/HiddenEvents.h"
-#include "ui/MxcAnimatedImage.h"
-#include "ui/MxcMediaProxy.h"
-#include "ui/NhekoCursorShape.h"
-#include "ui/NhekoDropArea.h"
-#include "ui/NhekoEventObserver.h"
 #include "ui/NhekoGlobalObject.h"
 #include "ui/RoomSummary.h"
 #include "ui/UIA.h"
@@ -83,7 +71,7 @@ MainWindow::MainWindow(QWindow *parent)
     registerQmlTypes();
 
     setColor(Theme::paletteFromTheme(userSettings_->theme()).window().color());
-    setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml")));
+    setSource(QUrl(QStringLiteral("qrc:///resources/qml/Root.qml")));
 
     trayIcon_ = new TrayIcon(QStringLiteral(":/logos/nheko.svg"), this);
 
@@ -132,156 +120,57 @@ MainWindow::MainWindow(QWindow *parent)
 void
 MainWindow::registerQmlTypes()
 {
-    qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "MtxEvent",
-                                     QStringLiteral("Can't instantiate enum!"));
-    qmlRegisterUncreatableMetaObject(
-      olm::staticMetaObject, "im.nheko", 1, 0, "Olm", QStringLiteral("Can't instantiate enum!"));
-    qmlRegisterUncreatableMetaObject(crypto::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "Crypto",
-                                     QStringLiteral("Can't instantiate enum!"));
-    qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "VerificationStatus",
-                                     QStringLiteral("Can't instantiate enum!"));
-
-    qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
-    qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
-    qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
-    qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
-    qmlRegisterType<NhekoEventObserver>("im.nheko", 1, 0, "EventObserver");
-    qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
-    qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
-    qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
-    qmlRegisterType<UserDirectoryModel>("im.nheko", 1, 0, "UserDirectoryModel");
-    qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
-    qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
-    qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
-    qmlRegisterType<TimelineFilter>("im.nheko", 1, 0, "TimelineFilter");
-    qmlRegisterUncreatableType<RoomSummary>(
-      "im.nheko",
-      1,
-      0,
-      "RoomSummary",
-      QStringLiteral("Please use joinRoom to create a room summary."));
-    qmlRegisterUncreatableType<AliasEditingModel>(
-      "im.nheko",
-      1,
-      0,
-      "AliasEditingModel",
-      QStringLiteral("Please use editAliases to create the models"));
-
-    qmlRegisterUncreatableType<PowerlevelEditingModels>(
-      "im.nheko",
-      1,
-      0,
-      "PowerlevelEditingModels",
-      QStringLiteral("Please use editPowerlevels to create the models"));
-    qmlRegisterUncreatableType<DeviceVerificationFlow>(
-      "im.nheko",
-      1,
-      0,
-      "DeviceVerificationFlow",
-      QStringLiteral("Can't create verification flow from QML!"));
-    qmlRegisterUncreatableType<UserProfile>(
-      "im.nheko",
-      1,
-      0,
-      "UserProfileModel",
-      QStringLiteral("UserProfile needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<MemberList>(
-      "im.nheko",
-      1,
-      0,
-      "MemberList",
-      QStringLiteral("MemberList needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<RoomSettings>(
-      "im.nheko",
-      1,
-      0,
-      "RoomSettingsModel",
-      QStringLiteral("Room Settings needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<TimelineModel>(
-      "im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<ImagePackListModel>(
-      "im.nheko",
-      1,
-      0,
-      "ImagePackListModel",
-      QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<SingleImagePackModel>(
-      "im.nheko",
-      1,
-      0,
-      "SingleImagePackModel",
-      QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<InviteesModel>(
-      "im.nheko",
-      1,
-      0,
-      "InviteesModel",
-      QStringLiteral("InviteesModel needs to be instantiated on the C++ side"));
-    qmlRegisterUncreatableType<ReadReceiptsProxy>(
-      "im.nheko",
-      1,
-      0,
-      "ReadReceiptsProxy",
-      QStringLiteral("ReadReceiptsProxy needs to be instantiated on the C++ side"));
-
-    qmlRegisterSingletonType<Clipboard>(
-      "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          return new Clipboard();
-      });
-    qmlRegisterSingletonType<Nheko>(
-      "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          return new Nheko();
-      });
-    qmlRegisterSingletonType<UserSettingsModel>(
-      "im.nheko", 1, 0, "UserSettingsModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          return new UserSettingsModel();
-      });
-
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "Settings", userSettings_.data());
-
-    qmlRegisterUncreatableType<FilteredCommunitiesModel>(
-      "im.nheko",
-      1,
-      0,
-      "FilteredCommunitiesModel",
-      QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
-
-    qmlRegisterUncreatableType<MediaUpload>(
-      "im.nheko", 1, 0, "MediaUpload", QStringLiteral("MediaUploads can not be created in Qml"));
-    qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
-                                     "im.nheko.EmojiModel",
-                                     1,
-                                     0,
-                                     "EmojiCategory",
-                                     QStringLiteral("Error: Only enums"));
-
-    qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
-
-    qmlRegisterSingletonType<SelfVerificationStatus>(
-      "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = new SelfVerificationStatus();
-          QObject::connect(ChatPage::instance(),
-                           &ChatPage::initializeEmptyViews,
-                           ptr,
-                           &SelfVerificationStatus::invalidate);
-          return ptr;
-      });
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", this);
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
-    qmlRegisterSingletonInstance(
-      "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
+    // qmlRegisterUncreatableType<DeviceVerificationFlow>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "DeviceVerificationFlow",
+    //   QStringLiteral("Can't create verification flow from QML!"));
+    // qmlRegisterUncreatableType<UserProfile>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "UserProfileModel",
+    //   QStringLiteral("UserProfile needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<MemberList>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "MemberList",
+    //   QStringLiteral("MemberList needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<RoomSettings>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "RoomSettingsModel",
+    //   QStringLiteral("Room Settings needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<TimelineModel>(
+    //   "im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<ImagePackListModel>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "ImagePackListModel",
+    //   QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<SingleImagePackModel>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "SingleImagePackModel",
+    //   QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side"));
+    // qmlRegisterUncreatableType<InviteesModel>(
+    //   "im.nheko",
+    //   1,
+    //   0,
+    //   "InviteesModel",
+    //   QStringLiteral("InviteesModel needs to be instantiated on the C++ side"));
+
+    // qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
+    //                                  "im.nheko.EmojiModel",
+    //                                  1,
+    //                                  0,
+    //                                  "EmojiCategory",
+    //                                  QStringLiteral("Error: Only enums"));
 
     imgProvider = new MxcImageProvider();
     engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider);
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 0a5f9433..20e81efc 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -50,14 +50,35 @@ public:
     bool eventFilter(QObject *obj, QEvent *event) override;
 };
 
-class MainWindow final : public QQuickView
+class MainWindow : public QQuickView
 {
     Q_OBJECT
+    QML_ELEMENT
+    QML_SINGLETON
 
 public:
-    explicit MainWindow(QWindow *parent = nullptr);
+    explicit MainWindow(QWindow *parent);
 
     static MainWindow *instance() { return instance_; }
+    static MainWindow *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
+    }
+
     void saveCurrentWindowSize();
 
     void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
diff --git a/src/PowerlevelsEditModels.h b/src/PowerlevelsEditModels.h
index fe9735d3..c9d262d8 100644
--- a/src/PowerlevelsEditModels.h
+++ b/src/PowerlevelsEditModels.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QAbstractListModel>
+#include <QQmlEngine>
 #include <QSortFilterProxyModel>
 
 #include <mtx/events/power_levels.hpp>
@@ -196,6 +197,9 @@ class PowerlevelEditingModels final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_UNCREATABLE("Please use editPowerlevels to create the models")
+
     Q_PROPERTY(PowerlevelsUserListModel *users READ users CONSTANT)
     Q_PROPERTY(PowerlevelsTypeListModel *types READ types CONSTANT)
     Q_PROPERTY(PowerlevelsSpacesListModel *spaces READ spaces CONSTANT)
diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h
index b870061a..56f67509 100644
--- a/src/ReadReceiptsModel.h
+++ b/src/ReadReceiptsModel.h
@@ -8,6 +8,7 @@
 #include <QAbstractListModel>
 #include <QDateTime>
 #include <QObject>
+#include <QQmlEngine>
 #include <QSortFilterProxyModel>
 #include <QString>
 
@@ -54,6 +55,9 @@ class ReadReceiptsProxy final : public QSortFilterProxyModel
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_UNCREATABLE("")
+
     Q_PROPERTY(QString eventId READ eventId CONSTANT)
     Q_PROPERTY(QString roomId READ roomId CONSTANT)
 
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 7c58b40c..dcf61489 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 #include <QString>
 
 #include <mtx/user_interactive.hpp>
@@ -13,6 +14,7 @@
 class RegisterPage : public QObject
 {
     Q_OBJECT
+    QML_ELEMENT
 
     Q_PROPERTY(QString error READ error NOTIFY errorChanged)
     Q_PROPERTY(QString hsError READ hsError NOTIFY hsErrorChanged)
diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h
index 8a367e2e..a5103112 100644
--- a/src/RoomDirectoryModel.h
+++ b/src/RoomDirectoryModel.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QAbstractListModel>
+#include <QQmlEngine>
 #include <QString>
 #include <string>
 #include <vector>
@@ -32,6 +33,7 @@ signals:
 class RoomDirectoryModel : public QAbstractListModel
 {
     Q_OBJECT
+    QML_ELEMENT
 
     Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged)
     Q_PROPERTY(
diff --git a/src/UserDirectoryModel.h b/src/UserDirectoryModel.h
index f0416ecf..ffa9ae93 100644
--- a/src/UserDirectoryModel.h
+++ b/src/UserDirectoryModel.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QAbstractListModel>
+#include <QQmlEngine>
 #include <QString>
 #include <string>
 #include <vector>
@@ -26,6 +27,7 @@ signals:
 class UserDirectoryModel : public QAbstractListModel
 {
     Q_OBJECT
+    QML_ELEMENT
 
     Q_PROPERTY(bool searchingUsers READ searchingUsers NOTIFY searchingUsersChanged)
 
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 657a362d..301a1b67 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -6,6 +6,7 @@
 
 #include <QAbstractListModel>
 #include <QProcessEnvironment>
+#include <QQmlEngine>
 #include <QSettings>
 #include <QSharedPointer>
 
@@ -23,6 +24,8 @@ class QVBoxLayout;
 class UserSettings final : public QObject
 {
     Q_OBJECT
+    QML_NAMED_ELEMENT(Settings)
+    QML_SINGLETON
 
     Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
     Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE setMessageHoverHighlight
@@ -131,6 +134,24 @@ class UserSettings final : public QObject
 public:
     static QSharedPointer<UserSettings> instance();
     static void initialize(std::optional<QString> profile);
+    static UserSettings *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance());
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance()->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance().get(), QJSEngine::CppOwnership);
+        return instance().get();
+    }
 
     QSettings *qsettings() { return &settings; }
 
@@ -431,9 +452,10 @@ private:
     static QSharedPointer<UserSettings> instance_;
 };
 
-class UserSettingsModel final : public QAbstractListModel
+class UserSettingsModel : public QAbstractListModel
 {
     Q_OBJECT
+    QML_ELEMENT
 
     enum Indices
     {
diff --git a/src/encryption/Olm.h b/src/encryption/Olm.h
index f0e51070..726b9590 100644
--- a/src/encryption/Olm.h
+++ b/src/encryption/Olm.h
@@ -9,10 +9,13 @@
 #include <mtx/events/encrypted.hpp>
 #include <mtxclient/crypto/client.hpp>
 
+#include <QQmlEngine>
+
 #include <CacheCryptoStructs.h>
 
 namespace olm {
 Q_NAMESPACE
+QML_NAMED_ELEMENT(Olm)
 
 enum DecryptionErrorCode
 {
diff --git a/src/encryption/SelfVerificationStatus.cpp b/src/encryption/SelfVerificationStatus.cpp
index 6fa737d4..d9d3d787 100644
--- a/src/encryption/SelfVerificationStatus.cpp
+++ b/src/encryption/SelfVerificationStatus.cpp
@@ -29,6 +29,11 @@ SelfVerificationStatus::SelfVerificationStatus(QObject *o)
                 Qt::UniqueConnection);
         cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()});
     });
+
+    connect(ChatPage::instance(),
+            &ChatPage::initializeEmptyViews,
+            this,
+            &SelfVerificationStatus::invalidate);
 }
 
 void
diff --git a/src/encryption/SelfVerificationStatus.h b/src/encryption/SelfVerificationStatus.h
index ea790c8b..c65fffd0 100644
--- a/src/encryption/SelfVerificationStatus.h
+++ b/src/encryption/SelfVerificationStatus.h
@@ -5,11 +5,15 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 
 class SelfVerificationStatus final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_SINGLETON
+
     Q_PROPERTY(Status status READ status NOTIFY statusChanged)
     Q_PROPERTY(bool hasSSSS READ hasSSSS NOTIFY hasSSSSChanged)
 
diff --git a/src/encryption/VerificationManager.cpp b/src/encryption/VerificationManager.cpp
index 802a8177..d1248755 100644
--- a/src/encryption/VerificationManager.cpp
+++ b/src/encryption/VerificationManager.cpp
@@ -15,6 +15,7 @@ VerificationManager::VerificationManager(TimelineViewManager *o)
   : QObject(o)
   , rooms_(o->rooms())
 {
+    instance_ = this;
 }
 
 static bool
diff --git a/src/encryption/VerificationManager.h b/src/encryption/VerificationManager.h
index 7b32bc98..cdc8af30 100644
--- a/src/encryption/VerificationManager.h
+++ b/src/encryption/VerificationManager.h
@@ -6,6 +6,7 @@
 
 #include <QHash>
 #include <QObject>
+#include <QQmlEngine>
 #include <QSharedPointer>
 
 #include <mtx/events.hpp>
@@ -21,8 +22,30 @@ class VerificationManager final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_SINGLETON
+
 public:
-    VerificationManager(TimelineViewManager *o = nullptr);
+    VerificationManager(TimelineViewManager *o);
+
+    static VerificationManager *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
+    }
 
     Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
     void verifyUser(QString userid);
@@ -45,4 +68,6 @@ private:
     QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;
     bool isInitialSync_ = false;
     RoomlistModel *rooms_;
+
+    inline static VerificationManager *instance_ = nullptr;
 };
diff --git a/src/main.cpp b/src/main.cpp
index da67ca43..07397d62 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -346,7 +346,7 @@ main(int argc, char *argv[])
                            QStringLiteral(":/translations")))
         app.installTranslator(&appTranslator);
 
-    MainWindow w;
+    MainWindow w(nullptr);
     // QQuickView w;
 
     // Move the MainWindow to the center
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index b04fd7a9..3c09d747 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -22,6 +22,7 @@ CommunitiesModel::CommunitiesModel(QObject *parent)
   , hiddenTagIds_{UserSettings::instance()->hiddenTags()}
   , mutedTagIds_{UserSettings::instance()->mutedTags()}
 {
+    instance_ = this;
 }
 
 QHash<int, QByteArray>
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index a90fa6a2..d0841f4b 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -6,6 +6,7 @@
 
 #include <QAbstractListModel>
 #include <QHash>
+#include <QQmlEngine>
 #include <QSortFilterProxyModel>
 #include <QString>
 #include <QStringList>
@@ -21,6 +22,8 @@ class CommunitiesModel;
 class FilteredCommunitiesModel final : public QSortFilterProxyModel
 {
     Q_OBJECT
+    QML_ELEMENT
+    QML_UNCREATABLE("Use Communities.filtered() to create a FilteredCommunitiesModel")
 
 public:
     explicit FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent = nullptr);
@@ -73,6 +76,9 @@ public:
 class CommunitiesModel final : public QAbstractListModel
 {
     Q_OBJECT
+    QML_NAMED_ELEMENT(Communities)
+    QML_SINGLETON
+
     Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY
                  currentTagIdChanged RESET resetCurrentTagId)
     Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged)
@@ -149,6 +155,26 @@ public:
     };
 
     CommunitiesModel(QObject *parent = nullptr);
+
+    static CommunitiesModel *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
+    }
+
     QHash<int, QByteArray> roleNames() const override;
     int rowCount(const QModelIndex &parent = QModelIndex()) const override
     {
@@ -221,4 +247,6 @@ private:
     mtx::responses::UnreadNotifications dmUnreads{};
 
     friend class FilteredCommunitiesModel;
+
+    inline static CommunitiesModel *instance_ = nullptr;
 };
diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h
index c27f2c43..ac227382 100644
--- a/src/timeline/DelegateChooser.h
+++ b/src/timeline/DelegateChooser.h
@@ -19,6 +19,7 @@ class QQmlAdaptorModel;
 class DelegateChoice : public QObject
 {
     Q_OBJECT
+    QML_ELEMENT
     Q_CLASSINFO("DefaultProperty", "delegate")
 
 public:
@@ -45,6 +46,7 @@ private:
 class DelegateChooser : public QQuickItem
 {
     Q_OBJECT
+    QML_ELEMENT
     Q_CLASSINFO("DefaultProperty", "choices")
 
 public:
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index f03e6019..3cd65524 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -7,11 +7,13 @@
 #include <QIODevice>
 #include <QImage>
 #include <QObject>
+#include <QQmlEngine>
 #include <QSize>
 #include <QStringList>
 #include <QTimer>
 #include <QUrl>
 #include <QVariantList>
+
 #include <deque>
 #include <memory>
 
@@ -43,6 +45,10 @@ enum class MarkdownOverride
 class MediaUpload final : public QObject
 {
     Q_OBJECT
+
+    QML_ELEMENT
+    QML_UNCREATABLE("")
+
     Q_PROPERTY(int mediaType READ type NOTIFY mediaTypeChanged)
     // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646
     Q_PROPERTY(QUrl thumbnail READ thumbnailDataUrl NOTIFY thumbnailChanged)
diff --git a/src/timeline/PresenceEmitter.h b/src/timeline/PresenceEmitter.h
index e89fb316..09ad1301 100644
--- a/src/timeline/PresenceEmitter.h
+++ b/src/timeline/PresenceEmitter.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 
 #include <vector>
 
@@ -15,10 +16,33 @@ class PresenceEmitter final : public QObject
 {
     Q_OBJECT
 
+    QML_NAMED_ELEMENT(Presence)
+    QML_SINGLETON
+
 public:
     PresenceEmitter(QObject *p = nullptr)
       : QObject(p)
     {
+        instance_ = this;
+    }
+
+    static PresenceEmitter *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
     }
 
     void sync(const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presences);
@@ -28,4 +52,7 @@ public:
 
 signals:
     void presenceChanged(QString userid);
+
+private:
+    inline static PresenceEmitter *instance_ = nullptr;
 };
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index ec41cc12..8d8d2977 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -909,6 +909,8 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
 {
+    instance_ = this;
+
     this->sortByImportance = UserSettings::instance()->sortByImportance();
     this->sortByAlphabet   = UserSettings::instance()->sortByAlphabet();
     setSourceModel(model);
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index c06ab67d..34bf3f9a 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -167,12 +167,36 @@ private:
 class FilteredRoomlistModel final : public QSortFilterProxyModel
 {
     Q_OBJECT
+
+    QML_NAMED_ELEMENT(Rooms)
+    QML_SINGLETON
+
     Q_PROPERTY(
       TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom)
     Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged
                  RESET resetCurrentRoom)
 public:
     FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
+
+    static FilteredRoomlistModel *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
+    }
+
     bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
     bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
 
@@ -249,4 +273,6 @@ private:
     FilterBy filterType = FilterBy::Nothing;
     QStringList hiddenTags, hiddenSpaces;
     bool hideDMs = false;
+
+    inline static FilteredRoomlistModel *instance_ = nullptr;
 };
diff --git a/src/timeline/TimelineFilter.h b/src/timeline/TimelineFilter.h
index 1c92c89a..658a8c57 100644
--- a/src/timeline/TimelineFilter.h
+++ b/src/timeline/TimelineFilter.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <QQmlEngine>
 #include <QSortFilterProxyModel>
 #include <QString>
 
@@ -14,6 +15,7 @@
 class TimelineFilter : public QSortFilterProxyModel
 {
     Q_OBJECT
+    QML_ELEMENT
 
     Q_PROPERTY(QString filterByThread READ filterByThread WRITE setThreadId NOTIFY threadIdChanged)
     Q_PROPERTY(QString filterByContent READ filterByContent WRITE setContentFilter NOTIFY
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index a232b4ee..fd1a4396 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -11,6 +11,7 @@
 #include <QTimer>
 #include <QVariant>
 
+#include <mtx/responses/common.hpp>
 #include <mtxclient/http/errors.hpp>
 
 #include "CacheCryptoStructs.h"
@@ -36,6 +37,7 @@ struct RelatedInfo;
 
 namespace qml_mtx_events {
 Q_NAMESPACE
+QML_NAMED_ELEMENT(MtxEvent)
 
 enum EventType
 {
@@ -193,6 +195,9 @@ class TimelineViewManager;
 class TimelineModel final : public QAbstractListModel
 {
     Q_OBJECT
+    QML_NAMED_ELEMENT(Room)
+    QML_UNCREATABLE("")
+
     Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
     Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
                  typingUsersChanged)
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index a3b91ce7..2f6553e5 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -96,29 +96,21 @@ TimelineViewManager::userColor(QString id, QColor background)
 TimelineViewManager::TimelineViewManager(CallManager *, ChatPage *parent)
   : QObject(parent)
   , rooms_(new RoomlistModel(this))
+  , frooms_(new FilteredRoomlistModel(this->rooms_))
   , communities_(new CommunitiesModel(this))
   , verificationManager_(new VerificationManager(this))
   , presenceEmitter(new PresenceEmitter(this))
 {
-    static auto self = this;
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
-    qmlRegisterSingletonType<RoomlistModel>(
-      "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-          auto ptr = new FilteredRoomlistModel(self->rooms_);
-
-          connect(self->communities_,
-                  &CommunitiesModel::currentTagIdChanged,
-                  ptr,
-                  &FilteredRoomlistModel::updateFilterTag);
-          connect(self->communities_,
-                  &CommunitiesModel::hiddenTagsChanged,
-                  ptr,
-                  &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
-          return ptr;
-      });
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_);
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter);
+    instance_ = this;
+
+    connect(this->communities_,
+            &CommunitiesModel::currentTagIdChanged,
+            frooms_,
+            &FilteredRoomlistModel::updateFilterTag);
+    connect(this->communities_,
+            &CommunitiesModel::hiddenTagsChanged,
+            frooms_,
+            &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
 
     updateColorPalette();
 
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 303e2af2..a4bc6c41 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -34,6 +34,9 @@ class TimelineViewManager final : public QObject
 {
     Q_OBJECT
 
+    QML_NAMED_ELEMENT(TimelineManager)
+    QML_SINGLETON
+
     Q_PROPERTY(
       bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
     Q_PROPERTY(bool isConnected READ isConnected NOTIFY isConnectedChanged)
@@ -41,6 +44,25 @@ class TimelineViewManager final : public QObject
 public:
     TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
 
+    static TimelineViewManager *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance_);
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance_->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance_, QJSEngine::CppOwnership);
+        return instance_;
+    }
+
     void sync(const mtx::responses::Sync &sync_);
 
     VerificationManager *verificationManager() { return verificationManager_; }
@@ -123,6 +145,7 @@ private:
     bool isConnected_   = true;
 
     RoomlistModel *rooms_          = nullptr;
+    FilteredRoomlistModel *frooms_ = nullptr;
     CommunitiesModel *communities_ = nullptr;
 
     // don't move this above the rooms_
@@ -130,4 +153,6 @@ private:
     PresenceEmitter *presenceEmitter          = nullptr;
 
     QHash<QPair<QString, quint64>, QColor> userColors;
+
+    inline static TimelineViewManager *instance_ = nullptr;
 };
diff --git a/src/ui/HiddenEvents.h b/src/ui/HiddenEvents.h
index bb68e0fa..4f0d23b4 100644
--- a/src/ui/HiddenEvents.h
+++ b/src/ui/HiddenEvents.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 #include <QString>
 #include <QVariantList>
 
@@ -13,6 +14,7 @@
 class HiddenEvents : public QObject
 {
     Q_OBJECT
+    QML_ELEMENT
     Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged REQUIRED)
     Q_PROPERTY(QVariantList hiddenEvents READ hiddenEvents NOTIFY hiddenEventsChanged)
 public:
diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h
index bc53e711..c9f89764 100644
--- a/src/ui/MxcAnimatedImage.h
+++ b/src/ui/MxcAnimatedImage.h
@@ -15,6 +15,7 @@
 class MxcAnimatedImage : public QQuickItem
 {
     Q_OBJECT
+    QML_ELEMENT
     Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
     Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
     Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged)
diff --git a/src/ui/MxcMediaProxy.h b/src/ui/MxcMediaProxy.h
index 5c2eac33..d245dcae 100644
--- a/src/ui/MxcMediaProxy.h
+++ b/src/ui/MxcMediaProxy.h
@@ -8,6 +8,7 @@
 #include <QMediaPlayer>
 #include <QObject>
 #include <QPointer>
+#include <QQuickItem>
 #include <QString>
 #include <QUrl>
 #include <QVideoSink>
@@ -21,6 +22,8 @@ class TimelineModel;
 class MxcMediaProxy : public QMediaPlayer
 {
     Q_OBJECT
+    QML_NAMED_ELEMENT(MxcMedia)
+
     Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
     Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
     Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged)
diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h
index 84d56fad..123852f9 100644
--- a/src/ui/NhekoCursorShape.h
+++ b/src/ui/NhekoCursorShape.h
@@ -12,7 +12,7 @@
 class NhekoCursorShape : public QQuickItem
 {
     Q_OBJECT
-
+    QML_ELEMENT
     Q_PROPERTY(
       Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY cursorShapeChanged)
 
diff --git a/src/ui/NhekoDropArea.h b/src/ui/NhekoDropArea.h
index 91116844..46a02da5 100644
--- a/src/ui/NhekoDropArea.h
+++ b/src/ui/NhekoDropArea.h
@@ -7,6 +7,7 @@
 class NhekoDropArea : public QQuickItem
 {
     Q_OBJECT
+    QML_ELEMENT
     Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged)
 public:
     NhekoDropArea(QQuickItem *parent = nullptr);
diff --git a/src/ui/NhekoEventObserver.cpp b/src/ui/NhekoEventObserver.cpp
deleted file mode 100644
index 713a0733..00000000
--- a/src/ui/NhekoEventObserver.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-FileCopyrightText: Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "NhekoEventObserver.h"
-
-#include <QMouseEvent>
-
-#include "Logging.h"
-
-NhekoEventObserver::NhekoEventObserver(QQuickItem *parent)
-  : QQuickItem(parent)
-{
-    setFiltersChildMouseEvents(true);
-}
-
-bool
-NhekoEventObserver::childMouseEventFilter(QQuickItem * /*item*/, QEvent *event)
-{
-    // nhlog::ui()->debug("Touched {}", item->metaObject()->className());
-
-    auto setTouched = [this](bool touched) {
-        if (touched != this->wasTouched_) {
-            this->wasTouched_ = touched;
-            emit wasTouchedChanged();
-        }
-    };
-
-    // see
-    // https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quicktemplates2/qquickscrollview.cpp?id=7f29e89c26ae2babc358b1c4e6f965af6ec759f4#n471
-    switch (event->type()) {
-    case QEvent::TouchBegin:
-    case QEvent::TouchEnd:
-        setTouched(true);
-        break;
-
-    case QEvent::MouseButtonPress:
-        if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized) {
-            setTouched(false);
-        }
-        break;
-
-    case QEvent::MouseMove:
-    case QEvent::MouseButtonRelease:
-        if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized)
-            setTouched(false);
-        break;
-
-    case QEvent::HoverEnter:
-    case QEvent::HoverMove:
-    case QEvent::Wheel:
-        setTouched(false);
-        break;
-
-    default:
-        break;
-    }
-
-    return false;
-}
diff --git a/src/ui/NhekoEventObserver.h b/src/ui/NhekoEventObserver.h
deleted file mode 100644
index 63739d4a..00000000
--- a/src/ui/NhekoEventObserver.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// SPDX-FileCopyrightText: Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QQuickItem>
-
-class NhekoEventObserver : public QQuickItem
-{
-    Q_OBJECT
-
-    Q_PROPERTY(bool wasTouched READ wasTouched NOTIFY wasTouchedChanged)
-
-public:
-    explicit NhekoEventObserver(QQuickItem *parent = 0);
-
-    bool childMouseEventFilter(QQuickItem *item, QEvent *event) override;
-
-private:
-    bool wasTouched() { return wasTouched_; }
-
-    bool wasTouched_ = false;
-
-signals:
-    void wasTouchedChanged();
-};
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index b7a7a637..91210c54 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -7,6 +7,7 @@
 #include <QFontDatabase>
 #include <QObject>
 #include <QPalette>
+#include <QQmlEngine>
 #include <QWindow>
 
 #include "AliasEditModel.h"
@@ -19,6 +20,9 @@ class Nheko final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_SINGLETON
+
     Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
     Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
     Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged)
diff --git a/src/ui/RoomSummary.h b/src/ui/RoomSummary.h
index c02ea5d5..8225f0ae 100644
--- a/src/ui/RoomSummary.h
+++ b/src/ui/RoomSummary.h
@@ -7,6 +7,7 @@
 #include <optional>
 
 #include <QObject>
+#include <QQmlEngine>
 
 #include <mtx/responses/public_rooms.hpp>
 
@@ -25,6 +26,9 @@ class RoomSummary final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_UNCREATABLE("Please use joinRoom to create a room summary.")
+
     Q_PROPERTY(QString reason READ reason WRITE setReason NOTIFY reasonChanged)
 
     Q_PROPERTY(QString roomid READ roomid NOTIFY loaded)
diff --git a/src/ui/UIA.h b/src/ui/UIA.h
index 7d23d88e..414cb804 100644
--- a/src/ui/UIA.h
+++ b/src/ui/UIA.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <QObject>
+#include <QQmlEngine>
 
 #include <mtxclient/http/client.hpp>
 
@@ -12,10 +13,31 @@ class UIA final : public QObject
 {
     Q_OBJECT
 
+    QML_ELEMENT
+    QML_SINGLETON
+
     Q_PROPERTY(QString title READ title NOTIFY titleChanged)
 
 public:
     static UIA *instance();
+    static UIA *create(QQmlEngine *qmlEngine, QJSEngine *)
+    {
+        // The instance has to exist before it is used. We cannot replace it.
+        Q_ASSERT(instance());
+
+        // The engine has to have the same thread affinity as the singleton.
+        Q_ASSERT(qmlEngine->thread() == instance()->thread());
+
+        // There can only be one engine accessing the singleton.
+        static QJSEngine *s_engine = nullptr;
+        if (s_engine)
+            Q_ASSERT(qmlEngine == s_engine);
+        else
+            s_engine = qmlEngine;
+
+        QJSEngine::setObjectOwnership(instance(), QJSEngine::CppOwnership);
+        return instance();
+    }
 
     UIA(QObject *parent = nullptr)
       : QObject(parent)
diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
index a880f320..d8e06aa1 100644
--- a/src/ui/UserProfile.h
+++ b/src/ui/UserProfile.h
@@ -6,6 +6,7 @@
 
 #include <QAbstractListModel>
 #include <QObject>
+#include <QQmlEngine>
 #include <QString>
 #include <QVector>
 #include <mtx/responses.hpp>
@@ -16,6 +17,7 @@
 
 namespace verification {
 Q_NAMESPACE
+QML_NAMED_ELEMENT(VerificationStatus)
 
 enum Status
 {
diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp
index feb06835..5479ba31 100644
--- a/src/voip/CallManager.cpp
+++ b/src/voip/CallManager.cpp
@@ -54,6 +54,27 @@ std::vector<std::string>
 getTurnURIs(const mtx::responses::TurnServer &turnServer);
 }
 
+CallManager *
+CallManager::create(QQmlEngine *qmlEngine, QJSEngine *)
+{
+    // The instance has to exist before it is used. We cannot replace it.
+    auto instance = ChatPage::instance()->callManager();
+    Q_ASSERT(instance);
+
+    // The engine has to have the same thread affinity as the singleton.
+    Q_ASSERT(qmlEngine->thread() == instance->thread());
+
+    // There can only be one engine accessing the singleton.
+    static QJSEngine *s_engine = nullptr;
+    if (s_engine)
+        Q_ASSERT(qmlEngine == s_engine);
+    else
+        s_engine = qmlEngine;
+
+    QJSEngine::setObjectOwnership(instance, QJSEngine::CppOwnership);
+    return instance;
+}
+
 CallManager::CallManager(QObject *parent)
   : QObject(parent)
   , session_(WebRTCSession::instance())
diff --git a/src/voip/CallManager.h b/src/voip/CallManager.h
index bbc7a903..e84b79c9 100644
--- a/src/voip/CallManager.h
+++ b/src/voip/CallManager.h
@@ -9,6 +9,7 @@
 
 #include <QMediaPlayer>
 #include <QObject>
+#include <QQmlEngine>
 #include <QString>
 #include <QStringList>
 #include <QTimer>
@@ -29,6 +30,10 @@ class QUrl;
 class CallManager final : public QObject
 {
     Q_OBJECT
+
+    QML_ELEMENT
+    QML_SINGLETON
+
     Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
     Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
     Q_PROPERTY(bool isOnCallOnOtherDevice READ isOnCallOnOtherDevice NOTIFY newCallDeviceState)
@@ -49,6 +54,8 @@ class CallManager final : public QObject
 public:
     CallManager(QObject *);
 
+    static CallManager *create(QQmlEngine *qmlEngine, QJSEngine *);
+
     bool haveCallInvite() const { return haveCallInvite_; }
     bool isOnCall() const { return (session_.state() != webrtc::State::DISCONNECTED); }
     bool isOnCallOnOtherDevice() const { return (isOnCallOnOtherDevice_ != ""); }
diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp
index c40b39a4..ff459bf9 100644
--- a/src/voip/WebRTCSession.cpp
+++ b/src/voip/WebRTCSession.cpp
@@ -48,26 +48,26 @@ using webrtc::State;
 WebRTCSession::WebRTCSession()
   : devices_(CallDevices::instance())
 {
-    qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "CallType",
-                                     QStringLiteral("Can't instantiate enum"));
-
-    qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "ScreenShareType",
-                                     QStringLiteral("Can't instantiate enum"));
-
-    qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
-                                     "im.nheko",
-                                     1,
-                                     0,
-                                     "WebRTCState",
-                                     QStringLiteral("Can't instantiate enum"));
+    // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
+    //                                  "im.nheko",
+    //                                  1,
+    //                                  0,
+    //                                  "CallType",
+    //                                  QStringLiteral("Can't instantiate enum"));
+
+    // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
+    //                                  "im.nheko",
+    //                                  1,
+    //                                  0,
+    //                                  "ScreenShareType",
+    //                                  QStringLiteral("Can't instantiate enum"));
+
+    // qmlRegisterUncreatableMetaObject(webrtc::staticMetaObject,
+    //                                  "im.nheko",
+    //                                  1,
+    //                                  0,
+    //                                  "WebRTCState",
+    //                                  QStringLiteral("Can't instantiate enum"));
 
     connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
     init();
diff --git a/src/voip/WebRTCSession.h b/src/voip/WebRTCSession.h
index 82753372..3357bff7 100644
--- a/src/voip/WebRTCSession.h
+++ b/src/voip/WebRTCSession.h
@@ -8,6 +8,7 @@
 #include <vector>
 
 #include <QObject>
+#include <QQmlEngine>
 
 #include "mtx/events/voip.hpp"
 
@@ -17,6 +18,7 @@ class QQuickItem;
 
 namespace webrtc {
 Q_NAMESPACE
+QML_NAMED_ELEMENT(Voip)
 
 enum class CallType
 {