diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4581df94a7fb6f9e20482c032b46104e64e8c3fe..7dd4d2450cb244fe4723951fa0c68e63c4d8dd6e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -331,24 +331,13 @@ set(SRC_FILES
 	src/timeline/RoomlistModel.cpp
 
 	# UI components
-	src/ui/DropShadow.cpp
-	src/ui/FlatButton.cpp
-	src/ui/Label.cpp
-	src/ui/LoadingIndicator.cpp
 	src/ui/MxcAnimatedImage.cpp
 	src/ui/MxcMediaProxy.cpp
 	src/ui/NhekoCursorShape.cpp
 	src/ui/NhekoDropArea.cpp
 	src/ui/NhekoGlobalObject.cpp
-	src/ui/OverlayModal.cpp
-	src/ui/OverlayWidget.cpp
-	src/ui/RaisedButton.cpp
-	src/ui/Ripple.cpp
-	src/ui/RippleOverlay.cpp
 	src/ui/RoomSettings.cpp
-	src/ui/SnackBar.cpp
 	src/ui/TextField.cpp
-	src/ui/TextLabel.cpp
 	src/ui/Theme.cpp
 	src/ui/ThemeManager.cpp
 	src/ui/ToggleButton.cpp
@@ -395,7 +384,6 @@ set(SRC_FILES
 	src/RoomDirectoryModel.cpp
 	src/RoomsModel.cpp
 	src/Utils.cpp
-	src/WelcomePage.cpp
 	src/main.cpp
 
 	third_party/blurhash/blurhash.cpp
@@ -414,7 +402,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        6a7eaa5006b1a18e132be7655e490d9819158dca
+		GIT_TAG        9781553b0186f2db9036d2abbde83c28828eb2b9
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -538,23 +526,13 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/timeline/RoomlistModel.h
 
 	# UI components
-	src/ui/FlatButton.h
-	src/ui/Label.h
-	src/ui/LoadingIndicator.h
 	src/ui/MxcAnimatedImage.h
 	src/ui/MxcMediaProxy.h
-	src/ui/Menu.h
 	src/ui/NhekoCursorShape.h
 	src/ui/NhekoDropArea.h
 	src/ui/NhekoGlobalObject.h
-	src/ui/OverlayWidget.h
-	src/ui/RaisedButton.h
-	src/ui/Ripple.h
-	src/ui/RippleOverlay.h
 	src/ui/RoomSettings.h
-	src/ui/SnackBar.h
 	src/ui/TextField.h
-	src/ui/TextLabel.h
 	src/ui/Theme.h
 	src/ui/ThemeManager.h
 	src/ui/ToggleButton.h
@@ -596,7 +574,6 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/UsersModel.h
 	src/RoomDirectoryModel.h
 	src/RoomsModel.h
-	src/WelcomePage.h
 	src/ReadReceiptsModel.h
 )
 
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index 946ca7b6bfcd1b5bad7c3aa978fa2236f6802171..700b6d02ad69fa946e2050d03113f1284c1d688f 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -191,7 +191,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: 6a7eaa5006b1a18e132be7655e490d9819158dca
+      - commit: 9781553b0186f2db9036d2abbde83c28828eb2b9
         #tag: v0.6.1
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index 29a043554cd9de205c6ea9613ab7a5f4d047529d..c499874b1fb77a27e2ee3c4bb3983fb7039e1075 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -319,7 +319,7 @@
     <message>
         <location line="+66"/>
         <source>Failed to kick %1 from %2: %3</source>
-        <translation>Kontte %1 nicht aus %2 entfernen: %3</translation>
+        <translation>Konnte %1 nicht aus %2 entfernen: %3</translation>
     </message>
 </context>
 <context>
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 33db1b1a71b97a0ff0a62b42ae535986c1323375..e3aa3e48a45e01495b324f9eb736374cb876f81a 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -97,6 +97,7 @@ Rectangle {
 
                 implicitHeight: chatPage.height
                 collapsed: parent.collapsed
+                anchors.fill: parent
             }
 
             Binding {
diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index af077124d357fa622f17807f0316a1b00b64b60e..05f2c82f5f1c2ec4a356ef7852f22e9d868be503 100644
--- a/resources/qml/MatrixTextField.qml
+++ b/resources/qml/MatrixTextField.qml
@@ -8,29 +8,139 @@ import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
 import im.nheko 1.0
 
-TextField {
-    id: input
 
-    property alias backgroundColor: backgroundRect.color
+ColumnLayout {
+    id: c
+    property color backgroundColor: Nheko.colors.base
+    property alias color: labelC.color
+    property alias textPadding: input.padding
+    property alias text: input.text
+    property alias label: labelC.text
+    property alias placeholderText: input.placeholderText
+    property alias font: input.font
+    property alias echoMode: input.echoMode
+    property alias selectByMouse: input.selectByMouse
+
+    Timer {
+        id: timer
+        interval: 350
+        onTriggered: editingFinished()
+    }
+
+    onTextChanged: timer.restart()
+
+    signal textEdited
+    signal accepted
+    signal editingFinished
+
+    function forceActiveFocus() {
+        input.forceActiveFocus();
+    }
+
+    ToolTip.delay: Nheko.tooltipDelay
+    ToolTip.visible: hover.hovered
+
+    spacing: 0
+
+    Item {
+        Layout.fillWidth: true
+        Layout.preferredHeight: labelC.contentHeight
+        Layout.margins: input.padding
+        Layout.bottomMargin: Nheko.paddingSmall
+        visible: labelC.text
+
+        z: 1
+
+        Label {
+            id: labelC
+
+            y: contentHeight + input.padding + Nheko.paddingSmall
+            enabled: false
+
+            palette: Nheko.colors
+            color: Nheko.colors.text
+            font.pixelSize: input.font.pixelSize
+            font.weight: Font.DemiBold
+            font.letterSpacing: input.font.pixelSize * 0.02
+            width: parent.width
+
+            state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : ""
+
+            states: State {
+                name: "focused"
+
+                PropertyChanges {
+                    target: labelC
+                    y: 0
+                }
+
+                PropertyChanges {
+                    target: input
+                    opacity: 1
+                }
+
+            }
+
+            transitions: Transition {
+                from: ""
+                to: "focused"
+                reversible: true
+
+                NumberAnimation {
+                    target: labelC
+                    properties: "y"
+                    duration: 210
+                    easing.type: Easing.InCubic
+                    alwaysRunToEnd: true
+                }
 
-    palette: Nheko.colors
-    color: Nheko.colors.text
+                NumberAnimation {
+                    target: input
+                    properties: "opacity"
+                    duration: 210
+                    easing.type: Easing.InCubic
+                    alwaysRunToEnd: true
+                }
+
+            }
+        }
+    }
+
+    TextField {
+        id: input
+        Layout.fillWidth: true
+
+        palette: Nheko.colors
+        color: labelC.color
+        opacity: labelC.text ? 0 : 1
+
+        onTextEdited: c.textEdited()
+        onAccepted: c.accepted()
+        onEditingFinished: c.editingFinished()
+
+
+        background: Rectangle {
+            id: backgroundRect
+
+            color: labelC.text ? "transparent" : backgroundColor
+        }
+
+    }
 
     Rectangle {
         id: blueBar
 
-        anchors.top: parent.bottom
-        anchors.horizontalCenter: parent.horizontalCenter
+        Layout.fillWidth: true
+
         color: Nheko.colors.highlight
         height: 1
-        width: parent.width
 
         Rectangle {
             id: blackBar
 
-            anchors.verticalCenter: blueBar.verticalCenter
+            anchors.top: parent.top
             anchors.horizontalCenter: parent.horizontalCenter
-            height: parent.height + 1
+            height: parent.height*2
             width: 0
             color: Nheko.colors.text
 
@@ -50,11 +160,12 @@ TextField {
                 to: "focused"
                 reversible: true
 
+
                 NumberAnimation {
                     target: blackBar
                     properties: "width"
-                    duration: 500
-                    easing.type: Easing.InOutQuad
+                    duration: 310
+                    easing.type: Easing.InCubic
                     alwaysRunToEnd: true
                 }
 
@@ -64,10 +175,8 @@ TextField {
 
     }
 
-    background: Rectangle {
-        id: backgroundRect
-
-        color: Nheko.colors.base
+    HoverHandler {
+        id: hover
+        enabled: c.ToolTip.text
     }
-
 }
diff --git a/resources/qml/PrivacyScreen.qml b/resources/qml/PrivacyScreen.qml
index e6286bc62c8db9e4ab48b9a36473e53d592c8484..fb3818df39c6d21d2c60cfc10e473a8aa96d4eed 100644
--- a/resources/qml/PrivacyScreen.qml
+++ b/resources/qml/PrivacyScreen.qml
@@ -5,6 +5,7 @@
 
 import QtGraphicalEffects 1.0
 import QtQuick 2.12
+import QtQuick.Window 2.2
 import im.nheko 1.0
 
 Item {
@@ -15,7 +16,7 @@ Item {
 
     Connections {
         function onFocusChanged() {
-            if (TimelineManager.isWindowFocused) {
+            if (MainWindow.active) {
                 screenSaverTimer.stop();
                 screenSaver.state = "Invisible";
             } else {
@@ -32,7 +33,7 @@ Item {
         id: screenSaverTimer
 
         interval: screenTimeout * 1000
-        running: true
+        running: !MainWindow.active
         onTriggered: {
             screenSaver.state = "Visible";
         }
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index 174951a0e86c2a8dd9b4d7064f47c1c3877500cf..8747c47ddf123001ccce5bda2fa6f76a211a875d 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -11,7 +11,6 @@ Popup {
     id: quickSwitcher
 
     property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
-    property int textMargin: Math.round(textHeight / 8)
 
     background: null
     width: Math.round(parent.width / 2)
@@ -34,7 +33,6 @@ Popup {
 
         anchors.fill: parent
         font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
-        padding: textMargin
         color: Nheko.colors.text
         onTextEdited: {
             completerPopup.completer.searchString = text;
@@ -60,7 +58,7 @@ Popup {
         id: completerPopup
 
         x: roomTextInput.x
-        y: roomTextInput.y + quickSwitcher.textHeight + quickSwitcher.textMargin
+        y: roomTextInput.y + quickSwitcher.textHeight
         visible: roomTextInput.length > 0
         width: parent.width
         completerName: "room"
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 6e7b683f6877ccd98da9ee83eeea398e84cf9fe8..da2059500f1c7245c6f56ea6147130496c4f5a63 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -385,7 +385,7 @@ Page {
     header: ColumnLayout {
         spacing: 0
 
-        Rectangle {
+        Pane {
             id: userInfoPanel
 
             function openUserProfile() {
@@ -396,12 +396,15 @@ Page {
                 userProfile.show();
             }
 
-            color: Nheko.colors.window
+
             Layout.fillWidth: true
             Layout.alignment: Qt.AlignBottom
-            Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
+            //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
+            padding: Nheko.paddingMedium
             Layout.minimumHeight: 40
 
+            background: Rectangle {color: Nheko.colors.window}
+
             InputDialog {
                 id: statusDialog
 
@@ -442,14 +445,12 @@ Page {
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
-            RowLayout {
+            contentItem: RowLayout {
                 id: userInfoGrid
 
                 property var profile: Nheko.currentUser
 
                 spacing: Nheko.paddingMedium
-                anchors.fill: parent
-                anchors.margins: Nheko.paddingMedium
 
                 Avatar {
                     id: avatar
@@ -614,19 +615,17 @@ Page {
             Layout.fillWidth: true
         }
 
-        Rectangle {
-            color: Nheko.colors.window
+        Pane {
             Layout.fillWidth: true
             Layout.alignment: Qt.AlignBottom
-            Layout.preferredHeight: buttonRow.implicitHeight
             Layout.minimumHeight: 40
 
-            RowLayout {
-                id: buttonRow
+            horizontalPadding: Nheko.paddingMedium
+            verticalPadding: 0
 
-                anchors.left: parent.left
-                anchors.right: parent.right
-                anchors.margins: Nheko.paddingMedium
+            background: Rectangle {color: Nheko.colors.window}
+            contentItem: RowLayout {
+                id: buttonRow
 
                 ImageButton {
                     Layout.fillWidth: true
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index e4b164e45a42c4891700b23b4b419c3b6db74b3b..88d3e7c6f1dd5d58ed5df4cf4eb5f0b22618a9bd 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -9,6 +9,7 @@ import "./dialogs"
 import "./emoji"
 import "./pages"
 import "./voip"
+import "./ui"
 import Qt.labs.platform 1.1 as Platform
 import QtQuick 2.15
 import QtQuick.Controls 2.15
@@ -17,10 +18,12 @@ import QtQuick.Window 2.15
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
-Page {
+Pane {
     id: timelineRoot
 
     palette: Nheko.colors
+    background: null
+    padding: 0
 
     FontMetrics {
         id: fontMetrics
@@ -153,11 +156,15 @@ Page {
 
     }
 
+    Shortcut {
+        sequence: StandardKey.Quit
+        onActivated: Qt.quit()
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
             var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
-            TimelineManager.focusTimeline();
             quickSwitch.open();
         }
     }
@@ -165,7 +172,6 @@ Page {
     Shortcut {
         // Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit
         sequences: ["Alt+A", "Ctrl+Shift+A"]
-        context: Qt.ApplicationShortcut
         onActivated: Rooms.nextRoomWithActivity()
     }
 
@@ -366,9 +372,51 @@ Page {
         id: mainWindow
 
         anchors.fill: parent
-        initialItem: ChatPage {
-            //anchors.fill: parent
+        initialItem: welcomePage
+    }
+
+    Component {
+        id: welcomePage
+
+        WelcomePage {
+        }
+    }
+
+    Component {
+        id: chatPage
+
+        ChatPage {
+        }
+    }
+
+    Component {
+        id: loginPage
+
+        LoginPage {
+        }
+    }
+
+    Component {
+        id: registerPage
+
+        RegisterPage {
+        }
+    }
+
+    Snackbar { id: snackbar }
+
+    Connections {
+        function onSwitchToChatPage() {
+            mainWindow.replace(null, chatPage);
+        }
+        function onSwitchToLoginPage(error) {
+            mainWindow.replace(welcomePage, {}, loginPage, {"error": error}, StackView.PopTransition);
+        }
+        function onShowNotification(msg) {
+            snackbar.showNotification(msg);
+            console.log("New snack: " + msg);
         }
+        target: MainWindow
     }
 
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 1c0115e23c0b2237c23135078d9787b99fbac35c..c9a8d0d2e0b28af01bfb53c85836c2f344a91807 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -12,7 +12,7 @@ import im.nheko 1.0
 
 import "./delegates"
 
-Rectangle {
+Pane {
     id: topBar
 
     property bool showBackButton: false
@@ -28,7 +28,11 @@ Rectangle {
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
-    color: Nheko.colors.window
+
+    padding: 0
+    background: Rectangle {
+        color: Nheko.colors.window
+    }
 
     TapHandler {
         onSingleTapped: {
@@ -65,248 +69,250 @@ Rectangle {
         grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
     }
 
-    GridLayout {
-        id: topLayout
-
-        anchors.left: parent.left
-        anchors.right: parent.right
-        anchors.margins: Nheko.paddingMedium
-        anchors.verticalCenter: parent.verticalCenter
-        columnSpacing: Nheko.paddingSmall
-        rowSpacing: Nheko.paddingSmall
-
-        ImageButton {
-            id: backToRoomsButton
-
-            Layout.column: 0
-            Layout.row: 0
-            Layout.rowSpan: 2
-            Layout.alignment: Qt.AlignVCenter
-            Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
-            Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-            visible: showBackButton
-            image: ":/icons/icons/ui/angle-arrow-left.svg"
-            ToolTip.visible: hovered
-            ToolTip.text: qsTr("Back to room list")
-            onClicked: Rooms.resetCurrentRoom()
-        }
+    contentItem: Item {
+        GridLayout {
+            id: topLayout
+
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.margins: Nheko.paddingMedium
+            anchors.verticalCenter: parent.verticalCenter
+            columnSpacing: Nheko.paddingSmall
+            rowSpacing: Nheko.paddingSmall
+
+            ImageButton {
+                id: backToRoomsButton
+
+                Layout.column: 0
+                Layout.row: 0
+                Layout.rowSpan: 2
+                Layout.alignment: Qt.AlignVCenter
+                Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                visible: showBackButton
+                image: ":/icons/icons/ui/angle-arrow-left.svg"
+                ToolTip.visible: hovered
+                ToolTip.text: qsTr("Back to room list")
+                onClicked: Rooms.resetCurrentRoom()
+            }
 
-        Avatar {
-            Layout.column: 1
-            Layout.row: 0
-            Layout.rowSpan: 2
-            Layout.alignment: Qt.AlignVCenter
-            width: Nheko.avatarSize
-            height: Nheko.avatarSize
-            url: avatarUrl.replace("mxc://", "image://MxcImage/")
-            roomid: roomId
-            userid: isDirect ? directChatOtherUserId : ""
-            displayName: roomName
-            enabled: false
-        }
+            Avatar {
+                Layout.column: 1
+                Layout.row: 0
+                Layout.rowSpan: 2
+                Layout.alignment: Qt.AlignVCenter
+                width: Nheko.avatarSize
+                height: Nheko.avatarSize
+                url: avatarUrl.replace("mxc://", "image://MxcImage/")
+                roomid: roomId
+                userid: isDirect ? directChatOtherUserId : ""
+                displayName: roomName
+                enabled: false
+            }
 
-        Label {
-            Layout.fillWidth: true
-            Layout.column: 2
-            Layout.row: 0
-            color: Nheko.colors.text
-            font.pointSize: fontMetrics.font.pointSize * 1.1
-            text: roomName
-            maximumLineCount: 1
-            elide: Text.ElideRight
-            textFormat: Text.RichText
-        }
+            Label {
+                Layout.fillWidth: true
+                Layout.column: 2
+                Layout.row: 0
+                color: Nheko.colors.text
+                font.pointSize: fontMetrics.font.pointSize * 1.1
+                text: roomName
+                maximumLineCount: 1
+                elide: Text.ElideRight
+                textFormat: Text.RichText
+            }
 
-        MatrixText {
-            id: roomTopicC
-            Layout.fillWidth: true
-            Layout.column: 2
-            Layout.row: 1
-            Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
-            selectByMouse: false
-            enabled: false
-            clip: true
-            text: roomTopic
-        }
+            MatrixText {
+                id: roomTopicC
+                Layout.fillWidth: true
+                Layout.column: 2
+                Layout.row: 1
+                Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
+                selectByMouse: false
+                enabled: false
+                clip: true
+                text: roomTopic
+            }
 
-        EncryptionIndicator {
-            Layout.column: 3
-            Layout.row: 0
-            Layout.rowSpan: 2
-            Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
-            Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-            sourceSize.height: Layout.preferredHeight * Screen.devicePixelRatio
-            sourceSize.width: Layout.preferredWidth * Screen.devicePixelRatio
-            visible: isEncrypted
-            encrypted: isEncrypted
-            trust: trustlevel
-            ToolTip.text: {
-                if (!encrypted)
-                return qsTr("This room is not encrypted!");
-
-                switch (trust) {
-                    case Crypto.Verified:
-                    return qsTr("This room contains only verified devices.");
-                    case Crypto.TOFU:
-                    return qsTr("This room contains verified devices and devices which have never changed their master key.");
-                    default:
-                    return qsTr("This room contains unverified devices!");
+            EncryptionIndicator {
+                Layout.column: 3
+                Layout.row: 0
+                Layout.rowSpan: 2
+                Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                sourceSize.height: Layout.preferredHeight * Screen.devicePixelRatio
+                sourceSize.width: Layout.preferredWidth * Screen.devicePixelRatio
+                visible: isEncrypted
+                encrypted: isEncrypted
+                trust: trustlevel
+                ToolTip.text: {
+                    if (!encrypted)
+                    return qsTr("This room is not encrypted!");
+
+                    switch (trust) {
+                        case Crypto.Verified:
+                        return qsTr("This room contains only verified devices.");
+                        case Crypto.TOFU:
+                        return qsTr("This room contains verified devices and devices which have never changed their master key.");
+                        default:
+                        return qsTr("This room contains unverified devices!");
+                    }
                 }
             }
-        }
 
-        ImageButton {
-            id: pinButton
-
-            property bool pinsShown: !Settings.hiddenPins.includes(roomId)
-
-            visible: !!room && room.pinnedMessages.length > 0
-            Layout.column: 4
-            Layout.row: 0
-            Layout.rowSpan: 2
-            Layout.alignment: Qt.AlignVCenter
-            Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
-            Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-            image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
-            ToolTip.visible: hovered
-            ToolTip.text: qsTr("Show or hide pinned messages")
-            onClicked: {
-                var ps = Settings.hiddenPins;
-                if (pinsShown) {
-                    ps.push(roomId);
-                } else {
-                    const index = ps.indexOf(roomId);
-                    if (index > -1) {
-                        ps.splice(index, 1);
+            ImageButton {
+                id: pinButton
+
+                property bool pinsShown: !Settings.hiddenPins.includes(roomId)
+
+                visible: !!room && room.pinnedMessages.length > 0
+                Layout.column: 4
+                Layout.row: 0
+                Layout.rowSpan: 2
+                Layout.alignment: Qt.AlignVCenter
+                Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
+                ToolTip.visible: hovered
+                ToolTip.text: qsTr("Show or hide pinned messages")
+                onClicked: {
+                    var ps = Settings.hiddenPins;
+                    if (pinsShown) {
+                        ps.push(roomId);
+                    } else {
+                        const index = ps.indexOf(roomId);
+                        if (index > -1) {
+                            ps.splice(index, 1);
+                        }
                     }
+                    Settings.hiddenPins = ps;
                 }
-                Settings.hiddenPins = ps;
+
             }
 
-        }
+            ImageButton {
+                id: roomOptionsButton
+
+                visible: !!room
+                Layout.column: 5
+                Layout.row: 0
+                Layout.rowSpan: 2
+                Layout.alignment: Qt.AlignVCenter
+                Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                image: ":/icons/icons/ui/options.svg"
+                ToolTip.visible: hovered
+                ToolTip.text: qsTr("Room options")
+                onClicked: roomOptionsMenu.open(roomOptionsButton)
+
+                Platform.Menu {
+                    id: roomOptionsMenu
+
+                    Platform.MenuItem {
+                        visible: room ? room.permissions.canInvite() : false
+                        text: qsTr("Invite users")
+                        onTriggered: TimelineManager.openInviteUsers(roomId)
+                    }
 
-        ImageButton {
-            id: roomOptionsButton
-
-            visible: !!room
-            Layout.column: 5
-            Layout.row: 0
-            Layout.rowSpan: 2
-            Layout.alignment: Qt.AlignVCenter
-            Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
-            Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-            image: ":/icons/icons/ui/options.svg"
-            ToolTip.visible: hovered
-            ToolTip.text: qsTr("Room options")
-            onClicked: roomOptionsMenu.open(roomOptionsButton)
-
-            Platform.Menu {
-                id: roomOptionsMenu
-
-                Platform.MenuItem {
-                    visible: room ? room.permissions.canInvite() : false
-                    text: qsTr("Invite users")
-                    onTriggered: TimelineManager.openInviteUsers(roomId)
-                }
+                    Platform.MenuItem {
+                        text: qsTr("Members")
+                        onTriggered: TimelineManager.openRoomMembers(room)
+                    }
 
-                Platform.MenuItem {
-                    text: qsTr("Members")
-                    onTriggered: TimelineManager.openRoomMembers(room)
-                }
+                    Platform.MenuItem {
+                        text: qsTr("Leave room")
+                        onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
+                    }
 
-                Platform.MenuItem {
-                    text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
-                }
+                    Platform.MenuItem {
+                        text: qsTr("Settings")
+                        onTriggered: TimelineManager.openRoomSettings(roomId)
+                    }
 
-                Platform.MenuItem {
-                    text: qsTr("Settings")
-                    onTriggered: TimelineManager.openRoomSettings(roomId)
                 }
 
             }
 
-        }
-
-        ScrollView {
-            id: pinnedMessages
-
-            Layout.row: 2
-            Layout.column: 2
-            Layout.columnSpan: 3
-
-            Layout.fillWidth: true
-            Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
-
-            visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
-            clip: true
-
-            palette: Nheko.colors
-            ScrollBar.horizontal.visible: false
-
-            ListView {
-
-                spacing: Nheko.paddingSmall
-                model: room ? room.pinnedMessages : undefined
-                delegate: RowLayout {
-                    required property string modelData
-
-                    width: ListView.view.width
-                    height: implicitHeight
-
-                    Reply {
-                        property var e: room ? room.getDump(modelData, "") : {}
-                        Layout.fillWidth: true
-                        Layout.preferredHeight: height
-
-                        userColor: TimelineManager.userColor(e.userId, Nheko.colors.window)
-                        blurhash: e.blurhash ?? ""
-                        body: e.body ?? ""
-                        formattedBody: e.formattedBody ?? ""
-                        eventId: e.eventId ?? ""
-                        filename: e.filename ?? ""
-                        filesize: e.filesize ?? ""
-                        proportionalHeight: e.proportionalHeight ?? 1
-                        type: e.type ?? MtxEvent.UnknownMessage
-                        typeString: e.typeString ?? ""
-                        url: e.url ?? ""
-                        originalWidth: e.originalWidth ?? 0
-                        isOnlyEmoji: e.isOnlyEmoji ?? false
-                        userId: e.userId ?? ""
-                        userName: e.userName ?? ""
-                        encryptionError: e.encryptionError ?? ""
+            ScrollView {
+                id: pinnedMessages
+
+                Layout.row: 2
+                Layout.column: 2
+                Layout.columnSpan: 3
+
+                Layout.fillWidth: true
+                Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
+
+                visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
+                clip: true
+
+                palette: Nheko.colors
+                ScrollBar.horizontal.visible: false
+
+                ListView {
+
+                    spacing: Nheko.paddingSmall
+                    model: room ? room.pinnedMessages : undefined
+                    delegate: RowLayout {
+                        required property string modelData
+
+                        width: ListView.view.width
+                        height: implicitHeight
+
+                        Reply {
+                            property var e: room ? room.getDump(modelData, "") : {}
+                            Layout.fillWidth: true
+                            Layout.preferredHeight: height
+
+                            userColor: TimelineManager.userColor(e.userId, Nheko.colors.window)
+                            blurhash: e.blurhash ?? ""
+                            body: e.body ?? ""
+                            formattedBody: e.formattedBody ?? ""
+                            eventId: e.eventId ?? ""
+                            filename: e.filename ?? ""
+                            filesize: e.filesize ?? ""
+                            proportionalHeight: e.proportionalHeight ?? 1
+                            type: e.type ?? MtxEvent.UnknownMessage
+                            typeString: e.typeString ?? ""
+                            url: e.url ?? ""
+                            originalWidth: e.originalWidth ?? 0
+                            isOnlyEmoji: e.isOnlyEmoji ?? false
+                            userId: e.userId ?? ""
+                            userName: e.userName ?? ""
+                            encryptionError: e.encryptionError ?? ""
+                        }
+
+                        ImageButton {
+                            id: deletePinButton
+
+                            Layout.preferredHeight: 16
+                            Layout.preferredWidth: 16
+                            Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+                            visible: room.permissions.canChange(MtxEvent.PinnedEvents)
+
+                            hoverEnabled: true
+                            image: ":/icons/icons/ui/dismiss.svg"
+                            ToolTip.visible: hovered
+                            ToolTip.text: qsTr("Unpin")
+
+                            onClicked: room.unpin(modelData)
+                        }
                     }
 
-                    ImageButton {
-                        id: deletePinButton
 
-                        Layout.preferredHeight: 16
-                        Layout.preferredWidth: 16
-                        Layout.alignment: Qt.AlignTop | Qt.AlignLeft
-                        visible: room.permissions.canChange(MtxEvent.PinnedEvents)
-
-                        hoverEnabled: true
-                        image: ":/icons/icons/ui/dismiss.svg"
-                        ToolTip.visible: hovered
-                        ToolTip.text: qsTr("Unpin")
-
-                        onClicked: room.unpin(modelData)
+                    ScrollHelper {
+                        flickable: parent
+                        anchors.fill: parent
+                        enabled: !Settings.mobileMode
                     }
                 }
-
-
-                ScrollHelper {
-                    flickable: parent
-                    anchors.fill: parent
-                    enabled: !Settings.mobileMode
-                }
             }
         }
-    }
 
-    CursorShape {
-        anchors.fill: parent
-        anchors.bottomMargin: pinnedMessages.visible ? pinnedMessages.height : 0
-        cursorShape: Qt.PointingHandCursor
+        CursorShape {
+            anchors.fill: parent
+            anchors.bottomMargin: pinnedMessages.visible ? pinnedMessages.height : 0
+            cursorShape: Qt.PointingHandCursor
+        }
     }
 }
diff --git a/resources/qml/components/FlatButton.qml b/resources/qml/components/FlatButton.qml
index 8ca3f104d5f3adc8642020f31f789898a5ba6fec..72184d2826a1abb7354ed8403c4872f5361aa1fc 100644
--- a/resources/qml/components/FlatButton.qml
+++ b/resources/qml/components/FlatButton.qml
@@ -12,7 +12,7 @@ import im.nheko 1.0
 Button {
     id: control
 
-    implicitHeight: Math.ceil(control.contentItem.implicitHeight * 1.5)
+    implicitHeight: Math.ceil(control.contentItem.implicitHeight * 1.70)
     implicitWidth: Math.ceil(control.contentItem.implicitWidth + control.contentItem.implicitHeight)
     hoverEnabled: true
 
@@ -42,7 +42,7 @@ Button {
     background: Rectangle {
         //height: control.contentItem.implicitHeight * 2
         //width: control.contentItem.implicitWidth * 2
-        radius: height / 6
+        radius: height / 8
         color: Qt.lighter(Nheko.colors.dark, control.down ? 1.4 : (control.hovered ? 1.2 : 1))
     }
 
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index c00a0bdbae95c304607a5baf3c9c8dffc348748d..90dc9ac47a005911b84ab8eebc5ff0ac1e4d3700 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -21,7 +21,6 @@ ApplicationWindow {
     minimumHeight: stack.implicitHeight
     width: stack.implicitWidth
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(dialog)
 
     StackView {
         id: stack
diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
index 3ba04d9499d14edbff2d5f098538d71615965c9d..eb420fced5c423d5811091620ec775707864f2cd 100644
--- a/resources/qml/dialogs/ImagePackEditorDialog.qml
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -12,8 +12,6 @@ import QtQuick.Layouts 1.12
 import im.nheko 1.0
 
 ApplicationWindow {
-    //Component.onCompleted: Nheko.reparent(win)
-
     id: win
 
     property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
@@ -175,39 +173,37 @@ ApplicationWindow {
                         }
                     }
 
-                    MatrixText {
-                        visible: imagePack.roomid
-                        text: qsTr("State key")
-                    }
-
                     MatrixTextField {
+                        id: statekeyField
+
                         visible: imagePack.roomid
                         Layout.fillWidth: true
+                        Layout.columnSpan: 2
+                        label: qsTr("State key")
                         text: imagePack.statekey
                         onTextEdited: imagePack.statekey = text
                     }
 
-                    MatrixText {
-                        text: qsTr("Packname")
-                    }
-
                     MatrixTextField {
                         Layout.fillWidth: true
+                        Layout.columnSpan: 2
+                        label: qsTr("Packname")
                         text: imagePack.packname
                         onTextEdited: imagePack.packname = text
                     }
 
-                    MatrixText {
-                        text: qsTr("Attribution")
-                    }
-
                     MatrixTextField {
                         Layout.fillWidth: true
+                        Layout.columnSpan: 2
+                        label: qsTr("Attribution")
                         text: imagePack.attribution
                         onTextEdited: imagePack.attribution = text
                     }
 
                     MatrixText {
+                        Layout.margins: statekeyField.textPadding
+                        font.weight: Font.DemiBold
+                        font.letterSpacing: font.pixelSize * 0.02
                         text: qsTr("Use as Emoji")
                     }
 
@@ -218,6 +214,9 @@ ApplicationWindow {
                     }
 
                     MatrixText {
+                        Layout.margins: statekeyField.textPadding
+                        font.weight: Font.DemiBold
+                        font.letterSpacing: font.pixelSize * 0.02
                         text: qsTr("Use as Sticker")
                     }
 
@@ -253,27 +252,28 @@ ApplicationWindow {
                         Layout.alignment: Qt.AlignHCenter
                     }
 
-                    MatrixText {
-                        text: qsTr("Shortcode")
-                    }
-
                     MatrixTextField {
                         Layout.fillWidth: true
+                        Layout.columnSpan: 2
+                        label: qsTr("Shortcode")
                         text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
                         onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode)
                     }
 
-                    MatrixText {
-                        text: qsTr("Body")
-                    }
-
                     MatrixTextField {
+                        id: bodyField
+
                         Layout.fillWidth: true
+                        Layout.columnSpan: 2
+                        label: qsTr("Body")
                         text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body)
                         onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body)
                     }
 
                     MatrixText {
+                        Layout.margins: bodyField.textPadding
+                        font.weight: Font.DemiBold
+                        font.letterSpacing: font.pixelSize * 0.02
                         text: qsTr("Use as Emoji")
                     }
 
@@ -284,6 +284,9 @@ ApplicationWindow {
                     }
 
                     MatrixText {
+                        Layout.margins: bodyField.textPadding
+                        font.weight: Font.DemiBold
+                        font.letterSpacing: font.pixelSize * 0.02
                         text: qsTr("Use as Sticker")
                     }
 
@@ -294,6 +297,9 @@ ApplicationWindow {
                     }
 
                     MatrixText {
+                        Layout.margins: bodyField.textPadding
+                        font.weight: Font.DemiBold
+                        font.letterSpacing: font.pixelSize * 0.02
                         text: qsTr("Remove from pack")
                     }
 
diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml
index fa079855a7f6d53eadb0051831dd36d323863761..18c32c416ccdcb350ead52d9b2208e9094a3d60a 100644
--- a/resources/qml/dialogs/ImagePackSettingsDialog.qml
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -28,7 +28,6 @@ ApplicationWindow {
     color: Nheko.colors.base
     modality: Qt.NonModal
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(win)
 
     Component {
         id: packEditor
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index 63ca3181888e74a7c1663d6b0b5362df512f898d..cf1474dc60a5cb712c2f4ab62b16262b4f566226 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -18,7 +18,6 @@ ApplicationWindow {
 
     modality: Qt.NonModal
     flags: Qt.Dialog
-    Component.onCompleted: Nheko.reparent(inputDialog)
     width: 350
     height: fontMetrics.lineSpacing * 7
 
diff --git a/resources/qml/dialogs/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml
index 917bc8567e38aeaacadc38fedb87d1eb5d64cd44..e7dd4e3a2ecaf51470b29f60e08232d6b83b8e01 100644
--- a/resources/qml/dialogs/InviteDialog.qml
+++ b/resources/qml/dialogs/InviteDialog.qml
@@ -37,7 +37,6 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.window
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(inviteDialogRoot)
 
     Shortcut {
         sequence: "Ctrl+Enter"
diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml
index 9ce6bcf15ecc7a50f5c2eba835dc11b213f9f705..e49f538d66c4cdf1603bfdc12faebe48a225e912 100644
--- a/resources/qml/dialogs/JoinRoomDialog.qml
+++ b/resources/qml/dialogs/JoinRoomDialog.qml
@@ -17,7 +17,6 @@ ApplicationWindow {
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
     palette: Nheko.colors
     color: Nheko.colors.window
-    Component.onCompleted: Nheko.reparent(joinRoomRoot)
     width: 350
     height: fontMetrics.lineSpacing * 7
 
diff --git a/resources/qml/dialogs/PhoneNumberInputDialog.qml b/resources/qml/dialogs/PhoneNumberInputDialog.qml
index 399b11d51cc62c7eaa683908dd8b722353361fff..9c36c98ff63cb38bf13174b7a256cf4fd512f254 100644
--- a/resources/qml/dialogs/PhoneNumberInputDialog.qml
+++ b/resources/qml/dialogs/PhoneNumberInputDialog.qml
@@ -19,7 +19,6 @@ ApplicationWindow {
 
     modality: Qt.NonModal
     flags: Qt.Dialog
-    Component.onCompleted: Nheko.reparent(inputDialog)
     width: 350
     height: fontMetrics.lineSpacing * 7
 
diff --git a/resources/qml/dialogs/RawMessageDialog.qml b/resources/qml/dialogs/RawMessageDialog.qml
index 34104394e1a4cc70aea1478b211507c908faec48..774b078bee63350360a1e36bb0fdac2ada9b14bd 100644
--- a/resources/qml/dialogs/RawMessageDialog.qml
+++ b/resources/qml/dialogs/RawMessageDialog.qml
@@ -17,7 +17,6 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.window
     flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(rawMessageRoot)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/dialogs/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
index aced4374b201f562aa6019dd2b502094bd6e4ac6..da87996e07d7e49d4c90cad0857c194e7ddaa62b 100644
--- a/resources/qml/dialogs/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -22,7 +22,6 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.window
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(readReceiptsRoot)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/dialogs/RoomDirectory.qml b/resources/qml/dialogs/RoomDirectory.qml
index f458ac51c034e6f2317fbb344b27bdebe879cee5..36c29a0b6cf1720319c57198e94db8a266cf396d 100644
--- a/resources/qml/dialogs/RoomDirectory.qml
+++ b/resources/qml/dialogs/RoomDirectory.qml
@@ -22,7 +22,6 @@ ApplicationWindow {
     color: Nheko.colors.window
     modality: Qt.WindowModal
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
     title: qsTr("Explore Public Rooms")
 
     Shortcut {
@@ -189,7 +188,6 @@ ApplicationWindow {
             Layout.fillWidth: true
             selectByMouse: true
             font.pixelSize: fontMetrics.font.pixelSize
-            padding: Nheko.paddingMedium
             color: Nheko.colors.text
             placeholderText: qsTr("Search for public rooms")
             onTextChanged: searchTimer.restart()
@@ -200,7 +198,6 @@ ApplicationWindow {
 
             Layout.minimumWidth: 0.3 * header.width
             Layout.maximumWidth: 0.3 * header.width
-            padding: Nheko.paddingMedium
             color: Nheko.colors.text
             placeholderText: qsTr("Choose custom homeserver")
             onTextChanged: publicRooms.setMatrixServer(text)
diff --git a/resources/qml/dialogs/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml
index 89cce4148af5dbe888cd5e7382e5b3e94ddc0e3e..55d5488bc046f60c2707b4808a0564511d9a3a59 100644
--- a/resources/qml/dialogs/RoomMembers.qml
+++ b/resources/qml/dialogs/RoomMembers.qml
@@ -24,7 +24,6 @@ ApplicationWindow {
     palette: Nheko.colors
     color: Nheko.colors.window
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(roomMembersRoot)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml
index c9f2b1a133977f6564eb10f89302ea540fcbc33e..48d2e2b726958a44cd6e593a72bc9db236296590 100644
--- a/resources/qml/dialogs/RoomSettings.qml
+++ b/resources/qml/dialogs/RoomSettings.qml
@@ -23,7 +23,6 @@ ApplicationWindow {
     color: Nheko.colors.window
     modality: Qt.NonModal
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(roomSettingsDialog)
     title: qsTr("Room Settings")
 
     Shortcut {
diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml
index 29ce2c3feb8c0f83492756852d6db67878b96814..73c4e68bff379b191b81cfa747ae6c5e768954b5 100644
--- a/resources/qml/dialogs/UserProfile.qml
+++ b/resources/qml/dialogs/UserProfile.qml
@@ -13,9 +13,6 @@ import QtQuick.Window 2.13
 import im.nheko 1.0
 
 ApplicationWindow {
-    // this does not work in ApplicationWindow, just in Window
-    //transientParent: Nheko.mainwindow()
-
     id: userProfileDialog
 
     property var profile
@@ -29,7 +26,6 @@ ApplicationWindow {
     title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
     modality: Qt.NonModal
     flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
-    Component.onCompleted: Nheko.reparent(userProfileDialog)
 
     Shortcut {
         sequence: StandardKey.Cancel
diff --git a/resources/qml/pages/LoginPage.qml b/resources/qml/pages/LoginPage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..4d3a52b3b6bfbd03c63d16181e2990389fe722bc
--- /dev/null
+++ b/resources/qml/pages/LoginPage.qml
@@ -0,0 +1,182 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.15
+import im.nheko 1.0
+import "../components/"
+import "../ui/"
+import "../"
+
+Item {
+    id: loginPage
+    property int maxExpansion: 400
+
+    property string error: login.error
+
+    Login {
+        id: login
+    }
+
+    ScrollView {
+        id: scroll
+
+        clip: false
+        palette: Nheko.colors
+        ScrollBar.horizontal.visible: false
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.verticalCenter: parent.verticalCenter
+        height: Math.min(loginPage.height, col.implicitHeight)
+        anchors.margins: Nheko.paddingLarge
+
+        contentWidth: availableWidth
+
+        ColumnLayout {
+            id: col
+
+            spacing: Nheko.paddingMedium
+
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: Math.min(loginPage.maxExpansion, scroll.width- Nheko.paddingLarge*2)
+
+            Image {
+                Layout.alignment: Qt.AlignHCenter
+                source: "qrc:/logos/login.png"
+                height: 128
+                width: 128
+            }
+
+            RowLayout {
+                spacing: Nheko.paddingLarge
+
+                Layout.fillWidth: true
+                MatrixTextField {
+                    id: matrixIdLabel
+                    label: qsTr("Matrix ID")
+                    placeholderText: qsTr("e.g @joe:matrix.org")
+                    onEditingFinished: login.mxid = text
+
+                    ToolTip.text: qsTr("Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.\nYou can also put your homeserver address there, if your server doesn't support .well-known lookup.\nExample: @user:server.my\nIf Nheko fails to discover your homeserver, it will show you a field to enter the server manually.")
+                    Keys.forwardTo: [pwBtn, ssoBtn]
+                }
+
+
+                Spinner {
+                    height: matrixIdLabel.height/2
+                    Layout.alignment: Qt.AlignBottom
+
+                    visible: running
+                    running: login.lookingUpHs
+                    foreground: Nheko.colors.mid
+                }
+            }
+
+            MatrixText {
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: login.mxidError
+                visible: text
+            }
+
+            MatrixTextField {
+                id: passwordLabel
+                Layout.fillWidth: true
+                label: qsTr("Password")
+                echoMode: TextInput.Password
+                ToolTip.text: qsTr("Your password.")
+                visible: login.passwordSupported
+                Keys.forwardTo: [pwBtn, ssoBtn]
+            }
+
+            MatrixTextField {
+                id: deviceNameLabel
+                Layout.fillWidth: true
+                label: qsTr("Device name")
+                placeholderText: login.initialDeviceName()
+                ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.")
+                Keys.forwardTo: [pwBtn, ssoBtn]
+            }
+
+            MatrixTextField {
+                id: hsLabel
+                enabled: visible
+                visible: login.homeserverNeeded
+
+                Layout.fillWidth: true
+                label: qsTr("Homeserver address")
+                placeholderText: qsTr("server.my:8787")
+                text: login.homeserver
+                onEditingFinished: login.homeserver = text
+                ToolTip.text: qsTr("The address that can be used to contact you homeservers client API.\nExample: https://server.my:8787")
+                Keys.forwardTo: [pwBtn, ssoBtn]
+            }
+
+            Item {
+                height: Nheko.avatarSize
+                Layout.fillWidth: true
+
+                Spinner {
+                    height: parent.height
+                    anchors.centerIn: parent
+
+                    visible: running
+                    running: login.loggingIn
+                    foreground: Nheko.colors.mid
+                }
+            }
+
+            MatrixText {
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: loginPage.error
+                visible: text
+            }
+
+            FlatButton {
+                id: pwBtn
+                visible: login.passwordSupported
+                enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
+                Layout.alignment: Qt.AlignHCenter
+                text: qsTr("LOGIN")
+                function pwLogin() {
+                    login.onLoginButtonClicked(Login.Password, matrixIdLabel.text, passwordLabel.text, deviceNameLabel.text)
+                }
+                onClicked: pwBtn.pwLogin()
+                Keys.onEnterPressed: pwBtn.pwLogin()
+                Keys.onReturnPressed: pwBtn.pwLogin()
+                Keys.enabled: pwBtn.enabled && login.passwordSupported
+            }
+            FlatButton {
+                id: ssoBtn
+                visible: login.ssoSupported
+                enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
+                Layout.alignment: Qt.AlignHCenter
+                text: qsTr("SSO LOGIN")
+                function ssoLogin() {
+                    login.onLoginButtonClicked(Login.SSO, matrixIdLabel.text, passwordLabel.text, deviceNameLabel.text)
+                }
+                onClicked: ssoBtn.ssoLogin()
+                Keys.onEnterPressed: ssoBtn.ssoLogin()
+                Keys.onReturnPressed: ssoBtn.ssoLogin()
+                Keys.enabled: ssoBtn.enabled && !login.passwordSupported
+            }
+
+        }
+    }
+
+    ImageButton {
+        anchors.top: parent.top
+        anchors.left: parent.left
+        anchors.margins: Nheko.paddingMedium
+        width: Nheko.avatarSize
+        height: Nheko.avatarSize
+        image: ":/icons/icons/ui/angle-arrow-left.svg"
+        ToolTip.visible: hovered
+        ToolTip.text: qsTr("Back")
+        onClicked: mainWindow.pop()
+    }
+}
diff --git a/resources/qml/pages/RegisterPage.qml b/resources/qml/pages/RegisterPage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..44836ccbdcf38afd14c522d8168097afa2bd3917
--- /dev/null
+++ b/resources/qml/pages/RegisterPage.qml
@@ -0,0 +1,215 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.15
+import im.nheko 1.0
+import "../components/"
+import "../ui/"
+import "../"
+
+Item {
+    id: registrationPage
+    property int maxExpansion: 400
+
+    property string error: regis.error
+
+    Registration {
+        id: regis
+    }
+
+    ScrollView {
+        id: scroll
+
+        clip: false
+        palette: Nheko.colors
+        ScrollBar.horizontal.visible: false
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.verticalCenter: parent.verticalCenter
+        height: Math.min(registrationPage.height, col.implicitHeight)
+        anchors.margins: Nheko.paddingLarge
+
+        contentWidth: availableWidth
+
+        ColumnLayout {
+            id: col
+
+            spacing: Nheko.paddingMedium
+
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: Math.min(registrationPage.maxExpansion, scroll.width- Nheko.paddingLarge*2)
+
+            Image {
+                Layout.alignment: Qt.AlignHCenter
+                source: "qrc:/logos/login.png"
+                height: 128
+                width: 128
+            }
+
+            RowLayout {
+                spacing: Nheko.paddingLarge
+
+                Layout.fillWidth: true
+                MatrixTextField {
+                    id: hsLabel
+                    label: qsTr("Homeserver")
+                    placeholderText: qsTr("your.server")
+                    onEditingFinished: regis.setServer(text)
+
+                    ToolTip.text: qsTr("A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.")
+                }
+
+
+                Spinner {
+                    height: hsLabel.height/2
+                    Layout.alignment: Qt.AlignBottom
+
+                    visible: running
+                    running: regis.lookingUpHs
+                    foreground: Nheko.colors.mid
+                }
+            }
+
+            MatrixText {
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: regis.hsError
+                visible: text
+            }
+
+            RowLayout {
+                spacing: Nheko.paddingLarge
+
+                visible: regis.supported
+
+                Layout.fillWidth: true
+                MatrixTextField {
+                    id: usernameLabel
+                    Layout.fillWidth: true
+                    label: qsTr("Username")
+                    ToolTip.text: qsTr("The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.")
+                    onEditingFinished: regis.checkUsername(text)
+                }
+                Spinner {
+                    height: usernameLabel.height/2
+                    Layout.alignment: Qt.AlignBottom
+
+                    visible: running
+                    running: regis.lookingUpUsername
+                    foreground: Nheko.colors.mid
+                }
+
+                Image {
+                    width: usernameLabel.height/2
+                    height: width
+                    Layout.preferredHeight: usernameLabel.height/2
+                    Layout.preferredWidth: usernameLabel.height/2
+                    Layout.alignment: Qt.AlignBottom
+                    source: regis.usernameAvailable ? ("image://colorimage/:/icons/icons/ui/checkmark.svg?green") : ("image://colorimage/:/icons/icons/ui/dismiss.svg?"+Nheko.theme.error)
+                    visible: regis.usernameAvailable || regis.usernameUnavailable
+                    ToolTip.visible: ma.hovered
+                    ToolTip.text: qsTr("Back")
+                    sourceSize.height: height * Screen.devicePixelRatio
+                    sourceSize.width: width * Screen.devicePixelRatio
+                    HoverHandler {
+                        id: ma
+                    }
+                }
+            }
+
+            MatrixText {
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: regis.usernameError
+                visible: text
+            }
+
+
+            MatrixTextField {
+                visible: regis.supported
+                id: passwordLabel
+                Layout.fillWidth: true
+                label: qsTr("Password")
+                echoMode: TextInput.Password
+                ToolTip.text: qsTr("Please choose a secure password. The exact requirements for password strength may depend on your server.")
+            }
+
+            MatrixTextField {
+                visible: regis.supported
+                id: passwordConfirmationLabel
+                Layout.fillWidth: true
+                label: qsTr("Password confirmation")
+                echoMode: TextInput.Password
+            }
+
+            MatrixText {
+                visible: regis.supported
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: passwordLabel.text != passwordConfirmationLabel.text ? qsTr("Your passwords do not match!") : ""
+            }
+
+            MatrixTextField {
+                visible: regis.supported
+                id: deviceNameLabel
+                Layout.fillWidth: true
+                label: qsTr("Device name")
+                placeholderText: regis.initialDeviceName()
+                ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.")
+            }
+
+            Item {
+                height: Nheko.avatarSize
+                Layout.fillWidth: true
+
+                Spinner {
+                    height: parent.height
+                    anchors.centerIn: parent
+
+                    visible: running
+                    running: regis.registering
+                    foreground: Nheko.colors.mid
+                }
+            }
+
+            MatrixText {
+                textFormat: Text.PlainText
+                color: Nheko.theme.error
+                text: registrationPage.error
+                visible: text
+            }
+
+            FlatButton {
+                id: regisBtn
+                visible: regis.supported
+                enabled: usernameLabel.text && passwordLabel.text && passwordLabel.text == passwordConfirmationLabel.text
+                Layout.alignment: Qt.AlignHCenter
+                text: qsTr("REGISTER")
+                function register() {
+                    regis.startRegistration(usernameLabel.text, passwordLabel.text, deviceNameLabel.text)
+                }
+                onClicked: regisBtn.register()
+                Keys.onEnterPressed: regisBtn.register()
+                Keys.onReturnPressed: regisBtn.register()
+                Keys.enabled: regisBtn.enabled && regis.supported
+            }
+        }
+    }
+
+    ImageButton {
+        anchors.top: parent.top
+        anchors.left: parent.left
+        anchors.margins: Nheko.paddingMedium
+        width: Nheko.avatarSize
+        height: Nheko.avatarSize
+        image: ":/icons/icons/ui/angle-arrow-left.svg"
+        ToolTip.visible: hovered
+        ToolTip.text: qsTr("Back")
+        onClicked: mainWindow.pop()
+    }
+}
+
diff --git a/resources/qml/pages/WelcomePage.qml b/resources/qml/pages/WelcomePage.qml
new file mode 100644
index 0000000000000000000000000000000000000000..e1ecc31d2e518505226fe9a01605cc00fd195b9a
--- /dev/null
+++ b/resources/qml/pages/WelcomePage.qml
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import QtQuick.Window 2.15
+import im.nheko 1.0
+import "../components/"
+
+ColumnLayout {
+    Item {
+        Layout.fillHeight: true
+    }
+
+    Image {
+        Layout.alignment: Qt.AlignHCenter
+        source: "qrc:/logos/splash.png"
+        height: 256
+        width: 256
+    }
+
+    Label {
+        Layout.margins: Nheko.paddingLarge
+        Layout.bottomMargin: 0
+        Layout.alignment: Qt.AlignHCenter
+        Layout.fillWidth: true
+        text: qsTr("Welcome to nheko! The desktop client for the Matrix protocol.")
+        color: Nheko.colors.text
+        font.pointSize: fontMetrics.font.pointSize*2
+        wrapMode: Text.Wrap
+        horizontalAlignment: Text.AlignHCenter
+    }
+    Label {
+        Layout.margins: Nheko.paddingLarge
+        Layout.alignment: Qt.AlignHCenter
+        Layout.fillWidth: true
+        text: qsTr("Enjoy your stay!")
+        color: Nheko.colors.text
+        font.pointSize: fontMetrics.font.pointSize*1.5
+        wrapMode: Text.Wrap
+        horizontalAlignment: Text.AlignHCenter
+    }
+
+    RowLayout {
+        Item {
+            Layout.fillWidth: true
+        }
+        FlatButton {
+            Layout.margins: Nheko.paddingLarge
+            Layout.alignment: Qt.AlignHCenter
+            text: qsTr("REGISTER")
+            onClicked: {
+                mainWindow.push(registerPage);
+            }
+        }
+        FlatButton {
+            Layout.margins: Nheko.paddingLarge
+            Layout.alignment: Qt.AlignHCenter
+            text: qsTr("LOGIN")
+            onClicked: {
+                mainWindow.push(loginPage);
+            }
+        }
+        Item {
+            Layout.fillWidth: true
+        }
+    }
+    Item {
+        Layout.fillHeight: true
+    }
+}
diff --git a/resources/qml/ui/Snackbar.qml b/resources/qml/ui/Snackbar.qml
new file mode 100644
index 0000000000000000000000000000000000000000..80c0d88860a0aa181216c807d6b7039665413929
--- /dev/null
+++ b/resources/qml/ui/Snackbar.qml
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+Popup {
+    id: snackbar
+
+    property var messages: []
+    property string currentMessage: ""
+
+    function showNotification(msg) {
+        messages.push(msg);
+        currentMessage = messages[0];
+        if (!visible) {
+            open();
+            dismissTimer.start();
+        }
+    }
+
+    Timer {
+        id: dismissTimer
+        interval: 10000
+        onTriggered: snackbar.close()
+    }
+
+    onAboutToHide: {
+        messages.shift();
+    }
+    onClosed: {
+        if (messages.length > 0) {
+            currentMessage = messages[0];
+            open();
+            dismissTimer.restart();
+        }
+    }
+
+    parent: Overlay.overlay
+    opacity: 0
+    y: -100
+    x: (parent.width - width)/2
+    padding: Nheko.paddingLarge
+
+    contentItem: Label {
+        color: Nheko.colors.light
+        width: Math.max(Overlay.overlay? Overlay.overlay.width/2 : 0, 400)
+        text: snackbar.currentMessage
+        font.bold: true
+    }
+
+    background: Rectangle {
+        radius: Nheko.paddingLarge
+        color: Nheko.colors.dark
+        opacity: 0.8
+    }
+
+    enter: Transition {
+        NumberAnimation {
+            target: snackbar
+            property: "opacity"
+            from: 0.0
+            to: 1.0
+            duration: 200
+            easing.type: Easing.OutCubic
+        }
+        NumberAnimation {
+            target: snackbar
+            properties: "y"
+            from: -100
+            to: 100
+            duration: 1000
+            easing.type: Easing.OutCubic
+        }
+    }
+    exit: Transition {
+        NumberAnimation {
+            target: snackbar
+            property: "opacity"
+            from: 1.0
+            to: 0.0
+            duration: 300
+            easing.type: Easing.InCubic
+        }
+        NumberAnimation {
+            target: snackbar
+            properties: "y"
+            to: -100
+            from: 100
+            duration: 300
+            easing.type: Easing.InCubic
+        }
+    }
+}
+
+
diff --git a/resources/res.qrc b/resources/res.qrc
index a2ee393f4ebfce001227faadf54c098e27f76748..2fba5f4cc0c5caa20d8533b5241c4c1703eec0ff 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -110,6 +110,9 @@
         <file>qml/TypingIndicator.qml</file>
         <file>qml/NotificationWarning.qml</file>
         <file>qml/pages/UserSettingsPage.qml</file>
+        <file>qml/pages/WelcomePage.qml</file>
+        <file>qml/pages/LoginPage.qml</file>
+        <file>qml/pages/RegisterPage.qml</file>
         <file>qml/components/AdaptiveLayout.qml</file>
         <file>qml/components/AdaptiveLayoutElement.qml</file>
         <file>qml/components/AvatarListTile.qml</file>
@@ -154,6 +157,7 @@
         <file>qml/ui/NhekoSlider.qml</file>
         <file>qml/ui/Ripple.qml</file>
         <file>qml/ui/Spinner.qml</file>
+        <file>qml/ui/Snackbar.qml</file>
         <file>qml/ui/animations/BlinkAnimation.qml</file>
         <file>qml/ui/media/MediaControls.qml</file>
         <file>qml/voip/ActiveCallBar.qml</file>
diff --git a/src/Cache.cpp b/src/Cache.cpp
index d1723d98df6f6fac815f6959be9993ab2bcfa5b5..b55d53a6aab3c0dafb5654d44bbecb1c5bcf6f9a 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -325,7 +325,7 @@ static void
 fatalSecretError()
 {
     QMessageBox::critical(
-      ChatPage::instance(),
+      nullptr,
       QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
       QCoreApplication::translate(
         "SecretStorage",
@@ -391,6 +391,7 @@ Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad)
             &QKeychain::ReadPasswordJob::finished,
             this,
             [this, name, toLoad, job](QKeychain::Job *) mutable {
+                nhlog::db()->debug("Finished reading '{}'", toLoad.begin()->first);
                 const QString secret = job->textData();
                 if (job->error() && job->error() != QKeychain::Error::EntryNotFound) {
                     nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
@@ -413,6 +414,7 @@ Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad)
                 // You can't start a job from the finish signal of a job.
                 QTimer::singleShot(0, this, [this, toLoad] { loadSecrets(toLoad); });
             });
+    nhlog::db()->debug("Reading '{}'", name_);
     job->start();
 }
 
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index bfaa63895e18fb32646440646f00546383df7ddc..cdaf726001fc44fbfab4f89df7983909e0a740d3 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -22,7 +22,6 @@
 #include "Utils.h"
 #include "encryption/DeviceVerificationFlow.h"
 #include "encryption/Olm.h"
-#include "ui/OverlayModal.h"
 #include "ui/Theme.h"
 #include "ui/UserProfile.h"
 #include "voip/CallManager.h"
@@ -44,8 +43,8 @@ Q_DECLARE_METATYPE(mtx::presence::PresenceState)
 Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription)
 Q_DECLARE_METATYPE(SecretsToDecrypt)
 
-ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
-  : QWidget(parent)
+ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
+  : QObject(parent)
   , isConnected_(true)
   , userSettings_{userSettings}
   , notificationsManager(this)
@@ -61,14 +60,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
     qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
     qRegisterMetaType<SecretsToDecrypt>();
 
-    topLayout_ = new QHBoxLayout(this);
-    topLayout_->setSpacing(0);
-    topLayout_->setContentsMargins(0, 0, 0, 0);
-
     view_manager_ = new TimelineViewManager(callManager_, this);
 
-    topLayout_->addWidget(view_manager_->getWidget());
-
     connect(this,
             &ChatPage::downloadedSecrets,
             this,
@@ -154,7 +147,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
             [this](const QString &roomid, const QString &eventid) {
                 Q_UNUSED(eventid)
                 view_manager_->rooms()->setCurrentRoom(roomid);
-                activateWindow();
+                MainWindow::instance()->requestActivate();
             });
     connect(&notificationsManager,
             &NotificationsManager::sendNotificationReply,
@@ -162,17 +155,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
             [this](const QString &roomid, const QString &eventid, const QString &body) {
                 view_manager_->rooms()->setCurrentRoom(roomid);
                 view_manager_->queueReply(roomid, eventid, body);
-                activateWindow();
+                MainWindow::instance()->requestActivate();
             });
 
-    connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
-        // ensure the qml context is shutdown before we destroy all other singletons
-        // Otherwise Qml will try to access the room list or settings, after they have been
-        // destroyed
-        topLayout_->removeWidget(view_manager_->getWidget());
-        delete view_manager_->getWidget();
-    });
-
     connect(
       this,
       &ChatPage::initializeViews,
@@ -183,8 +168,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
             &ChatPage::initializeEmptyViews,
             view_manager_,
             &TimelineViewManager::initializeRoomlist);
-    connect(
-      this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
     connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Sync &sync) {
         view_manager_->sync(sync);
 
@@ -201,7 +184,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
         // TODO: Replace this once we have proper pushrules support. This is a horrible hack
         if (prevNotificationCount < notificationCount) {
             if (userSettings_->hasAlertOnNotification())
-                QApplication::alert(this);
+                MainWindow::instance()->alert(0);
         }
         prevNotificationCount = notificationCount;
 
@@ -331,7 +314,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                     } else if (cacheVersion == cache::CacheVersion::Older) {
                         if (!cache::runMigrations()) {
                             QMessageBox::critical(
-                              this,
+                              nullptr,
                               tr("Cache migration failed!"),
                               tr("Migrating the cache to the current version failed. "
                                  "This can have different reasons. Please open an "
@@ -344,7 +327,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                         return;
                     } else if (cacheVersion == cache::CacheVersion::Newer) {
                         QMessageBox::critical(
-                          this,
+                          nullptr,
                           tr("Incompatible cache version"),
                           tr("The cache on your disk is newer than this version of Nheko "
                              "supports. Please update Nheko or clear your cache."));
@@ -690,7 +673,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
     if (promptForConfirmation &&
         QMessageBox::Yes !=
           QMessageBox::question(
-            this,
+            nullptr,
             tr("Confirm join"),
             tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
         return;
@@ -776,7 +759,7 @@ ChatPage::inviteUser(QString userid, QString reason)
 {
     auto room = currentRoom();
 
-    if (QMessageBox::question(this,
+    if (QMessageBox::question(nullptr,
                               tr("Confirm invite"),
                               tr("Do you really want to invite %1 (%2)?")
                                 .arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@@ -787,6 +770,8 @@ ChatPage::inviteUser(QString userid, QString reason)
       userid.toStdString(),
       [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
           if (err) {
+              nhlog::net()->error(
+                "Failed to invite {} to {}: {}", userid.toStdString(), room.toStdString(), *err);
               emit showNotification(
                 tr("Failed to invite %1 to %2: %3")
                   .arg(userid, room, QString::fromStdString(err->matrix_error.error)));
@@ -800,7 +785,7 @@ ChatPage::kickUser(QString userid, QString reason)
 {
     auto room = currentRoom();
 
-    if (QMessageBox::question(this,
+    if (QMessageBox::question(nullptr,
                               tr("Confirm kick"),
                               tr("Do you really want to kick %1 (%2)?")
                                 .arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@@ -825,7 +810,7 @@ ChatPage::banUser(QString userid, QString reason)
     auto room = currentRoom();
 
     if (QMessageBox::question(
-          this,
+          nullptr,
           tr("Confirm ban"),
           tr("Do you really want to ban %1 (%2)?").arg(cache::displayName(room, userid), userid)) !=
         QMessageBox::Yes)
@@ -849,7 +834,7 @@ ChatPage::unbanUser(QString userid, QString reason)
 {
     auto room = currentRoom();
 
-    if (QMessageBox::question(this,
+    if (QMessageBox::question(nullptr,
                               tr("Confirm unban"),
                               tr("Do you really want to unban %1 (%2)?")
                                 .arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@@ -1064,8 +1049,6 @@ ChatPage::initiateLogout()
 
         emit loggedOut();
     });
-
-    emit showOverlayProgressBar();
 }
 
 template<typename T>
@@ -1083,7 +1066,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
                                    const SecretsToDecrypt &secrets)
 {
     QString text = QInputDialog::getText(
-      ChatPage::instance(),
+      nullptr,
       QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
       keyDesc.name.empty()
         ? QCoreApplication::translate(
@@ -1115,7 +1098,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
 
     if (!decryptionKey) {
         QMessageBox::information(
-          ChatPage::instance(),
+          nullptr,
           QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
           QCoreApplication::translate("CrossSigningSecrets",
                                       "Failed to decrypt secrets with the "
@@ -1209,7 +1192,7 @@ ChatPage::startChat(QString userid)
 
     if (QMessageBox::Yes !=
         QMessageBox::question(
-          this,
+          nullptr,
           tr("Confirm invite"),
           tr("Do you really want to start a private chat with %1?").arg(userid)))
         return;
@@ -1395,7 +1378,7 @@ ChatPage::handleMatrixUri(const QUrl &uri)
 bool
 ChatPage::isRoomActive(const QString &room_id)
 {
-    return isActiveWindow() && currentRoom() == room_id;
+    return MainWindow::instance()->isActive() && currentRoom() == room_id;
 }
 
 QString
diff --git a/src/ChatPage.h b/src/ChatPage.h
index ae55c923b60f396829446e642f3546d4ec503508..e4b9e4e85634a49cbe3abee8ba975e799b21307a 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -8,7 +8,6 @@
 
 #include <atomic>
 #include <optional>
-#include <stack>
 #include <variant>
 
 #include <mtx/common.hpp>
@@ -18,17 +17,15 @@
 #include <mtx/events/presence.hpp>
 #include <mtx/secret_storage.hpp>
 
-#include <QHBoxLayout>
 #include <QMap>
 #include <QPoint>
+#include <QSharedPointer>
 #include <QTimer>
-#include <QWidget>
 
 #include "CacheCryptoStructs.h"
 #include "CacheStructs.h"
 #include "notifications/Manager.h"
 
-class OverlayModal;
 class TimelineViewManager;
 class UserSettings;
 class NotificationsManager;
@@ -51,12 +48,12 @@ struct Rooms;
 
 using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>;
 
-class ChatPage : public QWidget
+class ChatPage : public QObject
 {
     Q_OBJECT
 
 public:
-    ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr);
+    ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent = nullptr);
 
     // Initialize all the components of the UI.
     void bootstrap(QString userid, QString homeserver, QString token);
@@ -112,7 +109,6 @@ signals:
     void showNotification(const QString &msg);
     void showLoginPage(const QString &msg);
     void showUserSettingsPage();
-    void showOverlayProgressBar();
 
     void ownProfileOk();
     void setUserDisplayName(const QString &name);
@@ -143,7 +139,6 @@ signals:
     void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
     void themeChanged();
     void decryptSidebarChanged();
-    void chatFocusChanged(const bool focused);
 
     //! Signals for device verificaiton
     void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message);
@@ -201,8 +196,6 @@ private:
     template<typename T>
     void connectCallMessage();
 
-    QHBoxLayout *topLayout_;
-
     TimelineViewManager *view_manager_;
 
     QTimer connectivityTimer_;
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index 4c28f3645309164467a5901e43999f6d1097ae9b..6bed446ed84649cf7e28b7b301f191845d8da6f7 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -5,11 +5,6 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 #include <QDesktopServices>
-#include <QFontMetrics>
-#include <QLabel>
-#include <QPainter>
-#include <QStyleOption>
-#include <QtMath>
 
 #include <mtx/identifiers.hpp>
 #include <mtx/requests.hpp>
@@ -18,247 +13,94 @@
 #include "Config.h"
 #include "Logging.h"
 #include "LoginPage.h"
+#include "MainWindow.h"
 #include "MatrixClient.h"
 #include "SSOHandler.h"
 #include "UserSettingsPage.h"
-#include "ui/FlatButton.h"
-#include "ui/LoadingIndicator.h"
-#include "ui/OverlayModal.h"
-#include "ui/RaisedButton.h"
-#include "ui/TextField.h"
 
 Q_DECLARE_METATYPE(LoginPage::LoginMethod)
 
 using namespace mtx::identifiers;
 
-LoginPage::LoginPage(QWidget *parent)
-  : QWidget(parent)
+LoginPage::LoginPage(QObject *parent)
+  : QObject(parent)
   , inferredServerAddress_()
 {
-    qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
-
-    top_layout_ = new QVBoxLayout();
-
-    top_bar_layout_ = new QHBoxLayout();
-    top_bar_layout_->setSpacing(0);
-    top_bar_layout_->setContentsMargins(0, 0, 0, 0);
-
-    back_button_ = new FlatButton(this);
-    back_button_->setMinimumSize(QSize(30, 30));
-
-    top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
-    top_bar_layout_->addStretch(1);
-
-    QIcon icon;
-    icon.addFile(QStringLiteral(":/icons/icons/ui/angle-arrow-left.svg"));
-
-    back_button_->setIcon(icon);
-    back_button_->setIconSize(QSize(32, 32));
-
-    QIcon logo;
-    logo.addFile(QStringLiteral(":/logos/login.png"));
-
-    logo_ = new QLabel(this);
-    logo_->setPixmap(logo.pixmap(128));
-
-    logo_layout_ = new QHBoxLayout();
-    logo_layout_->setContentsMargins(0, 0, 0, 20);
-    logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
-
-    form_wrapper_ = new QHBoxLayout();
-    form_widget_  = new QWidget();
-    form_widget_->setMinimumSize(QSize(350, 200));
-
-    form_layout_ = new QVBoxLayout();
-    form_layout_->setSpacing(20);
-    form_layout_->setContentsMargins(0, 0, 0, 30);
-    form_widget_->setLayout(form_layout_);
-
-    form_wrapper_->addStretch(1);
-    form_wrapper_->addWidget(form_widget_);
-    form_wrapper_->addStretch(1);
-
-    matrixid_input_ = new TextField(this);
-    matrixid_input_->setLabel(tr("Matrix ID"));
-    matrixid_input_->setRegexp(QRegularExpression(QStringLiteral("@.+?:.{3,}")));
-    matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org"));
-    matrixid_input_->setToolTip(
-      tr("Your login name. A mxid should start with @ followed by the user id. After the user "
-         "id you need to include your server name after a :.\nYou can also put your homeserver "
-         "address there, if your server doesn't support .well-known lookup.\nExample: "
-         "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a "
-         "field to enter the server manually."));
-
-    spinner_ = new LoadingIndicator(this);
-    spinner_->setFixedHeight(40);
-    spinner_->setFixedWidth(40);
-    spinner_->hide();
-
-    errorIcon_ = new QLabel(this);
-    errorIcon_->setPixmap(QPixmap(QStringLiteral(":/icons/icons/error.png")));
-    errorIcon_->hide();
-
-    matrixidLayout_ = new QHBoxLayout();
-    matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter);
-
-    QFont font;
-
-    error_matrixid_label_ = new QLabel(this);
-    error_matrixid_label_->setFont(font);
-    error_matrixid_label_->setWordWrap(true);
-
-    password_input_ = new TextField(this);
-    password_input_->setLabel(tr("Password"));
-    password_input_->setEchoMode(QLineEdit::Password);
-    password_input_->setToolTip(tr("Your password."));
-
-    deviceName_ = new TextField(this);
-    deviceName_->setLabel(tr("Device name"));
-    deviceName_->setToolTip(
-      tr("A name for this device, which will be shown to others, when verifying your devices. "
-         "If none is provided a default is used."));
-
-    serverInput_ = new TextField(this);
-    serverInput_->setLabel(tr("Homeserver address"));
-    serverInput_->setPlaceholderText(tr("server.my:8787"));
-    serverInput_->setToolTip(tr("The address that can be used to contact you homeservers "
-                                "client API.\nExample: https://server.my:8787"));
-    serverInput_->hide();
-
-    serverLayout_ = new QHBoxLayout();
-    serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter);
-
-    form_layout_->addLayout(matrixidLayout_);
-    form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter);
-    form_layout_->addWidget(password_input_);
-    form_layout_->addWidget(deviceName_, Qt::AlignHCenter);
-    form_layout_->addLayout(serverLayout_);
-
-    error_matrixid_label_->hide();
-
-    button_layout_ = new QHBoxLayout();
-    button_layout_->setSpacing(20);
-    button_layout_->setContentsMargins(0, 0, 0, 30);
-
-    login_button_ = new RaisedButton(tr("LOGIN"), this);
-    login_button_->setMinimumSize(150, 65);
-    login_button_->setFontSize(20);
-    login_button_->setCornerRadius(3);
-
-    sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
-    sso_login_button_->setMinimumSize(150, 65);
-    sso_login_button_->setFontSize(20);
-    sso_login_button_->setCornerRadius(3);
-    sso_login_button_->setVisible(false);
-
-    button_layout_->addStretch(1);
-    button_layout_->addWidget(login_button_);
-    button_layout_->addWidget(sso_login_button_);
-    button_layout_->addStretch(1);
-
-    error_label_ = new QLabel(this);
-    error_label_->setFont(font);
-    error_label_->setWordWrap(true);
-
-    top_layout_->addLayout(top_bar_layout_);
-    top_layout_->addStretch(1);
-    top_layout_->addLayout(logo_layout_);
-    top_layout_->addLayout(form_wrapper_);
-    top_layout_->addStretch(1);
-    top_layout_->addLayout(button_layout_);
-    top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
-    top_layout_->addStretch(1);
-
-    setLayout(top_layout_);
+    [[maybe_unused]] static auto ignored =
+      qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
 
     connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection);
     connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
-
-    connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
-    connect(login_button_, &RaisedButton::clicked, this, [this]() {
-        onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
-    });
-    connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
-        onLoginButtonClicked(LoginMethod::SSO);
-    });
-    connect(this,
-            &LoginPage::showErrorMessage,
-            this,
-            static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
-            Qt::QueuedConnection);
-    connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-    connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-    connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-    connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
-    connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
-    connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
+    connect(
+      this,
+      &LoginPage::loginOk,
+      this,
+      [this](const mtx::responses::Login &res) {
+          loggingIn_ = false;
+          emit loggingInChanged();
+
+          http::client()->set_user(res.user_id);
+          MainWindow::instance()->showChatPage();
+      },
+      Qt::QueuedConnection);
 }
 void
 LoginPage::showError(const QString &msg)
 {
-    auto rect  = QFontMetrics(font()).boundingRect(msg);
-    int width  = rect.width();
-    int height = rect.height();
-    error_label_->setFixedHeight((int)qCeil(width / 200.0) * height);
-    error_label_->setText(msg);
+    loggingIn_ = false;
+    emit loggingInChanged();
+
+    error_ = msg;
+    emit errorOccurred();
 }
 
 void
-LoginPage::showError(QLabel *label, const QString &msg)
+LoginPage::setHomeserver(QString hs)
 {
-    auto rect  = QFontMetrics(font()).boundingRect(msg);
-    int width  = rect.width();
-    int height = rect.height();
-    label->setFixedHeight((int)qCeil(width / 200.0) * height);
-    label->setText(msg);
+    if (hs != homeserver_) {
+        homeserver_      = hs;
+        homeserverValid_ = false;
+        emit homeserverChanged();
+        http::client()->set_server(hs.toStdString());
+        checkHomeserverVersion();
+    }
 }
 
 void
 LoginPage::onMatrixIdEntered()
 {
-    error_label_->setText(QLatin1String(""));
+    clearErrors();
 
-    User user;
-
-    if (!matrixid_input_->isValid()) {
-        error_matrixid_label_->show();
-        showError(error_matrixid_label_,
-                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-        return;
-    } else {
-        error_matrixid_label_->setText(QLatin1String(""));
-        error_matrixid_label_->hide();
-    }
+    homeserverValid_ = false;
+    emit homeserverChanged();
 
+    User user;
     try {
-        user = parse<User>(matrixid_input_->text().toStdString());
+        user = parse<User>(mxid_.toStdString());
     } catch (const std::exception &) {
-        showError(error_matrixid_label_,
-                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        mxidError_ = tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org");
+        emit mxidErrorChanged();
         return;
     }
 
-    QString homeServer = QString::fromStdString(user.hostname());
-    if (homeServer != inferredServerAddress_) {
-        serverInput_->hide();
-        serverLayout_->removeWidget(errorIcon_);
-        errorIcon_->hide();
-        if (serverInput_->isVisible()) {
-            matrixidLayout_->removeWidget(spinner_);
-            serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-            spinner_->start();
-        } else {
-            serverLayout_->removeWidget(spinner_);
-            matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-            spinner_->start();
-        }
+    if (user.hostname().empty() || user.localpart().empty()) {
+        mxidError_ = tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org");
+        emit mxidErrorChanged();
+        return;
+    } else {
+        nhlog::net()->debug("hostname: {}", user.hostname());
+    }
 
-        inferredServerAddress_ = homeServer;
-        serverInput_->setText(homeServer);
+    if (user.hostname() != inferredServerAddress_.toStdString()) {
+        homeserverNeeded_ = false;
+        lookingUpHs_      = true;
+        emit lookingUpHsChanged();
 
         http::client()->set_server(user.hostname());
         http::client()->verify_certificates(
           !UserSettings::instance()->disableCertificateValidation());
+        homeserver_ = QString::fromStdString(user.hostname());
+        emit homeserverChanged();
 
         http::client()->well_known(
           [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
@@ -286,6 +128,7 @@ LoginPage::onMatrixIdEntered()
 
               nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
               http::client()->set_server(res.homeserver.base_url);
+              emit homeserverChanged();
               checkHomeserverVersion();
           });
     }
@@ -294,6 +137,16 @@ LoginPage::onMatrixIdEntered()
 void
 LoginPage::checkHomeserverVersion()
 {
+    clearErrors();
+
+    try {
+        User user = parse<User>(mxid_.toStdString());
+    } catch (const std::exception &) {
+        mxidError_ = tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org");
+        emit mxidErrorChanged();
+        return;
+    }
+
     http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
         if (err) {
             if (err->status_code == 404) {
@@ -318,107 +171,78 @@ LoginPage::checkHomeserverVersion()
               if (err || flows.flows.empty())
                   emit versionOkCb(true, false);
 
-              bool ssoSupported_      = false;
-              bool passwordSupported_ = false;
+              bool ssoSupported      = false;
+              bool passwordSupported = false;
               for (const auto &flow : flows.flows) {
                   if (flow.type == mtx::user_interactive::auth_types::sso) {
-                      ssoSupported_ = true;
+                      ssoSupported = true;
                   } else if (flow.type == mtx::user_interactive::auth_types::password) {
-                      passwordSupported_ = true;
+                      passwordSupported = true;
                   }
               }
-              emit versionOkCb(passwordSupported_, ssoSupported_);
+              emit versionOkCb(passwordSupported, ssoSupported);
           });
     });
 }
 
-void
-LoginPage::onServerAddressEntered()
-{
-    error_label_->setText(QLatin1String(""));
-    http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
-    http::client()->set_server(serverInput_->text().toStdString());
-    checkHomeserverVersion();
-
-    serverLayout_->removeWidget(errorIcon_);
-    errorIcon_->hide();
-    serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
-    spinner_->start();
-}
-
 void
 LoginPage::versionError(const QString &error)
 {
-    showError(error_label_, error);
-    serverInput_->show();
-
-    spinner_->stop();
-    serverLayout_->removeWidget(spinner_);
-    serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight);
-    errorIcon_->show();
-    matrixidLayout_->removeWidget(spinner_);
+    showError(error);
+
+    homeserverNeeded_ = true;
+    lookingUpHs_      = false;
+    homeserverValid_  = false;
+    emit lookingUpHsChanged();
+    emit versionLookedUp();
 }
 
 void
-LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
+LoginPage::versionOk(bool passwordSupported, bool ssoSupported)
 {
-    passwordSupported = passwordSupported_;
-    ssoSupported      = ssoSupported_;
-
-    serverLayout_->removeWidget(spinner_);
-    matrixidLayout_->removeWidget(spinner_);
-    spinner_->stop();
-
-    password_input_->setVisible(passwordSupported);
-    password_input_->setEnabled(passwordSupported);
-    sso_login_button_->setVisible(ssoSupported);
-    login_button_->setVisible(passwordSupported);
-
-    if (serverInput_->isVisible())
-        serverInput_->hide();
+    passwordSupported_ = passwordSupported;
+    ssoSupported_      = ssoSupported;
+
+    lookingUpHs_     = false;
+    homeserverValid_ = true;
+    emit homeserverChanged();
+    emit lookingUpHsChanged();
+    emit versionLookedUp();
 }
 
 void
-LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
+LoginPage::onLoginButtonClicked(LoginMethod loginMethod,
+                                QString userid,
+                                QString password,
+                                QString deviceName)
 {
-    error_label_->setText(QLatin1String(""));
-    User user;
+    clearErrors();
 
-    if (!matrixid_input_->isValid()) {
-        error_matrixid_label_->show();
-        showError(error_matrixid_label_,
-                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
-        return;
-    } else {
-        error_matrixid_label_->setText(QLatin1String(""));
-        error_matrixid_label_->hide();
-    }
+    User user;
 
     try {
-        user = parse<User>(matrixid_input_->text().toStdString());
+        user = parse<User>(userid.toStdString());
     } catch (const std::exception &) {
-        showError(error_matrixid_label_,
-                  tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org"));
+        mxidError_ = tr("You have entered an invalid Matrix ID  e.g @joe:matrix.org");
+        emit mxidErrorChanged();
         return;
     }
 
     if (loginMethod == LoginMethod::Password) {
-        if (password_input_->text().isEmpty())
-            return showError(error_label_, tr("Empty password"));
+        if (password.isEmpty())
+            return showError(tr("Empty password"));
 
         http::client()->login(
           user.localpart(),
-          password_input_->text().toStdString(),
-          deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
-                                                  : deviceName_->text().toStdString(),
+          password.toStdString(),
+          deviceName.trimmed().isEmpty() ? initialDeviceName_() : deviceName.toStdString(),
           [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
               if (err) {
                   auto error = err->matrix_error.error;
                   if (error.empty())
                       error = err->parse_error;
 
-                  showErrorMessage(error_label_, QString::fromStdString(error));
-                  emit errorOccurred();
+                  showError(QString::fromStdString(error));
                   return;
               }
 
@@ -432,34 +256,33 @@ LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
           });
     } else {
         auto sso = new SSOHandler();
-        connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) {
-            mtx::requests::Login req{};
-            req.token     = token;
-            req.type      = mtx::user_interactive::auth_types::token;
-            req.device_id = deviceName_->text().trimmed().isEmpty()
-                              ? initialDeviceName()
-                              : deviceName_->text().toStdString();
-            http::client()->login(
-              req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
-                  if (err) {
-                      showErrorMessage(error_label_,
-                                       QString::fromStdString(err->matrix_error.error));
-                      emit errorOccurred();
-                      return;
-                  }
-
-                  if (res.well_known) {
-                      http::client()->set_server(res.well_known->homeserver.base_url);
-                      nhlog::net()->info("Login requested to user server: " +
-                                         res.well_known->homeserver.base_url);
-                  }
-
-                  emit loginOk(res);
-              });
-            sso->deleteLater();
-        });
+        connect(
+          sso, &SSOHandler::ssoSuccess, this, [this, sso, userid, deviceName](std::string token) {
+              mtx::requests::Login req{};
+              req.token = token;
+              req.type  = mtx::user_interactive::auth_types::token;
+              req.device_id =
+                deviceName.trimmed().isEmpty() ? initialDeviceName_() : deviceName.toStdString();
+              http::client()->login(
+                req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+                    if (err) {
+                        showError(QString::fromStdString(err->matrix_error.error));
+                        emit errorOccurred();
+                        return;
+                    }
+
+                    if (res.well_known) {
+                        http::client()->set_server(res.well_known->homeserver.base_url);
+                        nhlog::net()->info("Login requested to user server: " +
+                                           res.well_known->homeserver.base_url);
+                    }
+
+                    emit loginOk(res);
+                });
+              sso->deleteLater();
+          });
         connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
-            showErrorMessage(error_label_, tr("SSO login failed"));
+            showError(tr("SSO login failed"));
             emit errorOccurred();
             sso->deleteLater();
         });
@@ -468,37 +291,6 @@ LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
           QString::fromStdString(http::client()->login_sso_redirect(sso->url())));
     }
 
-    emit loggingIn();
-}
-
-void
-LoginPage::reset()
-{
-    matrixid_input_->clear();
-    password_input_->clear();
-    password_input_->show();
-    serverInput_->clear();
-
-    spinner_->stop();
-    errorIcon_->hide();
-    serverLayout_->removeWidget(spinner_);
-    serverLayout_->removeWidget(errorIcon_);
-    matrixidLayout_->removeWidget(spinner_);
-
-    inferredServerAddress_.clear();
-}
-
-void
-LoginPage::onBackButtonClicked()
-{
-    emit backButtonClicked();
-}
-
-void
-LoginPage::paintEvent(QPaintEvent *)
-{
-    QStyleOption opt;
-    opt.initFrom(this);
-    QPainter p(this);
-    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+    loggingIn_ = true;
+    emit loggingInChanged();
 }
diff --git a/src/LoginPage.h b/src/LoginPage.h
index fbfd87103dd6babdcb3e372f467a0b92444dc128..9a1b96530d585f1ab8d2b242d9e2cb75aa400325 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -6,16 +6,7 @@
 
 #pragma once
 
-#include <QWidget>
-
-class FlatButton;
-class LoadingIndicator;
-class OverlayModal;
-class RaisedButton;
-class TextField;
-class QLabel;
-class QVBoxLayout;
-class QHBoxLayout;
+#include <QObject>
 
 namespace mtx {
 namespace responses {
@@ -23,24 +14,77 @@ struct Login;
 }
 }
 
-class LoginPage : public QWidget
+class LoginPage : public QObject
 {
     Q_OBJECT
 
+    Q_PROPERTY(QString mxid READ mxid WRITE setMxid NOTIFY matrixIdChanged)
+    Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
+
+    Q_PROPERTY(QString mxidError READ mxidError NOTIFY mxidErrorChanged)
+    Q_PROPERTY(QString error READ error NOTIFY errorOccurred)
+    Q_PROPERTY(bool lookingUpHs READ lookingUpHs NOTIFY lookingUpHsChanged)
+    Q_PROPERTY(bool homeserverValid READ homeserverValid NOTIFY lookingUpHsChanged)
+    Q_PROPERTY(bool loggingIn READ loggingIn NOTIFY loggingInChanged)
+    Q_PROPERTY(bool passwordSupported READ passwordSupported NOTIFY versionLookedUp)
+    Q_PROPERTY(bool ssoSupported READ ssoSupported NOTIFY versionLookedUp)
+    Q_PROPERTY(bool homeserverNeeded READ homeserverNeeded NOTIFY versionLookedUp)
+
 public:
     enum class LoginMethod
     {
         Password,
         SSO,
     };
+    Q_ENUM(LoginMethod)
+
+    LoginPage(QObject *parent = nullptr);
+
+    Q_INVOKABLE QString initialDeviceName() const
+    {
+        return QString::fromStdString(initialDeviceName_());
+    }
 
-    LoginPage(QWidget *parent = nullptr);
+    bool lookingUpHs() const { return lookingUpHs_; }
+    bool loggingIn() const { return loggingIn_; }
+    bool passwordSupported() const { return passwordSupported_; }
+    bool ssoSupported() const { return ssoSupported_; }
+    bool homeserverNeeded() const { return homeserverNeeded_; }
+    bool homeserverValid() const { return homeserverValid_; }
 
-    void reset();
+    QString homeserver() { return homeserver_; }
+    QString mxid() { return mxid_; }
+
+    QString error() { return error_; }
+    QString mxidError() { return mxidError_; }
+
+    void setHomeserver(QString hs);
+    void setMxid(QString id)
+    {
+        if (id != mxid_) {
+            mxid_ = id;
+            emit matrixIdChanged();
+            onMatrixIdEntered();
+        }
+    }
+
+    static std::string initialDeviceName_()
+    {
+#if defined(Q_OS_MAC)
+        return "Nheko on macOS";
+#elif defined(Q_OS_LINUX)
+        return "Nheko on Linux";
+#elif defined(Q_OS_WIN)
+        return "Nheko on Windows";
+#elif defined(Q_OS_FREEBSD)
+        return "Nheko on FreeBSD";
+#else
+        return "Nheko";
+#endif
+    }
 
 signals:
-    void backButtonClicked();
-    void loggingIn();
+    void loggingInChanged();
     void errorOccurred();
 
     //! Used to trigger the corresponding slot outside of the main thread.
@@ -48,28 +92,26 @@ signals:
     void versionOkCb(bool passwordSupported, bool ssoSupported);
 
     void loginOk(const mtx::responses::Login &res);
-    void showErrorMessage(QLabel *label, const QString &msg);
 
-protected:
-    void paintEvent(QPaintEvent *event) override;
+    void onServerAddressEntered();
+
+    void matrixIdChanged();
+    void homeserverChanged();
+
+    void mxidErrorChanged();
+    void lookingUpHsChanged();
+    void versionLookedUp();
+    void versionLookupFinished();
 
 public slots:
     // Displays errors produced during the login.
     void showError(const QString &msg);
-    void showError(QLabel *label, const QString &msg);
-
-private slots:
-    // Callback for the back button.
-    void onBackButtonClicked();
 
     // Callback for the login button.
-    void onLoginButtonClicked(LoginMethod loginMethod);
-
-    // Callback for probing the server found in the mxid
-    void onMatrixIdEntered();
-
-    // Callback for probing the manually entered server
-    void onServerAddressEntered();
+    void onLoginButtonClicked(LoginMethod loginMethod,
+                              QString userid,
+                              QString password,
+                              QString deviceName);
 
     // Callback for errors produced during server probing
     void versionError(const QString &error_message);
@@ -78,48 +120,28 @@ private slots:
 
 private:
     void checkHomeserverVersion();
-    std::string initialDeviceName()
+    void onMatrixIdEntered();
+    void clearErrors()
     {
-#if defined(Q_OS_MAC)
-        return "Nheko on macOS";
-#elif defined(Q_OS_LINUX)
-        return "Nheko on Linux";
-#elif defined(Q_OS_WIN)
-        return "Nheko on Windows";
-#elif defined(Q_OS_FREEBSD)
-        return "Nheko on FreeBSD";
-#else
-        return "Nheko";
-#endif
+        error_.clear();
+        mxidError_.clear();
+        emit errorOccurred();
+        emit mxidErrorChanged();
     }
 
-    QVBoxLayout *top_layout_;
-
-    QHBoxLayout *top_bar_layout_;
-    QHBoxLayout *logo_layout_;
-    QHBoxLayout *button_layout_;
-
-    QLabel *logo_;
-    QLabel *error_label_;
-    QLabel *error_matrixid_label_;
-
-    QHBoxLayout *serverLayout_;
-    QHBoxLayout *matrixidLayout_;
-    LoadingIndicator *spinner_;
-    QLabel *errorIcon_;
     QString inferredServerAddress_;
 
-    FlatButton *back_button_;
-    RaisedButton *login_button_, *sso_login_button_;
+    QString mxid_;
+    QString homeserver_;
+
+    QString mxidError_;
+    QString error_;
 
-    QWidget *form_widget_;
-    QHBoxLayout *form_wrapper_;
-    QVBoxLayout *form_layout_;
+    bool passwordSupported_ = true;
+    bool ssoSupported_      = false;
 
-    TextField *matrixid_input_;
-    TextField *password_input_;
-    TextField *deviceName_;
-    TextField *serverInput_;
-    bool passwordSupported = true;
-    bool ssoSupported      = false;
+    bool lookingUpHs_      = false;
+    bool loggingIn_        = false;
+    bool homeserverNeeded_ = false;
+    bool homeserverValid_  = false;
 };
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 5bfce89e77161c26123e83eb56c9e17a5e35d0a9..83504d866a4bc3e11c58a022a8f14e3893a73af7 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -5,91 +5,87 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 #include <QApplication>
-#include <QLayout>
 #include <QMessageBox>
-#include <QPluginLoader>
-#include <QShortcut>
 
 #include <mtx/requests.hpp>
 #include <mtx/responses/login.hpp>
 
+#include "BlurhashProvider.h"
 #include "Cache.h"
 #include "Cache_p.h"
 #include "ChatPage.h"
+#include "Clipboard.h"
+#include "ColorImageProvider.h"
+#include "CombinedImagePackModel.h"
+#include "CompletionProxyModel.h"
 #include "Config.h"
+#include "EventAccessors.h"
+#include "ImagePackListModel.h"
+#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 "ReadReceiptsModel.h"
 #include "RegisterPage.h"
+#include "RoomDirectoryModel.h"
+#include "RoomsModel.h"
+#include "SingleImagePackModel.h"
 #include "TrayIcon.h"
 #include "UserSettingsPage.h"
+#include "UsersModel.h"
 #include "Utils.h"
-#include "WelcomePage.h"
-#include "ui/LoadingIndicator.h"
-#include "ui/OverlayModal.h"
-#include "ui/SnackBar.h"
+#include "emoji/EmojiModel.h"
+#include "emoji/Provider.h"
+#include "encryption/DeviceVerificationFlow.h"
+#include "encryption/SelfVerificationStatus.h"
+#include "timeline/DelegateChooser.h"
+#include "timeline/TimelineViewManager.h"
+#include "ui/MxcAnimatedImage.h"
+#include "ui/MxcMediaProxy.h"
+#include "ui/NhekoCursorShape.h"
+#include "ui/NhekoDropArea.h"
+#include "ui/NhekoGlobalObject.h"
+#include "ui/UIA.h"
 #include "voip/WebRTCSession.h"
 
 #include "dialogs/CreateRoom.h"
 
+Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
+Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
+Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
+
 MainWindow *MainWindow::instance_ = nullptr;
 
-MainWindow::MainWindow(QWidget *parent)
-  : QMainWindow(parent)
+MainWindow::MainWindow(QWindow *parent)
+  : QQuickView(parent)
   , userSettings_{UserSettings::instance()}
 {
     instance_ = this;
 
-    QMainWindow::setWindowTitle(0);
+    MainWindow::setWindowTitle(0);
     setObjectName(QStringLiteral("MainWindow"));
-
-    modal_ = new OverlayModal(this);
-
+    setResizeMode(QQuickView::SizeRootObjectToView);
+    setMinimumHeight(400);
+    setMinimumWidth(400);
     restoreWindowSize();
 
-    QFont font;
-    font.setStyleStrategy(QFont::PreferAntialias);
-    setFont(font);
+    chat_page_ = new ChatPage(userSettings_, this);
+    registerQmlTypes();
+
+    setColor(Theme::paletteFromTheme(userSettings_->theme()).window().color());
+    setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml")));
 
     trayIcon_ = new TrayIcon(QStringLiteral(":/logos/nheko.svg"), this);
 
-    welcome_page_  = new WelcomePage(this);
-    login_page_    = new LoginPage(this);
-    register_page_ = new RegisterPage(this);
-    chat_page_     = new ChatPage(userSettings_, this);
-
-    // Initialize sliding widget manager.
-    pageStack_ = new QStackedWidget(this);
-    pageStack_->addWidget(welcome_page_);
-    pageStack_->addWidget(login_page_);
-    pageStack_->addWidget(register_page_);
-    pageStack_->addWidget(chat_page_);
-
-    setCentralWidget(pageStack_);
-
-    connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
-    connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
-
-    connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
-    connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar);
-    connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar);
-    connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
-    connect(
-      register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
-    connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
-
-    connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage);
-    connect(
-      chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar);
+    connect(chat_page_, &ChatPage::closing, this, [this] { switchToLoginPage(""); });
     connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
     connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
-    connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) {
-        login_page_->showError(msg);
-        showLoginPage();
-    });
+    connect(chat_page_, &ChatPage::showLoginPage, this, &MainWindow::switchToLoginPage);
+    connect(chat_page_, &ChatPage::showNotification, this, &MainWindow::showNotification);
 
     connect(userSettings_.get(), &UserSettings::trayChanged, trayIcon_, &TrayIcon::setVisible);
     connect(trayIcon_,
@@ -97,20 +93,6 @@ MainWindow::MainWindow(QWidget *parent)
             this,
             SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
 
-    connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar()));
-
-    connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged);
-
-    connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
-        http::client()->set_user(res.user_id);
-        showChatPage();
-    });
-
-    connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
-
-    QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
-    connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
-
     trayIcon_->setVisible(userSettings_->tray());
 
     // load cache on event loop
@@ -133,11 +115,171 @@ MainWindow::MainWindow(QWidget *parent)
                                       user_id.toStdString());
             }
 
+            nhlog::ui()->info("User already signed in, showing chat page");
             showChatPage();
         }
     });
 }
 
+void
+MainWindow::registerQmlTypes()
+{
+    qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
+    qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
+    qRegisterMetaType<CombinedImagePackModel *>();
+    qRegisterMetaType<mtx::events::collections::TimelineEvents>();
+    qRegisterMetaType<std::vector<DeviceInfo>>();
+
+    qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
+
+    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<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
+    qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
+    qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
+    qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
+    qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
+    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());
+
+    qRegisterMetaType<mtx::events::collections::TimelineEvents>();
+    qRegisterMetaType<std::vector<DeviceInfo>>();
+
+    qmlRegisterUncreatableType<FilteredCommunitiesModel>(
+      "im.nheko",
+      1,
+      0,
+      "FilteredCommunitiesModel",
+      QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
+
+    qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
+    qmlRegisterUncreatableType<emoji::Emoji>(
+      "im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
+    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());
+
+    imgProvider = new MxcImageProvider();
+    engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider);
+    engine()->addImageProvider(QStringLiteral("colorimage"), new ColorImageProvider());
+    engine()->addImageProvider(QStringLiteral("blurhash"), new BlurhashProvider());
+    if (JdenticonProvider::isAvailable())
+        engine()->addImageProvider(QStringLiteral("jdenticon"), new JdenticonProvider());
+
+    QObject::connect(engine(), &QQmlEngine::quit, &QGuiApplication::quit);
+}
+
 void
 MainWindow::setWindowTitle(int notificationCount)
 {
@@ -148,20 +290,19 @@ MainWindow::setWindowTitle(int notificationCount)
     if (notificationCount > 0) {
         name.append(QString{QStringLiteral(" (%1)")}.arg(notificationCount));
     }
-    QMainWindow::setWindowTitle(name);
+    QQuickView::setTitle(name);
 }
 
 bool
 MainWindow::event(QEvent *event)
 {
     auto type = event->type();
-    if (type == QEvent::WindowActivate) {
-        emit focusChanged(true);
-    } else if (type == QEvent::WindowDeactivate) {
-        emit focusChanged(false);
+
+    if (type == QEvent::Close) {
+        closeEvent(static_cast<QCloseEvent *>(event));
     }
 
-    return QMainWindow::event(event);
+    return QQuickView::event(event);
 }
 
 void
@@ -188,31 +329,6 @@ MainWindow::saveCurrentWindowSize()
     settings->setValue(QStringLiteral("window/height"), current.height());
 }
 
-void
-MainWindow::removeOverlayProgressBar()
-{
-    QTimer *timer = new QTimer(this);
-    timer->setSingleShot(true);
-
-    connect(timer, &QTimer::timeout, this, [this, timer]() {
-        timer->deleteLater();
-
-        if (modal_)
-            modal_->hide();
-
-        if (spinner_)
-            spinner_->stop();
-    });
-
-    // FIXME:  Snackbar doesn't work if it's initialized in the constructor.
-    QTimer::singleShot(0, this, [this]() {
-        snackBar_ = new SnackBar(this);
-        connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage);
-    });
-
-    timer->start(50);
-}
-
 void
 MainWindow::showChatPage()
 {
@@ -227,19 +343,13 @@ MainWindow::showChatPage()
     userSettings_.data()->setDeviceId(device_id);
     userSettings_.data()->setHomeserver(homeserver);
 
-    showOverlayProgressBar();
-
-    pageStack_->setCurrentWidget(chat_page_);
-
-    pageStack_->removeWidget(welcome_page_);
-    pageStack_->removeWidget(login_page_);
-    pageStack_->removeWidget(register_page_);
-
-    login_page_->reset();
     chat_page_->bootstrap(userid, homeserver, token);
     connect(cache::client(), &Cache::databaseReady, this, &MainWindow::secretsChanged);
     connect(cache::client(), &Cache::secretChanged, this, &MainWindow::secretsChanged);
+
     emit reload();
+    nhlog::ui()->info("Switching to chat page");
+    emit switchToChatPage();
 }
 
 void
@@ -247,7 +357,7 @@ MainWindow::closeEvent(QCloseEvent *event)
 {
     if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
         if (QMessageBox::question(
-              this, QStringLiteral("nheko"), QStringLiteral("A call is in progress. Quit?")) !=
+              nullptr, QStringLiteral("nheko"), QStringLiteral("A call is in progress. Quit?")) !=
             QMessageBox::Yes) {
             event->ignore();
             return;
@@ -289,23 +399,11 @@ MainWindow::hasActiveUser()
            settings->contains(prefix + "auth/user_id");
 }
 
-void
-MainWindow::showOverlayProgressBar()
-{
-    spinner_ = new LoadingIndicator(this);
-    spinner_->setFixedHeight(100);
-    spinner_->setFixedWidth(100);
-    spinner_->setObjectName(QStringLiteral("ChatPageLoadSpinner"));
-    spinner_->start();
-
-    showSolidOverlayModal(spinner_);
-}
-
 void
 MainWindow::openCreateRoomDialog(
   std::function<void(const mtx::requests::CreateRoom &request)> callback)
 {
-    auto dialog = new dialogs::CreateRoom(this);
+    auto dialog = new dialogs::CreateRoom(nullptr);
     connect(dialog,
             &dialogs::CreateRoom::createRoom,
             this,
@@ -314,76 +412,19 @@ MainWindow::openCreateRoomDialog(
     showDialog(dialog);
 }
 
-void
-MainWindow::showTransparentOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
-{
-    modal_->setWidget(content);
-    modal_->setColor(QColor(30, 30, 30, 150));
-    modal_->setDismissible(true);
-    modal_->setContentAlignment(flags);
-    modal_->raise();
-    modal_->show();
-}
-
-void
-MainWindow::showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
-{
-    modal_->setWidget(content);
-    modal_->setColor(QColor(30, 30, 30));
-    modal_->setDismissible(false);
-    modal_->setContentAlignment(flags);
-    modal_->raise();
-    modal_->show();
-}
-
-bool
-MainWindow::hasActiveDialogs() const
-{
-    return modal_ && modal_->isVisible();
-}
-
 bool
 MainWindow::pageSupportsTray() const
 {
-    return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible();
-}
-
-void
-MainWindow::hideOverlay()
-{
-    if (modal_)
-        modal_->hide();
+    return !http::client()->access_token().empty();
 }
 
 inline void
 MainWindow::showDialog(QWidget *dialog)
 {
-    utils::centerWidget(dialog, this);
+    dialog->setWindowFlags(Qt::WindowType::Dialog | Qt::WindowType::WindowCloseButtonHint |
+                           Qt::WindowType::WindowTitleHint);
     dialog->raise();
     dialog->show();
-}
-
-void
-MainWindow::showWelcomePage()
-{
-    removeOverlayProgressBar();
-    pageStack_->addWidget(welcome_page_);
-    pageStack_->setCurrentWidget(welcome_page_);
-}
-
-void
-MainWindow::showLoginPage()
-{
-    if (modal_)
-        modal_->hide();
-
-    pageStack_->addWidget(login_page_);
-    pageStack_->setCurrentWidget(login_page_);
-}
-
-void
-MainWindow::showRegisterPage()
-{
-    pageStack_->addWidget(register_page_);
-    pageStack_->setCurrentWidget(register_page_);
+    utils::centerWidget(dialog, this);
+    dialog->window()->windowHandle()->setTransientParent(this);
 }
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 458eb05497e3e5bb4c06bfb173fdecea88d3b85d..7bc94328bc535aaaeb655ef2b298eb2cd8a134fa 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -8,26 +8,21 @@
 
 #include <functional>
 
-#include <QMainWindow>
+#include <QQuickView>
 #include <QSharedPointer>
-#include <QStackedWidget>
 #include <QSystemTrayIcon>
 
 #include "UserSettingsPage.h"
-#include "ui/OverlayModal.h"
 
 #include "jdenticoninterface.h"
 
 class ChatPage;
 class RegisterPage;
-class LoginPage;
 class WelcomePage;
 
-class LoadingIndicator;
-class OverlayModal;
-class SnackBar;
 class TrayIcon;
 class UserSettings;
+class MxcImageProvider;
 
 namespace mtx {
 namespace requests {
@@ -42,17 +37,12 @@ class MemberList;
 class ReCaptcha;
 }
 
-class MainWindow : public QMainWindow
+class MainWindow : public QQuickView
 {
     Q_OBJECT
 
-    Q_PROPERTY(int x READ x CONSTANT)
-    Q_PROPERTY(int y READ y CONSTANT)
-    Q_PROPERTY(int width READ width CONSTANT)
-    Q_PROPERTY(int height READ height CONSTANT)
-
 public:
-    explicit MainWindow(QWidget *parent = nullptr);
+    explicit MainWindow(QWindow *parent = nullptr);
 
     static MainWindow *instance() { return instance_; }
     void saveCurrentWindowSize();
@@ -61,69 +51,51 @@ public:
     openCreateRoomDialog(std::function<void(const mtx::requests::CreateRoom &request)> callback);
     void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
 
-    void hideOverlay();
-    void showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter);
-    void
-    showTransparentOverlayModal(QWidget *content,
-                                QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop | Qt::AlignHCenter);
+    MxcImageProvider *imageProvider() { return imgProvider; }
+
+    //! Show the chat page and start communicating with the given access token.
+    void showChatPage();
 
 protected:
-    void closeEvent(QCloseEvent *event) override;
+    void closeEvent(QCloseEvent *event);
     bool event(QEvent *event) override;
 
 private slots:
     //! Handle interaction with the tray icon.
     void iconActivated(QSystemTrayIcon::ActivationReason reason);
 
-    //! Show the welcome page in the main window.
-    void showWelcomePage();
-
-    //! Show the login page in the main window.
-    void showLoginPage();
-
-    //! Show the register page in the main window.
-    void showRegisterPage();
-
-    //! Show the chat page and start communicating with the given access token.
-    void showChatPage();
-
-    void showOverlayProgressBar();
-    void removeOverlayProgressBar();
-
     virtual void setWindowTitle(int notificationCount);
 
 signals:
-    void focusChanged(const bool focused);
     void reload();
     void secretsChanged();
 
+    void showNotification(QString msg);
+
+    void switchToChatPage();
+    void switchToWelcomePage();
+    void switchToLoginPage(QString error);
+
 private:
     void showDialog(QWidget *dialog);
     bool hasActiveUser();
     void restoreWindowSize();
-    //! Check if there is an open dialog.
-    bool hasActiveDialogs() const;
     //! Check if the current page supports the "minimize to tray" functionality.
     bool pageSupportsTray() const;
 
+    void registerQmlTypes();
+
     static MainWindow *instance_;
 
     //! The initial welcome screen.
     WelcomePage *welcome_page_;
-    //! The login screen.
-    LoginPage *login_page_;
     //! The register page.
     RegisterPage *register_page_;
-    //! A stacked widget that handles the transitions between widgets.
-    QStackedWidget *pageStack_;
     //! The main chat area.
     ChatPage *chat_page_;
     QSharedPointer<UserSettings> userSettings_;
     //! Tray icon that shows the unread message count.
     TrayIcon *trayIcon_;
-    //! Notifications display.
-    SnackBar *snackBar_ = nullptr;
-    //! Overlay modal used to project other widgets.
-    OverlayModal *modal_       = nullptr;
-    LoadingIndicator *spinner_ = nullptr;
+
+    MxcImageProvider *imgProvider = nullptr;
 };
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index d089ac966d087092269a103a1418be27393549e7..5b2ebc78b8d41920117ed63cdd8bc6c20ffe0d83 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -4,312 +4,83 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-#include <QInputDialog>
-#include <QLabel>
-#include <QMetaType>
-#include <QPainter>
-#include <QStyleOption>
-#include <QTimer>
-#include <QtMath>
-
+#include <mtx/responses/common.hpp>
 #include <mtx/responses/register.hpp>
 #include <mtx/responses/well-known.hpp>
 #include <mtxclient/http/client.hpp>
 
 #include "Config.h"
 #include "Logging.h"
+#include "LoginPage.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "RegisterPage.h"
-#include "ui/FlatButton.h"
-#include "ui/RaisedButton.h"
-#include "ui/TextField.h"
 #include "ui/UIA.h"
 
-#include "dialogs/FallbackAuth.h"
-#include "dialogs/ReCaptcha.h"
-
-Q_DECLARE_METATYPE(mtx::user_interactive::Unauthorized)
-Q_DECLARE_METATYPE(mtx::user_interactive::Auth)
-
-RegisterPage::RegisterPage(QWidget *parent)
-  : QWidget(parent)
-{
-    qRegisterMetaType<mtx::user_interactive::Unauthorized>();
-    qRegisterMetaType<mtx::user_interactive::Auth>();
-    top_layout_ = new QVBoxLayout();
-
-    back_layout_ = new QHBoxLayout();
-    back_layout_->setSpacing(0);
-    back_layout_->setContentsMargins(5, 5, -1, -1);
-
-    back_button_ = new FlatButton(this);
-    back_button_->setMinimumSize(QSize(30, 30));
-
-    QIcon icon;
-    icon.addFile(QStringLiteral(":/icons/icons/ui/angle-arrow-left.svg"));
-
-    back_button_->setIcon(icon);
-    back_button_->setIconSize(QSize(32, 32));
-
-    back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
-    back_layout_->addStretch(1);
-
-    QIcon logo;
-    logo.addFile(QStringLiteral(":/logos/register.png"));
-
-    logo_ = new QLabel(this);
-    logo_->setPixmap(logo.pixmap(128));
-
-    logo_layout_ = new QHBoxLayout();
-    logo_layout_->setContentsMargins(0, 0, 0, 0);
-    logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
-
-    form_wrapper_ = new QHBoxLayout();
-    form_widget_  = new QWidget();
-    form_widget_->setMinimumSize(QSize(350, 300));
-
-    form_layout_ = new QVBoxLayout();
-    form_layout_->setSpacing(20);
-    form_layout_->setContentsMargins(0, 0, 0, 40);
-    form_widget_->setLayout(form_layout_);
-
-    form_wrapper_->addStretch(1);
-    form_wrapper_->addWidget(form_widget_);
-    form_wrapper_->addStretch(1);
-
-    username_input_ = new TextField();
-    username_input_->setLabel(tr("Username"));
-    username_input_->setRegexp(QRegularExpression(QStringLiteral("[a-z0-9._=/-]+")));
-    username_input_->setToolTip(tr("The username must not be empty, and must contain only the "
-                                   "characters a-z, 0-9, ., _, =, -, and /."));
-
-    password_input_ = new TextField();
-    password_input_->setLabel(tr("Password"));
-    password_input_->setRegexp(QRegularExpression(QStringLiteral("^.{8,}$")));
-    password_input_->setEchoMode(QLineEdit::Password);
-    password_input_->setToolTip(tr("Please choose a secure password. The exact requirements "
-                                   "for password strength may depend on your server."));
-
-    password_confirmation_ = new TextField();
-    password_confirmation_->setLabel(tr("Password confirmation"));
-    password_confirmation_->setEchoMode(QLineEdit::Password);
-
-    server_input_ = new TextField();
-    server_input_->setLabel(tr("Homeserver"));
-    server_input_->setRegexp(QRegularExpression(QStringLiteral(".+")));
-    server_input_->setToolTip(
-      tr("A server that allows registration. Since matrix is decentralized, you need to first "
-         "find a server you can register on or host your own."));
-
-    error_username_label_ = new QLabel(this);
-    error_username_label_->setWordWrap(true);
-    error_username_label_->hide();
-
-    error_password_label_ = new QLabel(this);
-    error_password_label_->setWordWrap(true);
-    error_password_label_->hide();
-
-    error_password_confirmation_label_ = new QLabel(this);
-    error_password_confirmation_label_->setWordWrap(true);
-    error_password_confirmation_label_->hide();
-
-    error_server_label_ = new QLabel(this);
-    error_server_label_->setWordWrap(true);
-    error_server_label_->hide();
-
-    form_layout_->addWidget(username_input_, Qt::AlignHCenter);
-    form_layout_->addWidget(error_username_label_, Qt::AlignHCenter);
-    form_layout_->addWidget(password_input_, Qt::AlignHCenter);
-    form_layout_->addWidget(error_password_label_, Qt::AlignHCenter);
-    form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter);
-    form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter);
-    form_layout_->addWidget(server_input_, Qt::AlignHCenter);
-    form_layout_->addWidget(error_server_label_, Qt::AlignHCenter);
-
-    button_layout_ = new QHBoxLayout();
-    button_layout_->setSpacing(0);
-    button_layout_->setContentsMargins(0, 0, 0, 0);
-
-    error_label_ = new QLabel(this);
-    error_label_->setWordWrap(true);
-
-    register_button_ = new RaisedButton(tr("REGISTER"), this);
-    register_button_->setMinimumSize(350, 65);
-    register_button_->setFontSize(conf::btn::fontSize);
-    register_button_->setCornerRadius(conf::btn::cornerRadius);
-
-    button_layout_->addStretch(1);
-    button_layout_->addWidget(register_button_);
-    button_layout_->addStretch(1);
-
-    top_layout_->addLayout(back_layout_);
-    top_layout_->addLayout(logo_layout_);
-    top_layout_->addLayout(form_wrapper_);
-    top_layout_->addStretch(1);
-    top_layout_->addLayout(button_layout_);
-    top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
-    top_layout_->addStretch(1);
-    setLayout(top_layout_);
-
-    connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
-    connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
-
-    connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-    connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
-    connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-    connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
-    connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-    connect(password_confirmation_,
-            &TextField::editingFinished,
-            this,
-            &RegisterPage::checkPasswordConfirmation);
-    connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
-    connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
-
-    connect(
-      this,
-      &RegisterPage::serverError,
-      this,
-      [this](const QString &msg) {
-          server_input_->setValid(false);
-          showError(error_server_label_, msg);
-      },
-      Qt::QueuedConnection);
-
-    connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
-    connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
-    connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
-}
-
-void
-RegisterPage::onBackButtonClicked()
+RegisterPage::RegisterPage(QObject *parent)
+  : QObject(parent)
 {
-    emit backButtonClicked();
+    connect(this, &RegisterPage::registerOk, this, [] { MainWindow::instance()->showChatPage(); });
 }
 
 void
-RegisterPage::showError(const QString &msg)
+RegisterPage::setError(QString err)
 {
-    emit errorOccurred();
-    auto rect  = QFontMetrics(font()).boundingRect(msg);
-    int width  = rect.width();
-    int height = rect.height();
-    error_label_->setFixedHeight(qCeil(width / 200.0) * height);
-    error_label_->setText(msg);
+    registrationError_ = err;
+    emit errorChanged();
+    registering_ = false;
+    emit registeringChanged();
 }
-
 void
-RegisterPage::showError(QLabel *label, const QString &msg)
-{
-    emit errorOccurred();
-    auto rect  = QFontMetrics(font()).boundingRect(msg);
-    int width  = rect.width();
-    int height = rect.height();
-    label->setFixedHeight((int)qCeil(width / 200.0) * height);
-    label->setText(msg);
-    label->show();
-}
-
-bool
-RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
+RegisterPage::setHsError(QString err)
 {
-    if (t_field->isValid()) {
-        label->hide();
-        return true;
-    } else {
-        showError(label, msg);
-        return false;
-    }
+    hsError_ = err;
+    emit hsErrorChanged();
+    lookingUpHs_ = false;
+    emit lookingUpHsChanged();
 }
 
-bool
-RegisterPage::checkUsername()
+QString
+RegisterPage::initialDeviceName() const
 {
-    return checkOneField(error_username_label_,
-                         username_input_,
-                         tr("The username must not be empty, and must contain only the "
-                            "characters a-z, 0-9, ., _, =, -, and /."));
-}
-
-bool
-RegisterPage::checkPassword()
-{
-    return checkOneField(
-      error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
-}
-
-bool
-RegisterPage::checkPasswordConfirmation()
-{
-    if (password_input_->text() == password_confirmation_->text()) {
-        error_password_confirmation_label_->hide();
-        password_confirmation_->setValid(true);
-        return true;
-    } else {
-        showError(error_password_confirmation_label_, tr("Passwords don't match"));
-        password_confirmation_->setValid(false);
-        return false;
-    }
-}
-
-bool
-RegisterPage::checkServer()
-{
-    // This doesn't check that the server is reachable,
-    // just that the input is not obviously wrong.
-    return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
+    return QString::fromStdString(LoginPage::initialDeviceName_());
 }
 
 void
-RegisterPage::onRegisterButtonClicked()
+RegisterPage::setServer(QString server)
 {
-    if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
-        auto server = server_input_->text().toStdString();
-
-        http::client()->set_server(server);
-        http::client()->verify_certificates(
-          !UserSettings::instance()->disableCertificateValidation());
+    if (server == lastServer)
+        return;
 
-        // This starts a chain of `emit`s which ends up doing the
-        // registration. Signals are used rather than normal function
-        // calls so that the dialogs used in UIA work correctly.
-        //
-        // The sequence of events looks something like this:
-        //
-        // doKnownLookup
-        //   v
-        // doVersionsCheck
-        //   v
-        // doRegistration -> loops the UIAHandler until complete
+    lastServer = server;
 
-        emit wellKnownLookup();
+    http::client()->set_server(server.toStdString());
+    http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
 
-        emit registering();
-    }
-}
+    hsError_.clear();
+    emit hsErrorChanged();
+    supported_   = false;
+    lookingUpHs_ = true;
+    emit lookingUpHsChanged();
 
-void
-RegisterPage::doWellKnownLookup()
-{
     http::client()->well_known(
       [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
           if (err) {
               if (err->status_code == 404) {
                   nhlog::net()->info("Autodiscovery: No .well-known.");
                   // Check that the homeserver can be reached
-                  emit versionsCheck();
+                  versionsCheck();
                   return;
               }
 
               if (!err->parse_error.empty()) {
-                  emit serverError(tr("Autodiscovery failed. Received malformed response."));
+                  setHsError(tr("Autodiscovery failed. Received malformed response."));
                   nhlog::net()->error("Autodiscovery failed. Received malformed response.");
+                  emit hsErrorChanged();
                   return;
               }
 
-              emit serverError(tr("Autodiscovery failed. Unknown error when "
-                                  "requesting .well-known."));
+              setHsError(tr("Autodiscovery failed. Unknown error when requesting .well-known."));
               nhlog::net()->error("Autodiscovery failed. Unknown error when "
                                   "requesting .well-known. {} {}",
                                   err->status_code,
@@ -319,98 +90,140 @@ RegisterPage::doWellKnownLookup()
 
           nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
           http::client()->set_server(res.homeserver.base_url);
+          emit hsErrorChanged();
           // Check that the homeserver can be reached
-          emit versionsCheck();
+          versionsCheck();
       });
 }
 
 void
-RegisterPage::doVersionsCheck()
+RegisterPage::versionsCheck()
 {
     // Make a request to /_matrix/client/versions to check the address
     // given is a Matrix homeserver.
     http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
         if (err) {
             if (err->status_code == 404) {
-                emit serverError(tr("The required endpoints were not found. Possibly "
-                                    "not a Matrix server."));
+                setHsError(
+                  tr("The required endpoints were not found. Possibly not a Matrix server."));
+                emit hsErrorChanged();
                 return;
             }
 
             if (!err->parse_error.empty()) {
-                emit serverError(tr("Received malformed response. Make sure the homeserver "
-                                    "domain is valid."));
+                setHsError(
+                  tr("Received malformed response. Make sure the homeserver domain is valid."));
+                emit hsErrorChanged();
                 return;
             }
 
-            emit serverError(tr("An unknown error occured. Make sure the "
-                                "homeserver domain is valid."));
+            setHsError(tr("An unknown error occured. Make sure the homeserver domain is valid."));
+            emit hsErrorChanged();
             return;
         }
 
-        // Attempt registration without an `auth` dict
-        emit registration();
+        http::client()->registration(
+          [this](const mtx::responses::Register &, mtx::http::RequestErr e) {
+              nhlog::net()->debug("Registration check: {}", e);
+
+              if (!e) {
+                  setHsError(tr("Server does not support querying registration flows!"));
+                  emit hsErrorChanged();
+                  return;
+              }
+              if (e->status_code != 401) {
+                  setHsError(tr("Server does not support registration."));
+                  emit hsErrorChanged();
+                  return;
+              }
+
+              supported_   = true;
+              lookingUpHs_ = false;
+              emit lookingUpHsChanged();
+          });
     });
 }
 
 void
-RegisterPage::doRegistration()
+RegisterPage::checkUsername(QString name)
+{
+    usernameAvailable_ = usernameUnavailable_ = false;
+    usernameError_.clear();
+    lookingUpUsername_ = true;
+    emit lookingUpUsernameChanged();
+
+    http::client()->register_username_available(
+      name.toStdString(),
+      [this](const mtx::responses::Available &available, mtx::http::RequestErr e) {
+          if (e) {
+              if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_INVALID_USERNAME) {
+                  usernameError_ = tr("Invalid username.");
+              } else if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_USER_IN_USE) {
+                  usernameError_ = tr("Name already in use.");
+              } else if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_EXCLUSIVE) {
+                  usernameError_ = tr("Part of the reserved namespace.");
+              } else {
+              }
+
+              usernameAvailable_   = false;
+              usernameUnavailable_ = true;
+          } else {
+              usernameAvailable_   = available.available;
+              usernameUnavailable_ = !available.available;
+          }
+          lookingUpUsername_ = false;
+          emit lookingUpUsernameChanged();
+      });
+}
+
+void
+RegisterPage::startRegistration(QString username, QString password, QString devicename)
 {
     // These inputs should still be alright, but check just in case
-    if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
-        auto username = username_input_->text().toStdString();
-        auto password = password_input_->text().toStdString();
+    if (!username.isEmpty() && !password.isEmpty() && usernameAvailable_ && supported_) {
+        registrationError_.clear();
+        emit errorChanged();
+        registering_ = true;
+        emit registeringChanged();
+
         connect(UIA::instance(), &UIA::error, this, [this](QString msg) {
-            showError(msg);
+            setError(msg);
             disconnect(UIA::instance(), &UIA::error, this, nullptr);
         });
         http::client()->registration(
-          username,
-          password,
+          username.toStdString(),
+          password.toStdString(),
           ::UIA::instance()->genericHandler(QStringLiteral("Registration")),
-          registrationCb());
-    }
-}
-
-mtx::http::Callback<mtx::responses::Register>
-RegisterPage::registrationCb()
-{
-    // Return a function to be used as the callback when an attempt at
-    // registration is made.
-    return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
-        if (!err) {
-            http::client()->set_user(res.user_id);
-            http::client()->set_access_token(res.access_token);
-            emit registerOk();
-            disconnect(UIA::instance(), &UIA::error, this, nullptr);
-            return;
-        }
-
-        // The server requires registration flows.
-        if (err->status_code == 401) {
-            if (err->matrix_error.unauthorized.flows.empty()) {
-                nhlog::net()->warn("failed to retrieve registration flows: "
-                                   "status_code({}), matrix_error({}) ",
-                                   static_cast<int>(err->status_code),
-                                   err->matrix_error.error);
-                showError(QString::fromStdString(err->matrix_error.error));
-            }
-            return;
-        }
+          [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
+              registering_ = false;
+              emit registeringChanged();
+
+              if (!err) {
+                  http::client()->set_user(res.user_id);
+                  http::client()->set_access_token(res.access_token);
+                  emit registerOk();
+                  disconnect(UIA::instance(), &UIA::error, this, nullptr);
+                  return;
+              }
 
-        nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
-                            static_cast<int>(err->status_code),
-                            err->matrix_error.error);
+              // The server requires registration flows.
+              if (err->status_code == 401 && err->matrix_error.unauthorized.flows.empty()) {
+                  nhlog::net()->warn("failed to retrieve registration flows: "
+                                     "status_code({}), matrix_error({}) ",
+                                     static_cast<int>(err->status_code),
+                                     err->matrix_error.error);
+                  setError(QString::fromStdString(err->matrix_error.error));
+                  disconnect(UIA::instance(), &UIA::error, this, nullptr);
+                  return;
+              }
 
-        showError(QString::fromStdString(err->matrix_error.error));
-    };
-}
+              nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
+                                  static_cast<int>(err->status_code),
+                                  err->matrix_error.error);
 
-void
-RegisterPage::paintEvent(QPaintEvent *)
-{
-    QStyleOption opt;
-    opt.initFrom(this);
-    QPainter p(this);
-    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+              setError(QString::fromStdString(err->matrix_error.error));
+              disconnect(UIA::instance(), &UIA::error, this, nullptr);
+          },
+          devicename.isEmpty() ? LoginPage::initialDeviceName_() : devicename.toStdString());
+    }
 }
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index f76313c8b468fc6c5599975de9c97d6b4b4ea147..67e2a22e5bf78db90659473bbb8d6cf8b920efd2 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -6,88 +6,69 @@
 
 #pragma once
 
-#include <QWidget>
-
-#include <memory>
+#include <QObject>
+#include <QString>
 
 #include <mtx/user_interactive.hpp>
 #include <mtxclient/http/client.hpp>
 
-class FlatButton;
-class RaisedButton;
-class TextField;
-class QLabel;
-class QVBoxLayout;
-class QHBoxLayout;
-
-class RegisterPage : public QWidget
+class RegisterPage : public QObject
 {
     Q_OBJECT
 
+    Q_PROPERTY(QString error READ error NOTIFY errorChanged)
+    Q_PROPERTY(QString hsError READ hsError NOTIFY hsErrorChanged)
+    Q_PROPERTY(QString usernameError READ usernameError NOTIFY lookingUpUsernameChanged)
+    Q_PROPERTY(bool registering READ registering NOTIFY registeringChanged)
+    Q_PROPERTY(bool supported READ supported NOTIFY lookingUpHsChanged)
+    Q_PROPERTY(bool lookingUpHs READ lookingUpHs NOTIFY lookingUpHsChanged)
+    Q_PROPERTY(bool lookingUpUsername READ lookingUpUsername NOTIFY lookingUpUsernameChanged)
+    Q_PROPERTY(bool usernameAvailable READ usernameAvailable NOTIFY lookingUpUsernameChanged)
+    Q_PROPERTY(bool usernameUnavailable READ usernameUnavailable NOTIFY lookingUpUsernameChanged)
+
 public:
-    RegisterPage(QWidget *parent = nullptr);
+    RegisterPage(QObject *parent = nullptr);
 
-protected:
-    void paintEvent(QPaintEvent *event) override;
+    Q_INVOKABLE void setServer(QString server);
+    Q_INVOKABLE void checkUsername(QString name);
+    Q_INVOKABLE void startRegistration(QString username, QString password, QString deviceName);
+    Q_INVOKABLE QString initialDeviceName() const;
 
-signals:
-    void backButtonClicked();
-    void errorOccurred();
+    bool registering() const { return registering_; }
+    bool supported() const { return supported_; }
+    bool lookingUpHs() const { return lookingUpHs_; }
+    bool lookingUpUsername() const { return lookingUpUsername_; }
+    bool usernameAvailable() const { return usernameAvailable_; }
+    bool usernameUnavailable() const { return usernameUnavailable_; }
 
-    //! Used to trigger the corresponding slot outside of the main thread.
-    void serverError(const QString &err);
+    QString error() const { return registrationError_; }
+    QString usernameError() const { return usernameError_; }
+    QString hsError() const { return hsError_; }
 
-    void wellKnownLookup();
-    void versionsCheck();
-    void registration();
+signals:
+    void errorChanged();
+    void hsErrorChanged();
+
+    void registeringChanged();
+    void lookingUpHsChanged();
+    void lookingUpUsernameChanged();
 
-    void registering();
     void registerOk();
 
-private slots:
-    void onBackButtonClicked();
-    void onRegisterButtonClicked();
+private:
+    void versionsCheck();
 
-    // function for showing different errors
-    void showError(const QString &msg);
-    void showError(QLabel *label, const QString &msg);
+    void setHsError(QString err);
+    void setError(QString err);
 
-    bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
-    bool checkUsername();
-    bool checkPassword();
-    bool checkPasswordConfirmation();
-    bool checkServer();
+    QString registrationError_, hsError_, usernameError_;
 
-    void doWellKnownLookup();
-    void doVersionsCheck();
-    void doRegistration();
-    mtx::http::Callback<mtx::responses::Register> registrationCb();
+    bool registering_;
+    bool supported_;
+    bool lookingUpHs_;
+    bool lookingUpUsername_;
+    bool usernameAvailable_;
+    bool usernameUnavailable_;
 
-private:
-    QVBoxLayout *top_layout_;
-
-    QHBoxLayout *back_layout_;
-    QHBoxLayout *logo_layout_;
-    QHBoxLayout *button_layout_;
-
-    QLabel *logo_;
-    QLabel *error_label_;
-    QLabel *error_username_label_;
-    QLabel *error_password_label_;
-    QLabel *error_password_confirmation_label_;
-    QLabel *error_server_label_;
-    QLabel *error_registration_token_label_;
-
-    FlatButton *back_button_;
-    RaisedButton *register_button_;
-
-    QWidget *form_widget_;
-    QHBoxLayout *form_wrapper_;
-    QVBoxLayout *form_layout_;
-
-    TextField *username_input_;
-    TextField *password_input_;
-    TextField *password_confirmation_;
-    TextField *server_input_;
-    TextField *registration_token_input_;
+    QString lastServer;
 };
diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index d83156a4965b010629a56f77a8a228d57ffbe27d..28da95589054367d8b69d0f453941f728d3c3d7b 100644
--- a/src/TrayIcon.cpp
+++ b/src/TrayIcon.cpp
@@ -10,6 +10,7 @@
 #include <QMenu>
 #include <QPainter>
 #include <QTimer>
+#include <QWindow>
 
 #include "TrayIcon.h"
 
@@ -100,7 +101,7 @@ MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State s
     return result;
 }
 
-TrayIcon::TrayIcon(const QString &filename, QWidget *parent)
+TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
   : QSystemTrayIcon(parent)
 {
 #if defined(Q_OS_MAC) || defined(Q_OS_WIN)
@@ -110,13 +111,13 @@ TrayIcon::TrayIcon(const QString &filename, QWidget *parent)
     setIcon(QIcon(icon_));
 #endif
 
-    QMenu *menu = new QMenu(parent);
+    QMenu *menu = new QMenu();
     setContextMenu(menu);
 
     viewAction_ = new QAction(tr("Show"), this);
     quitAction_ = new QAction(tr("Quit"), this);
 
-    connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show()));
+    connect(viewAction_, &QAction::triggered, parent, &QWindow::show);
     connect(quitAction_, &QAction::triggered, this, QApplication::quit);
 
     menu->addAction(viewAction_);
diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index 17bf5eff9c35b5c924122cb95a017b64da4a00a0..554a4a0ada8df6532528ea2d1808b00fb00dab8c 100644
--- a/src/TrayIcon.h
+++ b/src/TrayIcon.h
@@ -40,7 +40,7 @@ class TrayIcon : public QSystemTrayIcon
 {
     Q_OBJECT
 public:
-    TrayIcon(const QString &filename, QWidget *parent);
+    TrayIcon(const QString &filename, QWindow *parent);
 
 public slots:
     void setUnreadCount(int count);
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index c43733fb770ceb904ab4bb97a89ece62fceeda92..a0aa8f843ddb9a0b8c0e1769d5825e16023952b6 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -5,25 +5,14 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 #include <QApplication>
-#include <QComboBox>
 #include <QCoreApplication>
 #include <QFileDialog>
-#include <QFontComboBox>
-#include <QFormLayout>
+#include <QFontDatabase>
 #include <QInputDialog>
-#include <QLabel>
-#include <QLineEdit>
 #include <QMessageBox>
-#include <QPainter>
-#include <QPushButton>
-#include <QResizeEvent>
-#include <QScrollArea>
-#include <QScroller>
-#include <QSpinBox>
 #include <QStandardPaths>
 #include <QString>
 #include <QTextStream>
-#include <QtQml>
 #include <mtx/secret_storage.hpp>
 
 #include "Cache.h"
@@ -33,8 +22,7 @@
 #include "UserSettingsPage.h"
 #include "Utils.h"
 #include "encryption/Olm.h"
-#include "ui/FlatButton.h"
-#include "ui/ToggleButton.h"
+#include "ui/Theme.h"
 #include "voip/CallDevices.h"
 
 #include "config/nheko.h"
@@ -1518,7 +1506,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
                     QString homeFolder =
                       QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
                     auto filepath = QFileDialog::getOpenFileName(
-                      MainWindow::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
+                      nullptr, tr("Select a file"), homeFolder, tr("All Files (*)"));
                     if (!filepath.isEmpty()) {
                         i->setRingtone(filepath);
                         i->setRingtone(filepath);
@@ -1600,11 +1588,11 @@ UserSettingsModel::importSessionKeys()
 {
     const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
     const QString fileName   = QFileDialog::getOpenFileName(
-      MainWindow::instance(), tr("Open Sessions File"), homeFolder, QLatin1String(""));
+      nullptr, tr("Open Sessions File"), homeFolder, QLatin1String(""));
 
     QFile file(fileName);
     if (!file.open(QIODevice::ReadOnly)) {
-        QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString());
+        QMessageBox::warning(nullptr, tr("Error"), file.errorString());
         return;
     }
 
@@ -1612,7 +1600,7 @@ UserSettingsModel::importSessionKeys()
     auto payload = std::string(bin.data(), bin.size());
 
     bool ok;
-    auto password = QInputDialog::getText(MainWindow::instance(),
+    auto password = QInputDialog::getText(nullptr,
                                           tr("File Password"),
                                           tr("Enter the passphrase to decrypt the file:"),
                                           QLineEdit::Password,
@@ -1622,8 +1610,7 @@ UserSettingsModel::importSessionKeys()
         return;
 
     if (password.isEmpty()) {
-        QMessageBox::warning(
-          MainWindow::instance(), tr("Error"), tr("The password cannot be empty"));
+        QMessageBox::warning(nullptr, tr("Error"), tr("The password cannot be empty"));
         return;
     }
 
@@ -1631,7 +1618,7 @@ UserSettingsModel::importSessionKeys()
         auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString());
         cache::importSessionKeys(std::move(sessions));
     } catch (const std::exception &e) {
-        QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what());
+        QMessageBox::warning(nullptr, tr("Error"), e.what());
     }
 }
 void
@@ -1639,7 +1626,7 @@ UserSettingsModel::exportSessionKeys()
 {
     // Open password dialog.
     bool ok;
-    auto password = QInputDialog::getText(MainWindow::instance(),
+    auto password = QInputDialog::getText(nullptr,
                                           tr("File Password"),
                                           tr("Enter passphrase to encrypt your session keys:"),
                                           QLineEdit::Password,
@@ -1649,19 +1636,18 @@ UserSettingsModel::exportSessionKeys()
         return;
 
     if (password.isEmpty()) {
-        QMessageBox::warning(
-          MainWindow::instance(), tr("Error"), tr("The password cannot be empty"));
+        QMessageBox::warning(nullptr, tr("Error"), tr("The password cannot be empty"));
         return;
     }
 
     // Open file dialog to save the file.
     const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
     const QString fileName   = QFileDialog::getSaveFileName(
-      MainWindow::instance(), tr("File to save the exported session keys"), homeFolder);
+      nullptr, tr("File to save the exported session keys"), homeFolder);
 
     QFile file(fileName);
     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
-        QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString());
+        QMessageBox::warning(nullptr, tr("Error"), file.errorString());
         return;
     }
 
@@ -1679,7 +1665,7 @@ UserSettingsModel::exportSessionKeys()
         out << prefix << newline << b64 << newline << suffix << newline;
         file.close();
     } catch (const std::exception &e) {
-        QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what());
+        QMessageBox::warning(nullptr, tr("Error"), e.what());
     }
 }
 void
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index a44e0030e0cfb29a2b483300bbf00c4d895f326d..e9b8763dbcc22d7393bf43cbc0fc4193937d9aba 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -7,12 +7,9 @@
 #pragma once
 
 #include <QAbstractListModel>
-#include <QFontDatabase>
-#include <QFrame>
 #include <QProcessEnvironment>
 #include <QSettings>
 #include <QSharedPointer>
-#include <QWidget>
 
 #include "JdenticonProvider.h"
 #include <optional>
diff --git a/src/Utils.cpp b/src/Utils.cpp
index a9cfde22fe20f01f57c68131782e58232dfcb9a1..0ac37d8e747e78a6f157aa4cd37dcab4ec01a61a 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -17,6 +17,7 @@
 #include <QStringBuilder>
 #include <QTextBoundaryFinder>
 #include <QTextDocument>
+#include <QWindow>
 #include <QXmlStreamReader>
 
 #include <array>
@@ -770,20 +771,17 @@ utils::luminance(const QColor &col)
 }
 
 void
-utils::centerWidget(QWidget *widget, QWidget *parent)
+utils::centerWidget(QWidget *widget, QWindow *parent)
 {
-    auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
-        return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
-                      hostRect.center().y() - (childRect.height() * 0.5));
-    };
-
     if (parent) {
-        widget->move(parent->window()->frameGeometry().topLeft() +
-                     parent->window()->rect().center() - widget->rect().center());
+        widget->window()->windowHandle()->setTransientParent(parent);
         return;
     }
 
-    // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
+    auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
+        return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
+                      hostRect.center().y() - (childRect.height() * 0.5));
+    };
     widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
 }
 
diff --git a/src/Utils.h b/src/Utils.h
index 87ce1c34cf5fb6e4db0d8a5528fa45ee6eb138d5..0b6034acf54f034e1326152f5f40469463051ff1 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -290,7 +290,7 @@ luminance(const QColor &col);
 
 //! Center a widget in relation to another widget.
 void
-centerWidget(QWidget *widget, QWidget *parent);
+centerWidget(QWidget *widget, QWindow *parent);
 
 void
 restoreCombobox(QComboBox *combo, const QString &value);
diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp
deleted file mode 100644
index 5d540f4e20b2db3fee2821b060740778742e23be..0000000000000000000000000000000000000000
--- a/src/WelcomePage.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QLabel>
-#include <QLayout>
-#include <QPainter>
-#include <QStyleOption>
-
-#include "Config.h"
-#include "WelcomePage.h"
-#include "ui/RaisedButton.h"
-#include "ui/TextLabel.h"
-
-WelcomePage::WelcomePage(QWidget *parent)
-  : QWidget(parent)
-{
-    auto topLayout_ = new QVBoxLayout(this);
-    topLayout_->setSpacing(20);
-    topLayout_->setAlignment(Qt::AlignCenter);
-
-    QFont headingFont;
-    headingFont.setPointSizeF(headingFont.pointSizeF() * 2);
-    QFont subTitleFont;
-    subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5);
-
-    QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})};
-
-    auto logo_ = new QLabel(this);
-    logo_->setPixmap(icon.pixmap(256));
-    logo_->setAlignment(Qt::AlignCenter);
-
-    QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol."));
-    QString main(tr("Enjoy your stay!"));
-
-    auto intoTxt_ = new TextLabel(heading, this);
-    intoTxt_->setFont(headingFont);
-    intoTxt_->setAlignment(Qt::AlignCenter);
-
-    auto subTitle = new TextLabel(main, this);
-    subTitle->setFont(subTitleFont);
-    subTitle->setAlignment(Qt::AlignCenter);
-
-    topLayout_->addStretch(1);
-    topLayout_->addWidget(logo_);
-    topLayout_->addWidget(intoTxt_);
-    topLayout_->addWidget(subTitle);
-
-    auto btnLayout_ = new QHBoxLayout();
-    btnLayout_->setSpacing(20);
-    btnLayout_->setContentsMargins(0, 20, 0, 20);
-
-    const int fontHeight   = QFontMetrics{subTitleFont}.height();
-    const int buttonHeight = fontHeight * 2.5;
-    const int buttonWidth  = fontHeight * 8;
-
-    auto registerBtn = new RaisedButton(tr("REGISTER"), this);
-    registerBtn->setMinimumSize(buttonWidth, buttonHeight);
-    registerBtn->setFontSize(subTitleFont.pointSizeF());
-    registerBtn->setCornerRadius(conf::btn::cornerRadius);
-
-    auto loginBtn = new RaisedButton(tr("LOGIN"), this);
-    loginBtn->setMinimumSize(buttonWidth, buttonHeight);
-    loginBtn->setFontSize(subTitleFont.pointSizeF());
-    loginBtn->setCornerRadius(conf::btn::cornerRadius);
-
-    btnLayout_->addStretch(1);
-    btnLayout_->addWidget(registerBtn);
-    btnLayout_->addWidget(loginBtn);
-    btnLayout_->addStretch(1);
-
-    topLayout_->addLayout(btnLayout_);
-    topLayout_->addStretch(1);
-
-    connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister);
-    connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin);
-}
-
-void
-WelcomePage::paintEvent(QPaintEvent *)
-{
-    QStyleOption opt;
-    opt.initFrom(this);
-    QPainter p(this);
-    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/WelcomePage.h b/src/WelcomePage.h
deleted file mode 100644
index 9d5da8ba3f47a9c21bb584a97b6160fc8bf225fa..0000000000000000000000000000000000000000
--- a/src/WelcomePage.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QWidget>
-
-class WelcomePage : public QWidget
-{
-    Q_OBJECT
-
-public:
-    explicit WelcomePage(QWidget *parent = nullptr);
-
-protected:
-    void paintEvent(QPaintEvent *) override;
-
-signals:
-    // Notify that the user wants to login in.
-    void userLogin();
-
-    // Notify that the user wants to register.
-    void userRegister();
-};
diff --git a/src/main.cpp b/src/main.cpp
index 2ae631cf52491c11cfc527503d2dbb6818419d7d..24fc8415133171b5d275fe791311912df54c2e6d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -17,6 +17,7 @@
 #include <QLibraryInfo>
 #include <QMessageBox>
 #include <QPoint>
+#include <QQuickView>
 #include <QScreen>
 #include <QStandardPaths>
 #include <QTranslator>
@@ -279,6 +280,7 @@ main(int argc, char *argv[])
     font.setPointSizeF(settings.lock()->fontSize());
 
     app.setFont(font);
+    settings.lock()->applyTheme();
 
     if (QLocale().language() == QLocale::C)
         QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom));
@@ -296,9 +298,10 @@ main(int argc, char *argv[])
     app.installTranslator(&appTranslator);
 
     MainWindow w;
+    // QQuickView w;
 
     // Move the MainWindow to the center
-    w.move(screenCenter(w.width(), w.height()));
+    // w.move(screenCenter(w.width(), w.height()));
 
     if (!(settings.lock()->startInTray() && settings.lock()->tray()))
         w.show();
@@ -314,7 +317,7 @@ main(int argc, char *argv[])
     QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() {
         w.show();
         w.raise();
-        w.activateWindow();
+        w.requestActivate();
     });
 
     // It seems like handling the message in a blocking manner is a no-go. I have no idea how to
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 5284ce0e6d5b4106481c5a588296f086d8745f59..18e224b237c1e235d6458e1d135874701acefd34 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -266,8 +266,8 @@ void
 InputBar::openFileSelection()
 {
     const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
-    const auto fileName      = QFileDialog::getOpenFileName(
-      ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
+    const auto fileName =
+      QFileDialog::getOpenFileName(nullptr, tr("Select a file"), homeFolder, tr("All Files (*)"));
 
     if (fileName.isEmpty())
         return;
@@ -659,7 +659,7 @@ InputBar::command(const QString &command, QString args)
 void
 InputBar::showPreview(const QMimeData &source, const QString &path, const QStringList &formats)
 {
-    auto *previewDialog_ = new dialogs::PreviewUploadOverlay(ChatPage::instance());
+    auto *previewDialog_ = new dialogs::PreviewUploadOverlay(nullptr);
     previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
 
     // Force SVG to _not_ be handled as an image, but as raw data
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index eb453462d5f12fc0e35ea5ebf3a7d39c452043bc..aa81f501d220a962f302e9145d7459cfe05b4410 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -8,6 +8,7 @@
 #include "Cache_p.h"
 #include "ChatPage.h"
 #include "Logging.h"
+#include "MainWindow.h"
 #include "MatrixClient.h"
 #include "MxcImageProvider.h"
 #include "TimelineModel.h"
@@ -275,7 +276,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
 
         connect(newRoom.data(),
                 &TimelineModel::newEncryptedImage,
-                manager->imageProvider(),
+                MainWindow::instance()->imageProvider(),
                 &MxcImageProvider::addEncryptionInfo);
         connect(newRoom.data(),
                 &TimelineModel::forwardToRoom,
@@ -509,7 +510,7 @@ RoomlistModel::sync(const mtx::responses::Sync &sync_)
         // room_model->addEvents(room.timeline);
         connect(room_model.data(),
                 &TimelineModel::newCallEvent,
-                manager->callManager(),
+                ChatPage::instance()->callManager(),
                 &CallManager::syncEvent,
                 Qt::UniqueConnection);
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 7c9df403ba6afc100bcc7244518d2a33f32ee633..6b380f79ad7c17044200f43be8ad61f34c6ecc8f 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1031,7 +1031,7 @@ TimelineModel::setCurrentIndex(int index)
     if (index != oldIndex)
         emit currentIndexChanged(index);
 
-    if (!ChatPage::instance()->isActiveWindow())
+    if (MainWindow::instance() != QGuiApplication::focusWindow())
         return;
 
     if (!currentId.startsWith('m')) {
@@ -1495,7 +1495,7 @@ TimelineModel::saveMedia(const QString &eventId) const
     const QString openLocation = downloadsFolder + "/" + originalFilename;
 
     const QString filename =
-      QFileDialog::getSaveFileName(manager_->getWidget(), dialogTitle, openLocation, filterString);
+      QFileDialog::getSaveFileName(nullptr, dialogTitle, openLocation, filterString);
 
     if (filename.isEmpty())
         return false;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e689e2fa330d387576ffebdd2662c14dd36af15c..0abd102bef7bd1940e65e2c608e8d71345f67225 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -5,10 +5,10 @@
 
 #include "TimelineViewManager.h"
 
+#include <QApplication>
 #include <QDropEvent>
 #include <QFileDialog>
 #include <QMetaType>
-#include <QPalette>
 #include <QQmlContext>
 #include <QQmlEngine>
 #include <QStandardPaths>
@@ -45,10 +45,6 @@
 #include "ui/NhekoGlobalObject.h"
 #include "ui/UIA.h"
 
-Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
-Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
-Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
-
 namespace msgs = mtx::events::msg;
 
 namespace {
@@ -102,19 +98,6 @@ void
 TimelineViewManager::updateColorPalette()
 {
     userColors.clear();
-
-    if (ChatPage::instance()->userSettings()->theme() == QLatin1String("light")) {
-        view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
-        view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"),
-                                                QPalette());
-    } else if (ChatPage::instance()->userSettings()->theme() == QLatin1String("dark")) {
-        view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
-        view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"),
-                                                QPalette());
-    } else {
-        view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
-        view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"), nullptr);
-    }
 }
 
 QColor
@@ -126,112 +109,15 @@ TimelineViewManager::userColor(QString id, QColor background)
     return userColors.value(idx);
 }
 
-TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
+TimelineViewManager::TimelineViewManager(CallManager *, ChatPage *parent)
   : QObject(parent)
-  , imgProvider(new MxcImageProvider())
-  , colorImgProvider(new ColorImageProvider())
-  , blurhashProvider(new BlurhashProvider())
-  , jdenticonProvider(new JdenticonProvider())
   , rooms_(new RoomlistModel(this))
   , communities_(new CommunitiesModel(this))
-  , callManager_(callManager)
   , verificationManager_(new VerificationManager(this))
   , presenceEmitter(new PresenceEmitter(this))
 {
-    qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
-    qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
-    qRegisterMetaType<CombinedImagePackModel *>();
-
-    qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
-
-    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<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
-    qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
-    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"));
-
     static auto self = this;
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
     qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
-    qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
     qmlRegisterSingletonType<RoomlistModel>(
       "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
           auto ptr = new FilteredRoomlistModel(self->rooms_);
@@ -247,79 +133,15 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           return ptr;
       });
     qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
-    qmlRegisterSingletonInstance(
-      "im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
-    qmlRegisterSingletonInstance(
-      "im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
-    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, "VerificationManager", verificationManager_);
     qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter);
-    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;
-      });
 
-    qRegisterMetaType<mtx::events::collections::TimelineEvents>();
-    qRegisterMetaType<std::vector<DeviceInfo>>();
-
-    qmlRegisterUncreatableType<FilteredCommunitiesModel>(
-      "im.nheko",
-      1,
-      0,
-      "FilteredCommunitiesModel",
-      QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
-
-    qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
-    qmlRegisterUncreatableType<emoji::Emoji>(
-      "im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
-    qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
-                                     "im.nheko.EmojiModel",
-                                     1,
-                                     0,
-                                     "EmojiCategory",
-                                     QStringLiteral("Error: Only enums"));
-
-    qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
-
-#ifdef USE_QUICK_VIEW
-    view      = new QQuickView(parent);
-    container = QWidget::createWindowContainer(view, parent);
-#else
-    view      = new QQuickWidget(parent);
-    container = view;
-    view->setResizeMode(QQuickWidget::SizeRootObjectToView);
-    container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-
-    connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
-        nhlog::ui()->debug("Status changed to {}", status);
-    });
-#endif
-    container->setMinimumSize(200, 200);
     updateColorPalette();
-    view->engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider);
-    view->engine()->addImageProvider(QStringLiteral("colorimage"), colorImgProvider);
-    view->engine()->addImageProvider(QStringLiteral("blurhash"), blurhashProvider);
-    if (JdenticonProvider::isAvailable())
-        view->engine()->addImageProvider(QStringLiteral("jdenticon"), jdenticonProvider);
-    view->setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml")));
-
-    connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
+
+    connect(UserSettings::instance().get(),
+            &UserSettings::themeChanged,
+            this,
+            &TimelineViewManager::updateColorPalette);
     connect(parent,
             &ChatPage::receivedRoomDeviceVerificationRequest,
             verificationManager_,
@@ -336,6 +158,16 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         isInitialSync_ = true;
         emit initialSyncChanged(true);
     });
+    connect(qobject_cast<QApplication *>(QApplication::instance()),
+            &QApplication::focusWindowChanged,
+            this,
+            &TimelineViewManager::focusChanged);
+}
+
+bool
+TimelineViewManager::isWindowFocused() const
+{
+    return MainWindow::instance() == QApplication::focusWindow();
 }
 
 void
@@ -379,7 +211,8 @@ void
 TimelineViewManager::setVideoCallItem()
 {
     WebRTCSession::instance().setVideoItem(
-      view->rootObject()->findChild<QQuickItem *>(QStringLiteral("videoCallItem")));
+      MainWindow::instance()->rootObject()->findChild<QQuickItem *>(
+        QStringLiteral("videoCallItem")));
 }
 
 void
@@ -401,7 +234,7 @@ TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
     if (auto room = rooms_->getRoomById(room_id)) {
         if (rooms_->currentRoom() != room) {
             rooms_->setCurrentRoom(room_id);
-            container->setFocus();
+            MainWindow::instance()->requestActivate();
             nhlog::ui()->info("Activated room {}", room_id.toStdString());
         }
 
@@ -439,7 +272,7 @@ TimelineViewManager::saveMedia(QString mxcUrl)
       QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
     const QString openLocation = downloadsFolder + "/" + mxcUrl.splitRef(u'/').constLast();
 
-    const QString filename = QFileDialog::getSaveFileName(getWidget(), {}, openLocation);
+    const QString filename = QFileDialog::getSaveFileName(nullptr, {}, openLocation);
 
     if (filename.isEmpty())
         return;
@@ -590,12 +423,6 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
     return nullptr;
 }
 
-void
-TimelineViewManager::focusTimeline()
-{
-    getWidget()->setFocus();
-}
-
 void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 455702f4341fd0996fc7a0c3a022c3aea1025a7c..13ab5dbbbb4c37ef40429378d53afbc20170a4c8 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -8,8 +8,6 @@
 #include <QHash>
 #include <QQuickItem>
 #include <QQuickTextDocument>
-#include <QQuickView>
-#include <QQuickWidget>
 #include <QWidget>
 
 #include <mtx/common.hpp>
@@ -43,23 +41,19 @@ class TimelineViewManager : public QObject
 
     Q_PROPERTY(
       bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
-    Q_PROPERTY(
-      bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged)
+    Q_PROPERTY(bool isWindowFocused READ isWindowFocused NOTIFY focusChanged)
 
 public:
     TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
-    QWidget *getWidget() const { return container; }
 
     void sync(const mtx::responses::Sync &sync_);
 
-    MxcImageProvider *imageProvider() { return imgProvider; }
-    CallManager *callManager() { return callManager_; }
     VerificationManager *verificationManager() { return verificationManager_; }
 
     void clearAll() { rooms_->clear(); }
 
     Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
-    bool isWindowFocused() const { return isWindowFocused_; }
+    bool isWindowFocused() const;
     Q_INVOKABLE void openImageOverlay(TimelineModel *room, QString mxcUrl, QString eventId);
     Q_INVOKABLE void openImagePackSettings(QString roomid);
     Q_INVOKABLE void saveMedia(QString mxcUrl);
@@ -98,14 +92,8 @@ public slots:
     void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
     void receivedSessionKey(const std::string &room_id, const std::string &session_id);
     void initializeRoomlist();
-    void chatFocusChanged(bool focused)
-    {
-        isWindowFocused_ = focused;
-        emit focusChanged();
-    }
 
     void showEvent(const QString &room_id, const QString &event_id);
-    void focusTimeline();
 
     void updateColorPalette();
     void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody);
@@ -122,26 +110,12 @@ public slots:
     RoomlistModel *rooms() { return rooms_; }
 
 private:
-#ifdef USE_QUICK_VIEW
-    QQuickView *view;
-#else
-    QQuickWidget *view;
-#endif
-    QWidget *container;
-
-    MxcImageProvider *imgProvider;
-    ColorImageProvider *colorImgProvider;
-    BlurhashProvider *blurhashProvider;
-    JdenticonProvider *jdenticonProvider;
-
-    bool isInitialSync_   = true;
-    bool isWindowFocused_ = false;
+    bool isInitialSync_ = true;
 
     RoomlistModel *rooms_          = nullptr;
     CommunitiesModel *communities_ = nullptr;
 
     // don't move this above the rooms_
-    CallManager *callManager_                 = nullptr;
     VerificationManager *verificationManager_ = nullptr;
     PresenceEmitter *presenceEmitter          = nullptr;
 
diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp
deleted file mode 100644
index 039d65589074f01efe9759211953ea0934f886ef..0000000000000000000000000000000000000000
--- a/src/ui/DropShadow.cpp
+++ /dev/null
@@ -1,108 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "DropShadow.h"
-
-#include <QLinearGradient>
-#include <QPainter>
-
-void
-DropShadow::draw(QPainter &painter,
-                 qint16 margin,
-                 qreal radius,
-                 QColor start,
-                 QColor end,
-                 qreal startPosition,
-                 qreal endPosition0,
-                 qreal endPosition1,
-                 qreal width,
-                 qreal height)
-{
-    painter.setPen(Qt::NoPen);
-
-    QLinearGradient gradient;
-    gradient.setColorAt(startPosition, start);
-    gradient.setColorAt(endPosition0, end);
-
-    // Right
-    QPointF right0(width - margin, height / 2);
-    QPointF right1(width, height / 2);
-    gradient.setStart(right0);
-    gradient.setFinalStop(right1);
-    painter.setBrush(QBrush(gradient));
-    // Deprecated in 5.13: painter.drawRoundRect(
-    //  QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
-    //  margin)), 0.0, 0.0);
-    painter.drawRoundedRect(
-      QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, 0.0);
-
-    // Left
-    QPointF left0(margin, height / 2);
-    QPointF left1(0, height / 2);
-    gradient.setStart(left0);
-    gradient.setFinalStop(left1);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(
-      QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
-
-    // Top
-    QPointF top0(width / 2, margin);
-    QPointF top1(width / 2, 0);
-    gradient.setStart(top0);
-    gradient.setFinalStop(top1);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
-
-    // Bottom
-    QPointF bottom0(width / 2, height - margin);
-    QPointF bottom1(width / 2, height);
-    gradient.setStart(bottom0);
-    gradient.setFinalStop(bottom1);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(
-      QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0);
-
-    // BottomRight
-    QPointF bottomright0(width - margin, height - margin);
-    QPointF bottomright1(width, height);
-    gradient.setStart(bottomright0);
-    gradient.setFinalStop(bottomright1);
-    gradient.setColorAt(endPosition1, end);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
-
-    // BottomLeft
-    QPointF bottomleft0(margin, height - margin);
-    QPointF bottomleft1(0, height);
-    gradient.setStart(bottomleft0);
-    gradient.setFinalStop(bottomleft1);
-    gradient.setColorAt(endPosition1, end);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
-
-    // TopLeft
-    QPointF topleft0(margin, margin);
-    QPointF topleft1(0, 0);
-    gradient.setStart(topleft0);
-    gradient.setFinalStop(topleft1);
-    gradient.setColorAt(endPosition1, end);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
-
-    // TopRight
-    QPointF topright0(width - margin, margin);
-    QPointF topright1(width, 0);
-    gradient.setStart(topright0);
-    gradient.setFinalStop(topright1);
-    gradient.setColorAt(endPosition1, end);
-    painter.setBrush(QBrush(gradient));
-    painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
-
-    // Widget
-    painter.setBrush(QBrush(QColor(0xff, 0xff, 0xff)));
-    painter.setRenderHint(QPainter::Antialiasing);
-    painter.drawRoundedRect(
-      QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), radius, radius);
-}
diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h
deleted file mode 100644
index 1810a1fe0611fc7234379a16d28c851d1f0eff13..0000000000000000000000000000000000000000
--- a/src/ui/DropShadow.h
+++ /dev/null
@@ -1,25 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QColor>
-
-class QPainter;
-
-class DropShadow
-{
-public:
-    static void draw(QPainter &painter,
-                     qint16 margin,
-                     qreal radius,
-                     QColor start,
-                     QColor end,
-                     qreal startPosition,
-                     qreal endPosition0,
-                     qreal endPosition1,
-                     qreal width,
-                     qreal height);
-};
diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp
deleted file mode 100644
index da32237857792b1f50ae45eb947a1a9c8695f2bd..0000000000000000000000000000000000000000
--- a/src/ui/FlatButton.cpp
+++ /dev/null
@@ -1,730 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QEventTransition>
-#include <QFontDatabase>
-#include <QIcon>
-#include <QMouseEvent>
-#include <QPaintEvent>
-#include <QPainter>
-#include <QPainterPath>
-#include <QResizeEvent>
-#include <QSignalTransition>
-
-#include "FlatButton.h"
-#include "Ripple.h"
-#include "RippleOverlay.h"
-#include "ThemeManager.h"
-
-// The ampersand is automatically set in QPushButton or QCheckbx
-// by KDEPlatformTheme plugin in Qt5.
-// [https://bugs.kde.org/show_bug.cgi?id=337491]
-//
-// A workaroud is to add
-//
-// [Development]
-// AutoCheckAccelerators=false
-//
-// to ~/.config/kdeglobals
-static QString
-removeKDEAccelerators(QString text)
-{
-    return text.remove(QChar('&'));
-}
-
-void
-FlatButton::init()
-{
-    ripple_overlay_          = new RippleOverlay(this);
-    state_machine_           = new FlatButtonStateMachine(this);
-    role_                    = ui::Role::Default;
-    ripple_style_            = ui::RippleStyle::PositionedRipple;
-    icon_placement_          = ui::ButtonIconPlacement::LeftIcon;
-    overlay_style_           = ui::OverlayStyle::GrayOverlay;
-    bg_mode_                 = Qt::TransparentMode;
-    fixed_ripple_radius_     = 64;
-    corner_radius_           = 3;
-    base_opacity_            = 0.13;
-    font_size_               = 10; // 10.5;
-    use_fixed_ripple_radius_ = false;
-
-    setStyle(&ThemeManager::instance());
-    setAttribute(Qt::WA_Hover);
-    setMouseTracking(true);
-    setCursor(QCursor(Qt::PointingHandCursor));
-
-    QPainterPath path;
-    path.addRoundedRect(rect(), corner_radius_, corner_radius_);
-
-    ripple_overlay_->setClipPath(path);
-    ripple_overlay_->setClipping(true);
-
-    state_machine_->setupProperties();
-    state_machine_->startAnimations();
-}
-
-FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset)
-  : QPushButton(parent)
-{
-    init();
-    applyPreset(preset);
-}
-
-FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset)
-  : QPushButton(text, parent)
-{
-    init();
-    applyPreset(preset);
-}
-
-FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset)
-  : QPushButton(text, parent)
-{
-    init();
-    applyPreset(preset);
-    setRole(role);
-}
-
-void
-FlatButton::applyPreset(ui::ButtonPreset preset)
-{
-    switch (preset) {
-    case ui::ButtonPreset::FlatPreset:
-        setOverlayStyle(ui::OverlayStyle::NoOverlay);
-        break;
-    case ui::ButtonPreset::CheckablePreset:
-        setOverlayStyle(ui::OverlayStyle::NoOverlay);
-        setCheckable(true);
-        break;
-    default:
-        break;
-    }
-}
-
-void
-FlatButton::setRole(ui::Role role)
-{
-    role_ = role;
-    state_machine_->setupProperties();
-}
-
-ui::Role
-FlatButton::role() const
-{
-    return role_;
-}
-
-void
-FlatButton::setForegroundColor(const QColor &color)
-{
-    foreground_color_ = color;
-    emit foregroundColorChanged();
-}
-
-QColor
-FlatButton::foregroundColor() const
-{
-    if (!foreground_color_.isValid()) {
-        if (bg_mode_ == Qt::OpaqueMode) {
-            return ThemeManager::instance().themeColor(QStringLiteral("BrightWhite"));
-        }
-
-        switch (role_) {
-        case ui::Role::Primary:
-            return ThemeManager::instance().themeColor(QStringLiteral("Blue"));
-        case ui::Role::Secondary:
-            return ThemeManager::instance().themeColor(QStringLiteral("Gray"));
-        case ui::Role::Default:
-        default:
-            return ThemeManager::instance().themeColor(QStringLiteral("Black"));
-        }
-    }
-
-    return foreground_color_;
-}
-
-void
-FlatButton::setBackgroundColor(const QColor &color)
-{
-    background_color_ = color;
-    emit backgroundColorChanged();
-}
-
-QColor
-FlatButton::backgroundColor() const
-{
-    if (!background_color_.isValid()) {
-        switch (role_) {
-        case ui::Role::Primary:
-            return ThemeManager::instance().themeColor(QStringLiteral("Blue"));
-        case ui::Role::Secondary:
-            return ThemeManager::instance().themeColor(QStringLiteral("Gray"));
-        case ui::Role::Default:
-        default:
-            return ThemeManager::instance().themeColor(QStringLiteral("Black"));
-        }
-    }
-
-    return background_color_;
-}
-
-void
-FlatButton::setOverlayColor(const QColor &color)
-{
-    overlay_color_ = color;
-    setOverlayStyle(ui::OverlayStyle::TintedOverlay);
-    emit overlayColorChanged();
-}
-
-QColor
-FlatButton::overlayColor() const
-{
-    if (!overlay_color_.isValid()) {
-        return foregroundColor();
-    }
-
-    return overlay_color_;
-}
-
-void
-FlatButton::setDisabledForegroundColor(const QColor &color)
-{
-    disabled_color_ = color;
-    emit disabledForegroundColorChanged();
-}
-
-QColor
-FlatButton::disabledForegroundColor() const
-{
-    if (!disabled_color_.isValid()) {
-        return ThemeManager::instance().themeColor(QStringLiteral("FadedWhite"));
-    }
-
-    return disabled_color_;
-}
-
-void
-FlatButton::setDisabledBackgroundColor(const QColor &color)
-{
-    disabled_background_color_ = color;
-    emit disabledBackgroundColorChanged();
-}
-
-QColor
-FlatButton::disabledBackgroundColor() const
-{
-    if (!disabled_background_color_.isValid()) {
-        return ThemeManager::instance().themeColor(QStringLiteral("FadedWhite"));
-    }
-
-    return disabled_background_color_;
-}
-
-void
-FlatButton::setFontSize(qreal size)
-{
-    font_size_ = size;
-
-    QFont f(font());
-    f.setPointSizeF(size);
-    setFont(f);
-
-    emit fontSizeChanged();
-
-    update();
-}
-
-qreal
-FlatButton::fontSize() const
-{
-    return font_size_;
-}
-
-void
-FlatButton::setOverlayStyle(ui::OverlayStyle style)
-{
-    overlay_style_ = style;
-    update();
-}
-
-ui::OverlayStyle
-FlatButton::overlayStyle() const
-{
-    return overlay_style_;
-}
-
-void
-FlatButton::setRippleStyle(ui::RippleStyle style)
-{
-    ripple_style_ = style;
-}
-
-ui::RippleStyle
-FlatButton::rippleStyle() const
-{
-    return ripple_style_;
-}
-
-void
-FlatButton::setIconPlacement(ui::ButtonIconPlacement placement)
-{
-    icon_placement_ = placement;
-    update();
-}
-
-ui::ButtonIconPlacement
-FlatButton::iconPlacement() const
-{
-    return icon_placement_;
-}
-
-void
-FlatButton::setCornerRadius(qreal radius)
-{
-    corner_radius_ = radius;
-    updateClipPath();
-    update();
-}
-
-qreal
-FlatButton::cornerRadius() const
-{
-    return corner_radius_;
-}
-
-void
-FlatButton::setBackgroundMode(Qt::BGMode mode)
-{
-    bg_mode_ = mode;
-    state_machine_->setupProperties();
-}
-
-Qt::BGMode
-FlatButton::backgroundMode() const
-{
-    return bg_mode_;
-}
-
-void
-FlatButton::setBaseOpacity(qreal opacity)
-{
-    base_opacity_ = opacity;
-    state_machine_->setupProperties();
-}
-
-qreal
-FlatButton::baseOpacity() const
-{
-    return base_opacity_;
-}
-
-void
-FlatButton::setCheckable(bool value)
-{
-    state_machine_->updateCheckedStatus();
-    state_machine_->setCheckedOverlayProgress(0);
-
-    QPushButton::setCheckable(value);
-}
-
-void
-FlatButton::setHasFixedRippleRadius(bool value)
-{
-    use_fixed_ripple_radius_ = value;
-}
-
-bool
-FlatButton::hasFixedRippleRadius() const
-{
-    return use_fixed_ripple_radius_;
-}
-
-void
-FlatButton::setFixedRippleRadius(qreal radius)
-{
-    fixed_ripple_radius_ = radius;
-    setHasFixedRippleRadius(true);
-}
-
-QSize
-FlatButton::sizeHint() const
-{
-    ensurePolished();
-
-    QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
-
-    int w = 20 + label.width();
-    int h = label.height();
-
-    if (!icon().isNull()) {
-        w += iconSize().width() + FlatButton::IconPadding;
-        h = qMax(h, iconSize().height());
-    }
-
-    return QSize(w, 20 + h);
-}
-
-void
-FlatButton::checkStateSet()
-{
-    state_machine_->updateCheckedStatus();
-    QPushButton::checkStateSet();
-}
-
-void
-FlatButton::mousePressEvent(QMouseEvent *event)
-{
-    if (ui::RippleStyle::NoRipple != ripple_style_) {
-        QPoint pos;
-        qreal radiusEndValue;
-
-        if (ui::RippleStyle::CenteredRipple == ripple_style_) {
-            pos = rect().center();
-        } else {
-            pos = event->pos();
-        }
-
-        if (use_fixed_ripple_radius_) {
-            radiusEndValue = fixed_ripple_radius_;
-        } else {
-            radiusEndValue = static_cast<qreal>(width()) / 2;
-        }
-
-        Ripple *ripple = new Ripple(pos);
-
-        ripple->setRadiusEndValue(radiusEndValue);
-        ripple->setOpacityStartValue(0.35);
-        ripple->setColor(foregroundColor());
-        ripple->radiusAnimation()->setDuration(250);
-        ripple->opacityAnimation()->setDuration(250);
-
-        ripple_overlay_->addRipple(ripple);
-    }
-
-    QPushButton::mousePressEvent(event);
-}
-
-void
-FlatButton::mouseReleaseEvent(QMouseEvent *event)
-{
-    QPushButton::mouseReleaseEvent(event);
-    state_machine_->updateCheckedStatus();
-}
-
-void
-FlatButton::resizeEvent(QResizeEvent *event)
-{
-    QPushButton::resizeEvent(event);
-    updateClipPath();
-}
-
-void
-FlatButton::paintEvent(QPaintEvent *event)
-{
-    Q_UNUSED(event)
-
-    QPainter painter(this);
-    painter.setRenderHint(QPainter::Antialiasing);
-
-    const qreal cr = corner_radius_;
-
-    if (cr > 0) {
-        QPainterPath path;
-        path.addRoundedRect(rect(), cr, cr);
-
-        painter.setClipPath(path);
-        painter.setClipping(true);
-    }
-
-    paintBackground(&painter);
-
-    painter.setOpacity(1);
-    painter.setClipping(false);
-
-    paintForeground(&painter);
-}
-
-void
-FlatButton::paintBackground(QPainter *painter)
-{
-    const qreal overlayOpacity  = state_machine_->overlayOpacity();
-    const qreal checkedProgress = state_machine_->checkedOverlayProgress();
-
-    if (Qt::OpaqueMode == bg_mode_) {
-        QBrush brush;
-        brush.setStyle(Qt::SolidPattern);
-
-        if (isEnabled()) {
-            brush.setColor(backgroundColor());
-        } else {
-            brush.setColor(disabledBackgroundColor());
-        }
-
-        painter->setOpacity(1);
-        painter->setBrush(brush);
-        painter->setPen(Qt::NoPen);
-        painter->drawRect(rect());
-    }
-
-    QBrush brush;
-    brush.setStyle(Qt::SolidPattern);
-    painter->setPen(Qt::NoPen);
-
-    if (!isEnabled()) {
-        return;
-    }
-
-    if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) {
-        if (ui::OverlayStyle::TintedOverlay == overlay_style_) {
-            brush.setColor(overlayColor());
-        } else {
-            brush.setColor(Qt::gray);
-        }
-
-        painter->setOpacity(overlayOpacity);
-        painter->setBrush(brush);
-        painter->drawRect(rect());
-    }
-
-    if (isCheckable() && checkedProgress > 0) {
-        const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7;
-        brush.setColor(foregroundColor());
-        painter->setOpacity(q * checkedProgress);
-        painter->setBrush(brush);
-        QRect r(rect());
-        r.setHeight(static_cast<qreal>(r.height()) * checkedProgress);
-        painter->drawRect(r);
-    }
-}
-
-#define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH()
-
-void
-FlatButton::paintForeground(QPainter *painter)
-{
-    if (isEnabled()) {
-        painter->setPen(foregroundColor());
-        const qreal progress = state_machine_->checkedOverlayProgress();
-
-        if (isCheckable() && progress > 0) {
-            QColor source = foregroundColor();
-            QColor dest   = Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor();
-            if (qFuzzyCompare(1, progress)) {
-                painter->setPen(dest);
-            } else {
-                painter->setPen(QColor(COLOR_INTERPOLATE(red),
-                                       COLOR_INTERPOLATE(green),
-                                       COLOR_INTERPOLATE(blue),
-                                       COLOR_INTERPOLATE(alpha)));
-            }
-        }
-    } else {
-        painter->setPen(disabledForegroundColor());
-    }
-
-    if (icon().isNull()) {
-        painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text()));
-        return;
-    }
-
-    QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
-    QSize base(size() - textSize);
-
-    const int iw = iconSize().width() + IconPadding;
-    QPoint pos((base.width() - iw) / 2, 0);
-
-    QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize);
-    QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize());
-
-    /* if (ui::LeftIcon == icon_placement_) { */
-    /* 	textGeometry.translate(iw, 0); */
-    /* } else { */
-    /* 	iconGeometry.translate(textSize.width() + IconPadding, 0); */
-    /* } */
-
-    painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text()));
-
-    QPixmap pixmap = icon().pixmap(iconSize());
-    QPainter icon(&pixmap);
-    icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
-    icon.fillRect(pixmap.rect(), painter->pen().color());
-    painter->drawPixmap(iconGeometry, pixmap);
-}
-
-void
-FlatButton::updateClipPath()
-{
-    const qreal radius = corner_radius_;
-
-    QPainterPath path;
-    path.addRoundedRect(rect(), radius, radius);
-    ripple_overlay_->setClipPath(path);
-}
-
-FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent)
-  : QStateMachine(parent)
-  , button_(parent)
-  , top_level_state_(new QState(QState::ParallelStates))
-  , config_state_(new QState(top_level_state_))
-  , checkable_state_(new QState(top_level_state_))
-  , checked_state_(new QState(checkable_state_))
-  , unchecked_state_(new QState(checkable_state_))
-  , neutral_state_(new QState(config_state_))
-  , neutral_focused_state_(new QState(config_state_))
-  , hovered_state_(new QState(config_state_))
-  , hovered_focused_state_(new QState(config_state_))
-  , pressed_state_(new QState(config_state_))
-  , overlay_opacity_(0)
-  , checked_overlay_progress_(parent->isChecked() ? 1 : 0)
-  , was_checked_(false)
-{
-    Q_ASSERT(parent);
-
-    parent->installEventFilter(this);
-
-    config_state_->setInitialState(neutral_state_);
-    addState(top_level_state_);
-    setInitialState(top_level_state_);
-
-    checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_);
-    QSignalTransition *transition;
-    QPropertyAnimation *animation;
-
-    transition = new QSignalTransition(this, SIGNAL(buttonChecked()));
-    transition->setTargetState(checked_state_);
-    unchecked_state_->addTransition(transition);
-
-    animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
-    animation->setDuration(200);
-    transition->addAnimation(animation);
-
-    transition = new QSignalTransition(this, SIGNAL(buttonUnchecked()));
-    transition->setTargetState(unchecked_state_);
-    checked_state_->addTransition(transition);
-
-    animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
-    animation->setDuration(200);
-    transition->addAnimation(animation);
-
-    addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_);
-    addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_);
-    addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_);
-    addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_);
-    addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_);
-    addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_);
-    addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_);
-    addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_);
-    addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_);
-    addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_);
-    addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_);
-}
-
-void
-FlatButtonStateMachine::setOverlayOpacity(qreal opacity)
-{
-    overlay_opacity_ = opacity;
-    emit overlayOpacityChanged();
-    button_->update();
-}
-
-void
-FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity)
-{
-    checked_overlay_progress_ = opacity;
-    emit checkedOverlayProgressChanged();
-    button_->update();
-}
-
-void
-FlatButtonStateMachine::startAnimations()
-{
-    start();
-}
-
-void
-FlatButtonStateMachine::setupProperties()
-{
-    QColor overlayColor;
-
-    if (Qt::TransparentMode == button_->backgroundMode()) {
-        overlayColor = button_->backgroundColor();
-    } else {
-        overlayColor = button_->foregroundColor();
-    }
-
-    const qreal baseOpacity = button_->baseOpacity();
-
-    neutral_state_->assignProperty(this, "overlayOpacity", 0);
-    neutral_focused_state_->assignProperty(this, "overlayOpacity", 0);
-    hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-    hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-    pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity);
-    checked_state_->assignProperty(this, "checkedOverlayProgress", 1);
-    unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0);
-
-    button_->update();
-}
-
-void
-FlatButtonStateMachine::updateCheckedStatus()
-{
-    const bool checked = button_->isChecked();
-    if (was_checked_ != checked) {
-        was_checked_ = checked;
-        if (checked) {
-            emit buttonChecked();
-        } else {
-            emit buttonUnchecked();
-        }
-    }
-}
-
-bool
-FlatButtonStateMachine::eventFilter(QObject *watched, QEvent *event)
-{
-    if (QEvent::FocusIn == event->type()) {
-        QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event);
-        if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) {
-            emit buttonPressed();
-            return true;
-        }
-    }
-
-    return QStateMachine::eventFilter(watched, event);
-}
-
-void
-FlatButtonStateMachine::addTransition(QObject *object,
-                                      const char *signal,
-                                      QState *fromState,
-                                      QState *toState)
-{
-    addTransition(new QSignalTransition(object, signal), fromState, toState);
-}
-
-void
-FlatButtonStateMachine::addTransition(QObject *object,
-                                      QEvent::Type eventType,
-                                      QState *fromState,
-                                      QState *toState)
-{
-    addTransition(new QEventTransition(object, eventType), fromState, toState);
-}
-
-void
-FlatButtonStateMachine::addTransition(QAbstractTransition *transition,
-                                      QState *fromState,
-                                      QState *toState)
-{
-    transition->setTargetState(toState);
-
-    QPropertyAnimation *animation;
-
-    animation = new QPropertyAnimation(this, "overlayOpacity", this);
-    animation->setDuration(150);
-    transition->addAnimation(animation);
-
-    fromState->addTransition(transition);
-}
diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h
deleted file mode 100644
index e6215024729ac22b1955328b717c0bb5cc7d56e8..0000000000000000000000000000000000000000
--- a/src/ui/FlatButton.h
+++ /dev/null
@@ -1,198 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QPushButton>
-#include <QStateMachine>
-
-#include "Theme.h"
-
-class RippleOverlay;
-class FlatButton;
-
-class FlatButtonStateMachine : public QStateMachine
-{
-    Q_OBJECT
-
-    Q_PROPERTY(
-      qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity NOTIFY overlayOpacityChanged)
-    Q_PROPERTY(qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ
-                 checkedOverlayProgress NOTIFY checkedOverlayProgressChanged)
-
-public:
-    explicit FlatButtonStateMachine(FlatButton *parent);
-
-    void setOverlayOpacity(qreal opacity);
-    void setCheckedOverlayProgress(qreal opacity);
-
-    inline qreal overlayOpacity() const;
-    inline qreal checkedOverlayProgress() const;
-
-    void startAnimations();
-    void setupProperties();
-    void updateCheckedStatus();
-
-signals:
-    void buttonPressed();
-    void buttonChecked();
-    void buttonUnchecked();
-
-    void overlayOpacityChanged();
-    void checkedOverlayProgressChanged();
-
-protected:
-    bool eventFilter(QObject *watched, QEvent *event) override;
-
-private:
-    void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState);
-    void addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState);
-    void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState);
-
-    FlatButton *const button_;
-
-    QState *const top_level_state_;
-    QState *const config_state_;
-    QState *const checkable_state_;
-    QState *const checked_state_;
-    QState *const unchecked_state_;
-    QState *const neutral_state_;
-    QState *const neutral_focused_state_;
-    QState *const hovered_state_;
-    QState *const hovered_focused_state_;
-    QState *const pressed_state_;
-
-    qreal overlay_opacity_;
-    qreal checked_overlay_progress_;
-
-    bool was_checked_;
-};
-
-inline qreal
-FlatButtonStateMachine::overlayOpacity() const
-{
-    return overlay_opacity_;
-}
-
-inline qreal
-FlatButtonStateMachine::checkedOverlayProgress() const
-{
-    return checked_overlay_progress_;
-}
-
-class FlatButton : public QPushButton
-{
-    Q_OBJECT
-
-    Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor NOTIFY
-                 foregroundColorChanged)
-    Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor NOTIFY
-                 backgroundColorChanged)
-    Q_PROPERTY(
-      QColor overlayColor WRITE setOverlayColor READ overlayColor NOTIFY overlayColorChanged)
-    Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ
-                 disabledForegroundColor NOTIFY disabledForegroundColorChanged)
-    Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ
-                 disabledBackgroundColor NOTIFY disabledBackgroundColorChanged)
-    Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize NOTIFY fontSizeChanged)
-
-public:
-    explicit FlatButton(QWidget *parent         = nullptr,
-                        ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-    explicit FlatButton(const QString &text,
-                        QWidget *parent         = nullptr,
-                        ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-    FlatButton(const QString &text,
-               ui::Role role,
-               QWidget *parent         = nullptr,
-               ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
-
-    void applyPreset(ui::ButtonPreset preset);
-
-    void setBackgroundColor(const QColor &color);
-    void setBackgroundMode(Qt::BGMode mode);
-    void setBaseOpacity(qreal opacity);
-    void setCheckable(bool value);
-    void setCornerRadius(qreal radius);
-    void setDisabledBackgroundColor(const QColor &color);
-    void setDisabledForegroundColor(const QColor &color);
-    void setFixedRippleRadius(qreal radius);
-    void setFontSize(qreal size);
-    void setForegroundColor(const QColor &color);
-    void setHasFixedRippleRadius(bool value);
-    void setIconPlacement(ui::ButtonIconPlacement placement);
-    void setOverlayColor(const QColor &color);
-    void setOverlayStyle(ui::OverlayStyle style);
-    void setRippleStyle(ui::RippleStyle style);
-    void setRole(ui::Role role);
-
-    QColor foregroundColor() const;
-    QColor backgroundColor() const;
-    QColor overlayColor() const;
-    QColor disabledForegroundColor() const;
-    QColor disabledBackgroundColor() const;
-
-    qreal fontSize() const;
-    qreal cornerRadius() const;
-    qreal baseOpacity() const;
-
-    bool hasFixedRippleRadius() const;
-
-    ui::Role role() const;
-    ui::OverlayStyle overlayStyle() const;
-    ui::RippleStyle rippleStyle() const;
-    ui::ButtonIconPlacement iconPlacement() const;
-
-    Qt::BGMode backgroundMode() const;
-
-    QSize sizeHint() const override;
-
-protected:
-    int IconPadding = 0;
-
-    void checkStateSet() override;
-    void mousePressEvent(QMouseEvent *event) override;
-    void mouseReleaseEvent(QMouseEvent *event) override;
-    void resizeEvent(QResizeEvent *event) override;
-    void paintEvent(QPaintEvent *event) override;
-
-    virtual void paintBackground(QPainter *painter);
-    virtual void paintForeground(QPainter *painter);
-    virtual void updateClipPath();
-
-    void init();
-
-signals:
-    void foregroundColorChanged();
-    void backgroundColorChanged();
-    void overlayColorChanged();
-    void disabledForegroundColorChanged();
-    void disabledBackgroundColorChanged();
-    void fontSizeChanged();
-
-private:
-    RippleOverlay *ripple_overlay_;
-    FlatButtonStateMachine *state_machine_;
-
-    ui::Role role_;
-    ui::RippleStyle ripple_style_;
-    ui::ButtonIconPlacement icon_placement_;
-    ui::OverlayStyle overlay_style_;
-
-    Qt::BGMode bg_mode_;
-
-    QColor background_color_;
-    QColor foreground_color_;
-    QColor overlay_color_;
-    QColor disabled_color_;
-    QColor disabled_background_color_;
-
-    qreal fixed_ripple_radius_;
-    qreal corner_radius_;
-    qreal base_opacity_;
-    qreal font_size_;
-
-    bool use_fixed_ripple_radius_;
-};
diff --git a/src/ui/Label.cpp b/src/ui/Label.cpp
deleted file mode 100644
index 40d2cfa4ae63bf27447d6db939faa675b09e7119..0000000000000000000000000000000000000000
--- a/src/ui/Label.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "Label.h"
-#include <QMouseEvent>
-
-Label::Label(QWidget *parent, Qt::WindowFlags f)
-  : QLabel(parent, f)
-{}
-
-Label::Label(const QString &text, QWidget *parent, Qt::WindowFlags f)
-  : QLabel(text, parent, f)
-{}
-
-void
-Label::mousePressEvent(QMouseEvent *e)
-{
-    pressPosition_ = e->pos();
-    emit pressed(e);
-    QLabel::mousePressEvent(e);
-}
-
-void
-Label::mouseReleaseEvent(QMouseEvent *e)
-{
-    emit released(e);
-    if (pressPosition_ == e->pos())
-        emit clicked(e);
-    QLabel::mouseReleaseEvent(e);
-}
diff --git a/src/ui/Label.h b/src/ui/Label.h
deleted file mode 100644
index 034e3c6c19349c0cf672ce1a5163df4f2d72af35..0000000000000000000000000000000000000000
--- a/src/ui/Label.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QLabel>
-
-class Label : public QLabel
-{
-    Q_OBJECT
-
-public:
-    explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
-    explicit Label(const QString &text,
-                   QWidget *parent   = Q_NULLPTR,
-                   Qt::WindowFlags f = Qt::WindowFlags());
-
-signals:
-    void clicked(QMouseEvent *e);
-    void pressed(QMouseEvent *e);
-    void released(QMouseEvent *e);
-
-protected:
-    void mousePressEvent(QMouseEvent *e) override;
-    void mouseReleaseEvent(QMouseEvent *e) override;
-
-    QPoint pressPosition_;
-};
diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp
deleted file mode 100644
index 151f07507ab77652b0738886c74d4c41edfc36ef..0000000000000000000000000000000000000000
--- a/src/ui/LoadingIndicator.cpp
+++ /dev/null
@@ -1,84 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "LoadingIndicator.h"
-
-#include <QPaintEvent>
-#include <QPainter>
-#include <QTimer>
-
-LoadingIndicator::LoadingIndicator(QWidget *parent)
-  : QWidget(parent)
-  , interval_(70)
-  , angle_(0)
-  , color_(Qt::black)
-{
-    setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
-    setFocusPolicy(Qt::NoFocus);
-
-    timer_ = new QTimer(this);
-    connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout()));
-}
-
-void
-LoadingIndicator::paintEvent(QPaintEvent *e)
-{
-    Q_UNUSED(e)
-
-    if (!timer_->isActive())
-        return;
-
-    QPainter painter(this);
-    painter.setRenderHint(QPainter::Antialiasing);
-
-    int width = qMin(this->width(), this->height());
-
-    int outerRadius = (width - 4) * 0.5f;
-    int innerRadius = outerRadius * 0.78f;
-
-    int capsuleRadius = (outerRadius - innerRadius) / 2;
-
-    for (int i = 0; i < 8; ++i) {
-        QColor color = color_;
-
-        color.setAlphaF(1.0f - (i / 8.0f));
-
-        painter.setPen(Qt::NoPen);
-        painter.setBrush(color);
-
-        qreal radius = capsuleRadius * (1.0f - (i / 16.0f));
-
-        painter.save();
-
-        painter.translate(rect().center());
-        painter.rotate(angle_ - i * 45.0f);
-
-        QPointF center = QPointF(-capsuleRadius, -innerRadius);
-        painter.drawEllipse(center, radius * 2, radius * 2);
-
-        painter.restore();
-    }
-}
-
-void
-LoadingIndicator::start()
-{
-    timer_->start(interval_);
-    show();
-}
-
-void
-LoadingIndicator::stop()
-{
-    timer_->stop();
-    hide();
-}
-
-void
-LoadingIndicator::onTimeout()
-{
-    angle_ = (angle_ + 45) % 360;
-    repaint();
-}
diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h
deleted file mode 100644
index 6d3f2a8986d6d9ecb2678df996ce50ece0aa5900..0000000000000000000000000000000000000000
--- a/src/ui/LoadingIndicator.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QColor>
-#include <QWidget>
-
-class QPainter;
-class QTimer;
-class QPaintEvent;
-class LoadingIndicator : public QWidget
-{
-    Q_OBJECT
-    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
-
-public:
-    LoadingIndicator(QWidget *parent = nullptr);
-
-    void paintEvent(QPaintEvent *e) override;
-
-    void start();
-    void stop();
-
-    QColor color() { return color_; }
-    void setColor(QColor color)
-    {
-        color_ = color;
-        emit colorChanged();
-    }
-
-    int interval() { return interval_; }
-    void setInterval(int interval) { interval_ = interval; }
-
-private slots:
-    void onTimeout();
-
-signals:
-    void colorChanged();
-
-private:
-    int interval_;
-    int angle_;
-
-    QColor color_;
-    QTimer *timer_;
-};
diff --git a/src/ui/Menu.h b/src/ui/Menu.h
deleted file mode 100644
index a666229a073694a68b2babff825c33241b61b478..0000000000000000000000000000000000000000
--- a/src/ui/Menu.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QMenu>
-
-#include "Config.h"
-
-class Menu : public QMenu
-{
-    Q_OBJECT
-public:
-    Menu(QWidget *parent = nullptr)
-      : QMenu(parent){};
-
-protected:
-    void leaveEvent(QEvent *e) override
-    {
-        hide();
-
-        QMenu::leaveEvent(e);
-    }
-};
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index 450cb0d01016f35ebebe96812ec8d56a1a257a69..3d8d99591242d60cac471ca04a04c6dfeb8b8274 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -139,5 +139,5 @@ Nheko::openCreateRoomDialog() const
 void
 Nheko::reparent(QWindow *win) const
 {
-    win->setTransientParent(MainWindow::instance()->windowHandle());
+    win->setTransientParent(MainWindow::instance());
 }
diff --git a/src/ui/OverlayModal.cpp b/src/ui/OverlayModal.cpp
deleted file mode 100644
index 88334ce80695628834b386c34ff73d439634cd28..0000000000000000000000000000000000000000
--- a/src/ui/OverlayModal.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QPainter>
-#include <QVBoxLayout>
-
-#include "OverlayModal.h"
-
-OverlayModal::OverlayModal(QWidget *parent)
-  : OverlayWidget(parent)
-  , color_{QColor(30, 30, 30, 170)}
-{
-    layout_ = new QVBoxLayout(this);
-    layout_->setSpacing(0);
-    layout_->setContentsMargins(10, 40, 10, 20);
-    setContentAlignment(Qt::AlignCenter);
-}
-
-void
-OverlayModal::setWidget(QWidget *widget)
-{
-    // Delete the previous widget
-    if (layout_->count() > 0) {
-        QLayoutItem *item;
-        while ((item = layout_->takeAt(0)) != nullptr) {
-            delete item->widget();
-            delete item;
-        }
-    }
-
-    layout_->addWidget(widget);
-    content_ = widget;
-    content_->setFocus();
-}
-
-void
-OverlayModal::paintEvent(QPaintEvent *event)
-{
-    Q_UNUSED(event);
-
-    QPainter painter(this);
-    painter.fillRect(rect(), color_);
-}
-
-void
-OverlayModal::mousePressEvent(QMouseEvent *e)
-{
-    if (isDismissible_ && content_ && !content_->geometry().contains(e->pos()))
-        hide();
-}
-
-void
-OverlayModal::keyPressEvent(QKeyEvent *event)
-{
-    if (event->key() == Qt::Key_Escape) {
-        event->accept();
-        hide();
-    }
-}
diff --git a/src/ui/OverlayModal.h b/src/ui/OverlayModal.h
deleted file mode 100644
index 5e7ecc20112a78cb57470b3d92c4bf35592d6dfd..0000000000000000000000000000000000000000
--- a/src/ui/OverlayModal.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QKeyEvent>
-#include <QMouseEvent>
-#include <QPaintEvent>
-#include <QVBoxLayout>
-
-#include "OverlayWidget.h"
-
-class OverlayModal : public OverlayWidget
-{
-public:
-    OverlayModal(QWidget *parent);
-
-    void setColor(QColor color) { color_ = color; }
-    void setDismissible(bool state) { isDismissible_ = state; }
-
-    void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); }
-    void setWidget(QWidget *widget);
-
-protected:
-    void paintEvent(QPaintEvent *event) override;
-    void keyPressEvent(QKeyEvent *event) override;
-    void mousePressEvent(QMouseEvent *event) override;
-
-private:
-    QWidget *content_;
-    QVBoxLayout *layout_;
-
-    QColor color_;
-
-    //! Decides whether or not the modal can be removed by clicking into it.
-    bool isDismissible_ = true;
-};
diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp
deleted file mode 100644
index b755a44cfe456392613dbb136fb6fb354a2aea7e..0000000000000000000000000000000000000000
--- a/src/ui/OverlayWidget.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "OverlayWidget.h"
-
-#include <QPainter>
-#include <QStyleOption>
-
-OverlayWidget::OverlayWidget(QWidget *parent)
-  : QWidget(parent)
-{
-    if (parent) {
-        parent->installEventFilter(this);
-        setGeometry(overlayGeometry());
-        raise();
-    }
-}
-
-bool
-OverlayWidget::event(QEvent *event)
-{
-    if (!parent())
-        return QWidget::event(event);
-
-    switch (event->type()) {
-    case QEvent::ParentChange: {
-        parent()->installEventFilter(this);
-        setGeometry(overlayGeometry());
-        break;
-    }
-    case QEvent::ParentAboutToChange: {
-        parent()->removeEventFilter(this);
-        break;
-    }
-    default:
-        break;
-    }
-
-    return QWidget::event(event);
-}
-
-bool
-OverlayWidget::eventFilter(QObject *obj, QEvent *event)
-{
-    switch (event->type()) {
-    case QEvent::Move:
-    case QEvent::Resize:
-        setGeometry(overlayGeometry());
-        break;
-    default:
-        break;
-    }
-
-    return QWidget::eventFilter(obj, event);
-}
-
-QRect
-OverlayWidget::overlayGeometry() const
-{
-    QWidget *widget = parentWidget();
-
-    if (!widget)
-        return QRect();
-
-    return widget->rect();
-}
-
-void
-OverlayWidget::paintEvent(QPaintEvent *event)
-{
-    Q_UNUSED(event);
-
-    QStyleOption opt;
-    opt.initFrom(this);
-    QPainter p(this);
-    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h
deleted file mode 100644
index 19ad0cc6a9dbedb1bf1f796fadfeaf67479b5d7a..0000000000000000000000000000000000000000
--- a/src/ui/OverlayWidget.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QEvent>
-#include <QWidget>
-
-class QPainter;
-
-class OverlayWidget : public QWidget
-{
-    Q_OBJECT
-
-public:
-    explicit OverlayWidget(QWidget *parent = nullptr);
-
-protected:
-    bool event(QEvent *event) override;
-    bool eventFilter(QObject *obj, QEvent *event) override;
-
-    QRect overlayGeometry() const;
-    void paintEvent(QPaintEvent *event) override;
-};
diff --git a/src/ui/Painter.h b/src/ui/Painter.h
deleted file mode 100644
index 5a7dae3e0b53d1faa38b206ff6f4d9fbe966cd95..0000000000000000000000000000000000000000
--- a/src/ui/Painter.h
+++ /dev/null
@@ -1,154 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFontMetrics>
-#include <QPaintDevice>
-#include <QPainter>
-#include <QPainterPath>
-#include <QtGlobal>
-
-class Painter : public QPainter
-{
-public:
-    explicit Painter(QPaintDevice *device)
-      : QPainter(device)
-    {}
-
-    void drawTextLeft(int x, int y, const QString &text)
-    {
-        QFontMetrics m(fontMetrics());
-        drawText(x, y + m.ascent(), text);
-    }
-
-    void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1)
-    {
-        QFontMetrics m(fontMetrics());
-        if (textWidth < 0) {
-            textWidth = m.horizontalAdvance(text);
-        }
-        drawText((outerw - x - textWidth), y + m.ascent(), text);
-    }
-
-    void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from)
-    {
-        drawPixmap(QPoint(x, y), pix, from);
-    }
-
-    void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from)
-    {
-        return drawPixmapLeft(p.x(), p.y(), pix, from);
-    }
-
-    void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from)
-    {
-        drawPixmap(QRect(x, y, w, h), pix, from);
-    }
-
-    void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from)
-    {
-        return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from);
-    }
-
-    void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix)
-    {
-        Q_UNUSED(outerw);
-        drawPixmap(QPoint(x, y), pix);
-    }
-
-    void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix)
-    {
-        return drawPixmapLeft(p.x(), p.y(), outerw, pix);
-    }
-
-    void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from)
-    {
-        drawPixmap(QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from);
-    }
-
-    void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from)
-    {
-        return drawPixmapRight(p.x(), p.y(), outerw, pix, from);
-    }
-    void
-    drawPixmapRight(int x, int y, int w, int h, int outerw, const QPixmap &pix, const QRect &from)
-    {
-        drawPixmap(QRect((outerw - x - w), y, w, h), pix, from);
-    }
-
-    void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from)
-    {
-        return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from);
-    }
-
-    void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix)
-    {
-        drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix);
-    }
-
-    void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix)
-    {
-        return drawPixmapRight(p.x(), p.y(), outerw, pix);
-    }
-
-    void drawAvatar(const QPixmap &pix, int w, int h, int d)
-    {
-        QPainterPath pp;
-        pp.addEllipse((w - d) / 2, (h - d) / 2, d, d);
-
-        QRect region((w - d) / 2, (h - d) / 2, d, d);
-
-        setClipPath(pp);
-        drawPixmap(region, pix);
-    }
-
-    void drawLetterAvatar(const QString &c,
-                          const QColor &penColor,
-                          const QColor &brushColor,
-                          int w,
-                          int h,
-                          int d)
-    {
-        QRect region((w - d) / 2, (h - d) / 2, d, d);
-
-        setPen(Qt::NoPen);
-        setBrush(brushColor);
-
-        drawEllipse(region.center(), d / 2, d / 2);
-
-        setBrush(Qt::NoBrush);
-        drawEllipse(region.center(), d / 2, d / 2);
-
-        setPen(penColor);
-        drawText(region.translated(0, -1), Qt::AlignCenter, c);
-    }
-};
-
-class PainterHighQualityEnabler
-{
-public:
-    PainterHighQualityEnabler(Painter &p)
-      : _painter(p)
-    {
-        hints_ =
-          QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing;
-
-        _painter.setRenderHints(hints_);
-    }
-
-    ~PainterHighQualityEnabler()
-    {
-        if (hints_)
-            _painter.setRenderHints(hints_, false);
-    }
-
-    PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete;
-    PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete;
-
-private:
-    Painter &_painter;
-    QPainter::RenderHints hints_ = {};
-};
diff --git a/src/ui/RaisedButton.cpp b/src/ui/RaisedButton.cpp
deleted file mode 100644
index 491ab573c682a7529e25eb06254b93cf66a6506e..0000000000000000000000000000000000000000
--- a/src/ui/RaisedButton.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QEventTransition>
-#include <QPropertyAnimation>
-
-#include "RaisedButton.h"
-
-void
-RaisedButton::init()
-{
-    shadow_state_machine_ = new QStateMachine(this);
-    normal_state_         = new QState;
-    pressed_state_        = new QState;
-    effect_               = new QGraphicsDropShadowEffect;
-
-    effect_->setBlurRadius(7);
-    effect_->setOffset(QPointF(0, 2));
-    effect_->setColor(QColor(0, 0, 0, 75));
-
-    setBackgroundMode(Qt::OpaqueMode);
-    setMinimumHeight(42);
-    setGraphicsEffect(effect_);
-    setBaseOpacity(0.3);
-
-    shadow_state_machine_->addState(normal_state_);
-    shadow_state_machine_->addState(pressed_state_);
-
-    normal_state_->assignProperty(effect_, "offset", QPointF(0, 2));
-    normal_state_->assignProperty(effect_, "blurRadius", 7);
-
-    pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5));
-    pressed_state_->assignProperty(effect_, "blurRadius", 29);
-
-    QAbstractTransition *transition;
-
-    transition = new QEventTransition(this, QEvent::MouseButtonPress);
-    transition->setTargetState(pressed_state_);
-    normal_state_->addTransition(transition);
-
-    transition = new QEventTransition(this, QEvent::MouseButtonDblClick);
-    transition->setTargetState(pressed_state_);
-    normal_state_->addTransition(transition);
-
-    transition = new QEventTransition(this, QEvent::MouseButtonRelease);
-    transition->setTargetState(normal_state_);
-    pressed_state_->addTransition(transition);
-
-    QPropertyAnimation *animation;
-
-    animation = new QPropertyAnimation(effect_, "offset", this);
-    animation->setDuration(100);
-    shadow_state_machine_->addDefaultAnimation(animation);
-
-    animation = new QPropertyAnimation(effect_, "blurRadius", this);
-    animation->setDuration(100);
-    shadow_state_machine_->addDefaultAnimation(animation);
-
-    shadow_state_machine_->setInitialState(normal_state_);
-    shadow_state_machine_->start();
-}
-
-RaisedButton::RaisedButton(QWidget *parent)
-  : FlatButton(parent)
-{
-    init();
-}
-
-RaisedButton::RaisedButton(const QString &text, QWidget *parent)
-  : FlatButton(parent)
-{
-    init();
-    setText(text);
-}
-
-bool
-RaisedButton::event(QEvent *event)
-{
-    if (QEvent::EnabledChange == event->type()) {
-        if (isEnabled()) {
-            shadow_state_machine_->start();
-            effect_->setEnabled(true);
-        } else {
-            shadow_state_machine_->stop();
-            effect_->setEnabled(false);
-        }
-    }
-
-    return FlatButton::event(event);
-}
diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h
deleted file mode 100644
index 7464c207ff5570c194647620527c2bd9b5669268..0000000000000000000000000000000000000000
--- a/src/ui/RaisedButton.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QGraphicsDropShadowEffect>
-#include <QState>
-#include <QStateMachine>
-
-#include "FlatButton.h"
-
-class RaisedButton : public FlatButton
-{
-    Q_OBJECT
-
-public:
-    explicit RaisedButton(QWidget *parent = nullptr);
-    explicit RaisedButton(const QString &text, QWidget *parent = nullptr);
-
-protected:
-    bool event(QEvent *event) override;
-
-private:
-    void init();
-
-    QStateMachine *shadow_state_machine_;
-    QState *normal_state_;
-    QState *pressed_state_;
-    QGraphicsDropShadowEffect *effect_;
-};
diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp
deleted file mode 100644
index 84c0b3940d0be64e2019bd5b5ce491013d48efde..0000000000000000000000000000000000000000
--- a/src/ui/Ripple.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "Ripple.h"
-#include "RippleOverlay.h"
-
-Ripple::Ripple(const QPoint &center, QObject *parent)
-  : QParallelAnimationGroup(parent)
-  , overlay_(nullptr)
-  , radius_anim_(animate("radius"))
-  , opacity_anim_(animate("opacity"))
-  , radius_(0)
-  , opacity_(0)
-  , center_(center)
-{
-    init();
-}
-
-Ripple::Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent)
-  : QParallelAnimationGroup(parent)
-  , overlay_(overlay)
-  , radius_anim_(animate("radius"))
-  , opacity_anim_(animate("opacity"))
-  , radius_(0)
-  , opacity_(0)
-  , center_(center)
-{
-    init();
-}
-
-void
-Ripple::setRadius(qreal radius)
-{
-    Q_ASSERT(overlay_);
-
-    if (radius_ == radius)
-        return;
-
-    radius_ = radius;
-    overlay_->update();
-
-    emit radiusChanged();
-}
-
-void
-Ripple::setOpacity(qreal opacity)
-{
-    Q_ASSERT(overlay_);
-
-    if (opacity_ == opacity)
-        return;
-
-    opacity_ = opacity;
-    overlay_->update();
-
-    emit opacityChanged();
-}
-
-void
-Ripple::setColor(const QColor &color)
-{
-    if (brush_.color() == color)
-        return;
-
-    brush_.setColor(color);
-
-    if (overlay_)
-        overlay_->update();
-}
-
-void
-Ripple::setBrush(const QBrush &brush)
-{
-    brush_ = brush;
-
-    if (overlay_)
-        overlay_->update();
-}
-
-void
-Ripple::destroy()
-{
-    Q_ASSERT(overlay_);
-
-    overlay_->removeRipple(this);
-}
-
-QPropertyAnimation *
-Ripple::animate(const QByteArray &property, const QEasingCurve &easing, int duration)
-{
-    QPropertyAnimation *animation = new QPropertyAnimation;
-    animation->setTargetObject(this);
-    animation->setPropertyName(property);
-    animation->setEasingCurve(easing);
-    animation->setDuration(duration);
-
-    addAnimation(animation);
-
-    return animation;
-}
-
-void
-Ripple::init()
-{
-    setOpacityStartValue(0.5);
-    setOpacityEndValue(0);
-    setRadiusStartValue(0);
-    setRadiusEndValue(300);
-
-    brush_.setColor(Qt::black);
-    brush_.setStyle(Qt::SolidPattern);
-
-    connect(this, SIGNAL(finished()), this, SLOT(destroy()));
-}
diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h
deleted file mode 100644
index 43d291cbcbcb315a000b5dd928fc3bd4512aec93..0000000000000000000000000000000000000000
--- a/src/ui/Ripple.h
+++ /dev/null
@@ -1,154 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QBrush>
-#include <QEasingCurve>
-#include <QParallelAnimationGroup>
-#include <QPoint>
-#include <QPropertyAnimation>
-
-class RippleOverlay;
-
-class Ripple : public QParallelAnimationGroup
-{
-    Q_OBJECT
-
-    Q_PROPERTY(qreal radius WRITE setRadius READ radius NOTIFY radiusChanged)
-    Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity NOTIFY opacityChanged)
-
-public:
-    explicit Ripple(const QPoint &center, QObject *parent = nullptr);
-    Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = nullptr);
-
-    inline void setOverlay(RippleOverlay *overlay);
-
-    void setRadius(qreal radius);
-    void setOpacity(qreal opacity);
-    void setColor(const QColor &color);
-    void setBrush(const QBrush &brush);
-
-    inline qreal radius() const;
-    inline qreal opacity() const;
-    inline QColor color() const;
-    inline QBrush brush() const;
-    inline QPoint center() const;
-
-    inline QPropertyAnimation *radiusAnimation() const;
-    inline QPropertyAnimation *opacityAnimation() const;
-
-    inline void setOpacityStartValue(qreal value);
-    inline void setOpacityEndValue(qreal value);
-    inline void setRadiusStartValue(qreal value);
-    inline void setRadiusEndValue(qreal value);
-    inline void setDuration(int msecs);
-
-protected slots:
-    void destroy();
-
-signals:
-    void radiusChanged();
-    void opacityChanged();
-
-private:
-    Q_DISABLE_COPY(Ripple)
-
-    QPropertyAnimation *animate(const QByteArray &property,
-                                const QEasingCurve &easing = QEasingCurve::OutQuad,
-                                int duration               = 800);
-
-    void init();
-
-    RippleOverlay *overlay_;
-
-    QPropertyAnimation *const radius_anim_;
-    QPropertyAnimation *const opacity_anim_;
-
-    qreal radius_;
-    qreal opacity_;
-
-    QPoint center_;
-    QBrush brush_;
-};
-
-inline void
-Ripple::setOverlay(RippleOverlay *overlay)
-{
-    overlay_ = overlay;
-}
-
-inline qreal
-Ripple::radius() const
-{
-    return radius_;
-}
-
-inline qreal
-Ripple::opacity() const
-{
-    return opacity_;
-}
-
-inline QColor
-Ripple::color() const
-{
-    return brush_.color();
-}
-
-inline QBrush
-Ripple::brush() const
-{
-    return brush_;
-}
-
-inline QPoint
-Ripple::center() const
-{
-    return center_;
-}
-
-inline QPropertyAnimation *
-Ripple::radiusAnimation() const
-{
-    return radius_anim_;
-}
-
-inline QPropertyAnimation *
-Ripple::opacityAnimation() const
-{
-    return opacity_anim_;
-}
-
-inline void
-Ripple::setOpacityStartValue(qreal value)
-{
-    opacity_anim_->setStartValue(value);
-}
-
-inline void
-Ripple::setOpacityEndValue(qreal value)
-{
-    opacity_anim_->setEndValue(value);
-}
-
-inline void
-Ripple::setRadiusStartValue(qreal value)
-{
-    radius_anim_->setStartValue(value);
-}
-
-inline void
-Ripple::setRadiusEndValue(qreal value)
-{
-    radius_anim_->setEndValue(value);
-}
-
-inline void
-Ripple::setDuration(int msecs)
-{
-    radius_anim_->setDuration(msecs);
-    opacity_anim_->setDuration(msecs);
-}
diff --git a/src/ui/RippleOverlay.cpp b/src/ui/RippleOverlay.cpp
deleted file mode 100644
index 97ad8662bd41b4ecd16ddd623e1e23b422924746..0000000000000000000000000000000000000000
--- a/src/ui/RippleOverlay.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QPainter>
-
-#include "Ripple.h"
-#include "RippleOverlay.h"
-
-RippleOverlay::RippleOverlay(QWidget *parent)
-  : OverlayWidget(parent)
-  , use_clip_(false)
-{
-    setAttribute(Qt::WA_TransparentForMouseEvents);
-    setAttribute(Qt::WA_NoSystemBackground);
-}
-
-void
-RippleOverlay::addRipple(Ripple *ripple)
-{
-    ripple->setOverlay(this);
-    ripples_.push_back(ripple);
-    ripple->start();
-}
-
-void
-RippleOverlay::addRipple(const QPoint &position, qreal radius)
-{
-    Ripple *ripple = new Ripple(position);
-    ripple->setRadiusEndValue(radius);
-    addRipple(ripple);
-}
-
-void
-RippleOverlay::removeRipple(Ripple *ripple)
-{
-    if (ripples_.removeOne(ripple))
-        delete ripple;
-}
-
-void
-RippleOverlay::paintEvent(QPaintEvent *event)
-{
-    Q_UNUSED(event)
-
-    QPainter painter(this);
-    painter.setRenderHint(QPainter::Antialiasing);
-    painter.setPen(Qt::NoPen);
-
-    if (use_clip_)
-        painter.setClipPath(clip_path_);
-
-    for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it)
-        paintRipple(&painter, *it);
-}
-
-void
-RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple)
-{
-    const qreal radius   = ripple->radius();
-    const QPointF center = ripple->center();
-
-    painter->setOpacity(ripple->opacity());
-    painter->setBrush(ripple->brush());
-    painter->drawEllipse(center, radius, radius);
-}
diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h
deleted file mode 100644
index f70d00a79571b9b4ac8ab373c3ef9de04164105b..0000000000000000000000000000000000000000
--- a/src/ui/RippleOverlay.h
+++ /dev/null
@@ -1,62 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QPainterPath>
-
-#include "OverlayWidget.h"
-
-class Ripple;
-
-class RippleOverlay : public OverlayWidget
-{
-    Q_OBJECT
-
-public:
-    explicit RippleOverlay(QWidget *parent = nullptr);
-
-    void addRipple(Ripple *ripple);
-    void addRipple(const QPoint &position, qreal radius = 300);
-
-    void removeRipple(Ripple *ripple);
-
-    inline void setClipping(bool enable);
-    inline bool hasClipping() const;
-
-    inline void setClipPath(const QPainterPath &path);
-
-protected:
-    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
-
-private:
-    Q_DISABLE_COPY(RippleOverlay)
-
-    void paintRipple(QPainter *painter, Ripple *ripple);
-
-    QList<Ripple *> ripples_;
-    QPainterPath clip_path_;
-    bool use_clip_;
-};
-
-inline void
-RippleOverlay::setClipping(bool enable)
-{
-    use_clip_ = enable;
-    update();
-}
-
-inline bool
-RippleOverlay::hasClipping() const
-{
-    return use_clip_;
-}
-
-inline void
-RippleOverlay::setClipPath(const QPainterPath &path)
-{
-    clip_path_ = path;
-    update();
-}
diff --git a/src/ui/SnackBar.cpp b/src/ui/SnackBar.cpp
deleted file mode 100644
index 50c3d3f99a42cb7afa19c7345f4d780411c2196c..0000000000000000000000000000000000000000
--- a/src/ui/SnackBar.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QPainter>
-
-#include "SnackBar.h"
-
-constexpr int STARTING_OFFSET         = 1;
-constexpr int BOX_PADDING             = 10;
-constexpr double MIN_WIDTH            = 400.0;
-constexpr double MIN_WIDTH_PERCENTAGE = 0.3;
-
-SnackBar::SnackBar(QWidget *parent)
-  : OverlayWidget(parent)
-  , offset_anim(this, "offset", this)
-{
-    QFont font;
-    font.setPointSizeF(font.pointSizeF() * 1.2);
-    font.setWeight(QFont::Weight::Thin);
-    setFont(font);
-
-    boxHeight_ = QFontMetrics(font).height() * 2;
-    offset_    = STARTING_OFFSET;
-    position_  = SnackBarPosition::Top;
-
-    hideTimer_.setSingleShot(true);
-
-    offset_anim.setStartValue(1.0);
-    offset_anim.setEndValue(0.0);
-    offset_anim.setDuration(100);
-    offset_anim.setEasingCurve(QEasingCurve::OutCubic);
-
-    connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); });
-    connect(
-      &offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); });
-
-    connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage()));
-
-    hide();
-}
-
-void
-SnackBar::start()
-{
-    if (messages_.empty())
-        return;
-
-    show();
-    raise();
-
-    offset_anim.start();
-}
-
-void
-SnackBar::hideMessage()
-{
-    stopTimers();
-    hide();
-
-    if (!messages_.empty())
-        // Moving on to the next message.
-        messages_.pop_front();
-
-    // Resetting the starting position of the widget.
-    offset_ = STARTING_OFFSET;
-
-    if (!messages_.empty())
-        start();
-}
-
-void
-SnackBar::stopTimers()
-{
-    hideTimer_.stop();
-}
-
-void
-SnackBar::showMessage(const QString &msg)
-{
-    messages_.push_back(msg);
-
-    // There is already an active message.
-    if (isVisible())
-        return;
-
-    start();
-}
-
-void
-SnackBar::mousePressEvent(QMouseEvent *)
-{
-    hideMessage();
-}
-
-void
-SnackBar::paintEvent(QPaintEvent *event)
-{
-    Q_UNUSED(event)
-
-    if (messages_.empty())
-        return;
-
-    auto message_ = messages_.front();
-
-    QPainter p(this);
-    p.setRenderHint(QPainter::Antialiasing);
-
-    QBrush brush;
-    brush.setStyle(Qt::SolidPattern);
-    brush.setColor(bgColor_);
-    p.setBrush(brush);
-
-    QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_);
-
-    p.setPen(Qt::white);
-    QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
-
-    p.setPen(Qt::NoPen);
-    r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING);
-
-    const qreal s = 1 - offset_;
-
-    if (position_ == SnackBarPosition::Bottom)
-        p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
-                    height() - BOX_PADDING - s * (r.height()));
-    else
-        p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
-                    s * (r.height()) - 2 * BOX_PADDING);
-
-    br.moveCenter(r.center());
-    p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4);
-    p.setPen(textColor_);
-    p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
-}
diff --git a/src/ui/SnackBar.h b/src/ui/SnackBar.h
deleted file mode 100644
index caac68aa291a0abcb20dea164a4cfa0c6572f1f2..0000000000000000000000000000000000000000
--- a/src/ui/SnackBar.h
+++ /dev/null
@@ -1,98 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QCoreApplication>
-#include <QPaintEvent>
-#include <QPropertyAnimation>
-#include <QTimer>
-#include <deque>
-
-#include "OverlayWidget.h"
-
-enum class SnackBarPosition
-{
-    Bottom,
-    Top,
-};
-
-class SnackBar : public OverlayWidget
-{
-    Q_OBJECT
-
-    Q_PROPERTY(
-      QColor bgColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
-    Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
-    Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged)
-
-public:
-    explicit SnackBar(QWidget *parent);
-
-    QColor backgroundColor() const { return bgColor_; }
-    void setBackgroundColor(const QColor &color)
-    {
-        bgColor_ = color;
-        update();
-        emit backgroundColorChanged();
-    }
-
-    QColor textColor() const { return textColor_; }
-    void setTextColor(const QColor &color)
-    {
-        textColor_ = color;
-        update();
-        emit textColorChanged();
-    }
-    void setPosition(SnackBarPosition pos)
-    {
-        position_ = pos;
-        update();
-    }
-
-    double offset() { return offset_; }
-    void setOffset(double offset)
-    {
-        if (offset != offset_) {
-            offset_ = offset;
-            emit offsetChanged();
-        }
-    }
-
-public slots:
-    void showMessage(const QString &msg);
-
-signals:
-    void offsetChanged();
-    void backgroundColorChanged();
-    void textColorChanged();
-
-protected:
-    void paintEvent(QPaintEvent *event) override;
-    void mousePressEvent(QMouseEvent *event) override;
-
-private slots:
-    void hideMessage();
-
-private:
-    void stopTimers();
-    void start();
-
-    QColor bgColor_;
-    QColor textColor_;
-
-    qreal bgOpacity_;
-    qreal offset_;
-
-    std::deque<QString> messages_;
-
-    QTimer hideTimer_;
-
-    double boxHeight_;
-
-    QPropertyAnimation offset_anim;
-
-    SnackBarPosition position_;
-};
diff --git a/src/ui/TextLabel.cpp b/src/ui/TextLabel.cpp
deleted file mode 100644
index 246d9d6babc856e52a1687810e5faca515c5170d..0000000000000000000000000000000000000000
--- a/src/ui/TextLabel.cpp
+++ /dev/null
@@ -1,123 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "ui/TextLabel.h"
-
-#include <QAbstractTextDocumentLayout>
-#include <QDesktopServices>
-#include <QEvent>
-#include <QWheelEvent>
-
-#include "Utils.h"
-
-bool
-ContextMenuFilter::eventFilter(QObject *obj, QEvent *event)
-{
-    if (event->type() == QEvent::MouseButtonPress) {
-        emit contextMenuIsOpening();
-        return true;
-    }
-
-    return QObject::eventFilter(obj, event);
-}
-
-TextLabel::TextLabel(QWidget *parent)
-  : TextLabel(QString(), parent)
-{}
-
-TextLabel::TextLabel(const QString &text, QWidget *parent)
-  : QTextBrowser(parent)
-{
-    document()->setDefaultStyleSheet(QStringLiteral("a {color: %1; }").arg(utils::linkColor()));
-
-    setText(text);
-    setOpenExternalLinks(true);
-
-    // Make it look and feel like an ordinary label.
-    setReadOnly(true);
-    setFrameStyle(QFrame::NoFrame);
-    QPalette pal = palette();
-    pal.setColor(QPalette::Base, Qt::transparent);
-    setPalette(pal);
-
-    // Wrap anywhere but prefer words, adjust minimum height on the fly.
-    setLineWrapMode(QTextEdit::WidgetWidth);
-    setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
-    connect(document()->documentLayout(),
-            &QAbstractTextDocumentLayout::documentSizeChanged,
-            this,
-            &TextLabel::adjustHeight);
-    document()->setDocumentMargin(0);
-
-    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-    setFixedHeight(0);
-
-    connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation);
-
-    auto filter = new ContextMenuFilter(this);
-    installEventFilter(filter);
-    connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() {
-        contextMenuRequested_ = true;
-    });
-}
-
-void
-TextLabel::focusOutEvent(QFocusEvent *e)
-{
-    QTextBrowser::focusOutEvent(e);
-
-    // We keep the selection available for the context menu.
-    if (contextMenuRequested_) {
-        contextMenuRequested_ = false;
-        return;
-    }
-
-    QTextCursor cursor = textCursor();
-    cursor.clearSelection();
-    setTextCursor(cursor);
-}
-
-void
-TextLabel::mousePressEvent(QMouseEvent *e)
-{
-    link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString();
-    QTextBrowser::mousePressEvent(e);
-}
-
-void
-TextLabel::mouseReleaseEvent(QMouseEvent *e)
-{
-    if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) {
-        emit linkActivated(link_);
-        return;
-    }
-
-    QTextBrowser::mouseReleaseEvent(e);
-}
-
-void
-TextLabel::wheelEvent(QWheelEvent *event)
-{
-    event->ignore();
-}
-
-void
-TextLabel::handleLinkActivation(const QUrl &url)
-{
-    auto parts          = url.toString().split('/');
-    auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); };
-
-    if (url.host() != QLatin1String("matrix.to") || parts.isEmpty())
-        return defaultHandler(url);
-
-    try {
-        using namespace mtx::identifiers;
-        parse<User>(parts.last().toStdString());
-    } catch (const std::exception &) {
-        return defaultHandler(url);
-    }
-
-    emit userProfileTriggered(parts.last());
-}
diff --git a/src/ui/TextLabel.h b/src/ui/TextLabel.h
deleted file mode 100644
index 2c6f4aa318030ceacce954e0267e6ef9d03baece..0000000000000000000000000000000000000000
--- a/src/ui/TextLabel.h
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-// SPDX-FileCopyrightText: 2022 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QSize>
-#include <QString>
-#include <QTextBrowser>
-#include <QUrl>
-
-class QMouseEvent;
-class QFocusEvent;
-class QWheelEvent;
-
-class ContextMenuFilter : public QObject
-{
-    Q_OBJECT
-
-public:
-    explicit ContextMenuFilter(QWidget *parent)
-      : QObject(parent)
-    {}
-
-signals:
-    void contextMenuIsOpening();
-
-protected:
-    bool eventFilter(QObject *obj, QEvent *event) override;
-};
-
-class TextLabel : public QTextBrowser
-{
-    Q_OBJECT
-
-public:
-    TextLabel(const QString &text, QWidget *parent = nullptr);
-    TextLabel(QWidget *parent = nullptr);
-
-    void wheelEvent(QWheelEvent *event) override;
-    void clearLinks() { link_.clear(); }
-
-protected:
-    void mousePressEvent(QMouseEvent *e) override;
-    void mouseReleaseEvent(QMouseEvent *e) override;
-    void focusOutEvent(QFocusEvent *e) override;
-
-private slots:
-    void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); }
-    void handleLinkActivation(const QUrl &link);
-
-signals:
-    void userProfileTriggered(const QString &user_id);
-    void linkActivated(const QUrl &link);
-
-private:
-    QString link_;
-    bool contextMenuRequested_ = false;
-};
diff --git a/src/ui/UIA.cpp b/src/ui/UIA.cpp
index 291d0a9f1bb143b2c4c62f16f0f44419e67a32db..9f28ca6ad846df654d63486786d810b5fd77871d 100644
--- a/src/ui/UIA.cpp
+++ b/src/ui/UIA.cpp
@@ -13,7 +13,6 @@
 #include <mtx/responses/common.hpp>
 
 #include "Logging.h"
-#include "MainWindow.h"
 #include "dialogs/FallbackAuth.h"
 #include "dialogs/ReCaptcha.h"
 
@@ -71,7 +70,7 @@ UIA::genericHandler(QString context)
                 emit phoneNumber();
             } else if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
                 auto captchaDialog =
-                  new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance());
+                  new dialogs::ReCaptcha(QString::fromStdString(u.session), nullptr);
                 captchaDialog->setWindowTitle(context);
 
                 connect(
@@ -95,7 +94,7 @@ UIA::genericHandler(QString context)
             } else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
                 bool ok;
                 QString token =
-                  QInputDialog::getText(MainWindow::instance(),
+                  QInputDialog::getText(nullptr,
                                         context,
                                         tr("Please enter a valid registration token."),
                                         QLineEdit::Normal,
@@ -113,7 +112,7 @@ UIA::genericHandler(QString context)
                 // use fallback
                 auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage),
                                                         QString::fromStdString(u.session),
-                                                        MainWindow::instance());
+                                                        nullptr);
                 dialog->setWindowTitle(context);
 
                 connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() {