diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3e5c2f0997a055d8c29f404ee45980523fbd108e..dad34349671fda596a6cffa3b1c0f0cfe83dd93d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -281,7 +281,6 @@ set(SRC_FILES
 	src/dialogs/CreateRoom.cpp
 	src/dialogs/FallbackAuth.cpp
 	src/dialogs/ImageOverlay.cpp
-	src/dialogs/JoinRoom.cpp
 	src/dialogs/LeaveRoom.cpp
 	src/dialogs/Logout.cpp
 	src/dialogs/PreviewUploadOverlay.cpp
@@ -498,7 +497,6 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/dialogs/CreateRoom.h
 	src/dialogs/FallbackAuth.h
 	src/dialogs/ImageOverlay.h
-	src/dialogs/JoinRoom.h
 	src/dialogs/LeaveRoom.h
 	src/dialogs/Logout.h
 	src/dialogs/PreviewUploadOverlay.h
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 1b910592e56eb2eba276e9c8cdaeee778b7a56c6..c73d1f1d1c9a349906791e1805500396b3301858 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -118,6 +118,13 @@ Page {
         }
     }
 
+    Component {
+        id: joinRoomDialog
+
+        JoinRoomDialog {
+        }
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -148,6 +155,11 @@ Page {
             dialog.open();
         }
 
+        function onOpenJoinRoomDialog() {
+            var dialog = joinRoomDialog.createObject(timelineRoot);
+            dialog.show();
+        }
+
         target: Nheko
     }
 
diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..d3defa82aa4d77ae58ea4c3f85e7e14805ff3065
--- /dev/null
+++ b/resources/qml/dialogs/JoinRoomDialog.qml
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.12
+import QtQuick.Controls 2.5
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: joinRoomRoot
+
+    title: qsTr("Join room")
+    modality: Qt.WindowModal
+    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
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: dbb.rejected()
+    }
+
+    ColumnLayout {
+        spacing: Nheko.paddingMedium
+        anchors.margins: Nheko.paddingMedium
+        anchors.fill: parent
+
+        Label {
+            id: promptLabel
+
+            text: qsTr("Room ID or alias")
+            color: Nheko.colors.text
+        }
+
+        MatrixTextField {
+            id: input
+
+            focus: true
+            Layout.fillWidth: true
+            onAccepted: {
+                if (input.text.match("#.+?:.{3,}"))
+                    dbb.accepted();
+            }
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: dbb
+
+        onAccepted: {
+            Nheko.joinRoom(input.text);
+            joinRoomRoot.close();
+        }
+        onRejected: {
+            joinRoomRoot.close();
+        }
+
+        Button {
+            text: "Join"
+            enabled: input.text.match("#.+?:.{3,}")
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+        }
+
+        Button {
+            text: "Cancel"
+            DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+        }
+    }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 173206a7034ca4d779e2cc64874511fd22c197ce..9303ab5fcb72e9a85688ab1e8329e381c41b2a38 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -159,10 +159,19 @@
         <file>qml/device-verification/EmojiVerification.qml</file>
         <file>qml/device-verification/NewVerificationRequest.qml</file>
         <file>qml/device-verification/Failed.qml</file>
-        <file>qml/device-verification/Success.qml</file>
-        <file>qml/dialogs/InputDialog.qml</file>
+		<file>qml/device-verification/Success.qml</file>
         <file>qml/dialogs/ImagePackSettingsDialog.qml</file>
         <file>qml/dialogs/ImagePackEditorDialog.qml</file>
+		<file>qml/dialogs/InputDialog.qml</file>
+		<file>qml/dialogs/InviteDialog.qml</file>
+		<file>qml/dialogs/JoinRoomDialog.qml</file>
+		<file>qml/dialogs/LogoutDialog.qml</file>
+		<file>qml/dialogs/RawMessageDialog.qml</file>
+		<file>qml/dialogs/ReadReceipts.qml</file>
+		<file>qml/dialogs/RoomDirectory.qml</file>
+		<file>qml/dialogs/RoomMembers.qml</file>
+		<file>qml/dialogs/RoomSettings.qml</file>
+		<file>qml/dialogs/UserProfile.qml</file>
         <file>qml/ui/Ripple.qml</file>
         <file>qml/ui/Spinner.qml</file>
         <file>qml/ui/animations/BlinkAnimation.qml</file>
@@ -177,15 +186,7 @@
         <file>qml/components/AdaptiveLayout.qml</file>
         <file>qml/components/AdaptiveLayoutElement.qml</file>
         <file>qml/components/AvatarListTile.qml</file>
-        <file>qml/components/FlatButton.qml</file>
-        <file>qml/dialogs/InviteDialog.qml</file>
-        <file>qml/dialogs/RawMessageDialog.qml</file>
-        <file>qml/dialogs/ReadReceipts.qml</file>
-        <file>qml/dialogs/RoomDirectory.qml</file>
-        <file>qml/dialogs/RoomMembers.qml</file>
-        <file>qml/dialogs/RoomSettings.qml</file>
-        <file>qml/dialogs/UserProfile.qml</file>
-        <file>qml/dialogs/LogoutDialog.qml</file>
+		<file>qml/components/FlatButton.qml</file>
     </qresource>
     <qresource prefix="/media">
         <file>media/ring.ogg</file>
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 0978fc258c166dc5f6521f351ff17e12fc49801b..777b5b225dc34f2ef3c0407f115569d83d763e0a 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -33,7 +33,6 @@
 #include "ui/SnackBar.h"
 
 #include "dialogs/CreateRoom.h"
-#include "dialogs/JoinRoom.h"
 #include "dialogs/LeaveRoom.h"
 
 MainWindow *MainWindow::instance_ = nullptr;
@@ -324,18 +323,6 @@ MainWindow::showOverlayProgressBar()
     showSolidOverlayModal(spinner_);
 }
 
-void
-MainWindow::openJoinRoomDialog(std::function<void(const QString &room_id)> callback)
-{
-    auto dialog = new dialogs::JoinRoom(this);
-    connect(dialog, &dialogs::JoinRoom::joinRoom, this, [callback](const QString &room) {
-        if (!room.isEmpty())
-            callback(room);
-    });
-
-    showDialog(dialog);
-}
-
 void
 MainWindow::openCreateRoomDialog(
   std::function<void(const mtx::requests::CreateRoom &request)> callback)
diff --git a/src/MainWindow.h b/src/MainWindow.h
index a3c3c767111ed4a1cc8bd742a7db34714bab5bc4..01575a199a6d7dfd62d9d8691492baa07cd805f9 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -37,7 +37,6 @@ struct CreateRoom;
 namespace dialogs {
 class CreateRoom;
 class InviteUsers;
-class JoinRoom;
 class LeaveRoom;
 class Logout;
 class MemberList;
@@ -64,6 +63,7 @@ public:
     void openCreateRoomDialog(
       std::function<void(const mtx::requests::CreateRoom &request)> callback);
     void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
+    void openLogoutDialog();
 
     void hideOverlay();
     void showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter);
diff --git a/src/dialogs/JoinRoom.cpp b/src/dialogs/JoinRoom.cpp
deleted file mode 100644
index 76baf857e62020b9dc9a1bbd790282af81328d9c..0000000000000000000000000000000000000000
--- a/src/dialogs/JoinRoom.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QLabel>
-#include <QPushButton>
-#include <QVBoxLayout>
-
-#include "dialogs/JoinRoom.h"
-
-#include "Config.h"
-#include "ui/TextField.h"
-
-using namespace dialogs;
-
-JoinRoom::JoinRoom(QWidget *parent)
-  : QFrame(parent)
-{
-    setAutoFillBackground(true);
-    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-    setWindowModality(Qt::WindowModal);
-    setAttribute(Qt::WA_DeleteOnClose, true);
-
-    setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
-    setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-    auto layout = new QVBoxLayout(this);
-    layout->setSpacing(conf::modals::WIDGET_SPACING);
-    layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-    auto buttonLayout = new QHBoxLayout();
-    buttonLayout->setSpacing(15);
-
-    confirmBtn_ = new QPushButton(tr("Join"), this);
-    confirmBtn_->setDefault(true);
-    cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
-    buttonLayout->addStretch(1);
-    buttonLayout->addWidget(cancelBtn_);
-    buttonLayout->addWidget(confirmBtn_);
-
-    roomInput_ = new TextField(this);
-    roomInput_->setLabel(tr("Room ID or alias"));
-
-    layout->addWidget(roomInput_);
-    layout->addLayout(buttonLayout);
-    layout->addStretch(1);
-
-    connect(roomInput_, &QLineEdit::returnPressed, this, &JoinRoom::handleInput);
-    connect(confirmBtn_, &QPushButton::clicked, this, &JoinRoom::handleInput);
-    connect(cancelBtn_, &QPushButton::clicked, this, &JoinRoom::close);
-}
-
-void
-JoinRoom::handleInput()
-{
-    if (roomInput_->text().isEmpty())
-        return;
-
-    // TODO: input validation with error messages.
-    emit joinRoom(roomInput_->text());
-    roomInput_->clear();
-
-    emit close();
-}
-
-void
-JoinRoom::showEvent(QShowEvent *event)
-{
-    roomInput_->setFocus();
-
-    QFrame::showEvent(event);
-}
diff --git a/src/dialogs/JoinRoom.h b/src/dialogs/JoinRoom.h
deleted file mode 100644
index 11c54d7c989f1b273e3f9428b8f96dc18292bb43..0000000000000000000000000000000000000000
--- a/src/dialogs/JoinRoom.h
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-
-class QPushButton;
-class TextField;
-
-namespace dialogs {
-
-class JoinRoom : public QFrame
-{
-    Q_OBJECT
-public:
-    JoinRoom(QWidget *parent = nullptr);
-
-signals:
-    void joinRoom(const QString &room);
-
-protected:
-    void showEvent(QShowEvent *event) override;
-
-private slots:
-    void handleInput();
-
-private:
-    QPushButton *confirmBtn_;
-    QPushButton *cancelBtn_;
-
-    TextField *roomInput_;
-};
-
-} // dialogs
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index d687a3083359f3f2b79244ac9937ddc66ef974d4..11fc568179202feedd59daec8b8eef169484d287 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -21,6 +21,7 @@ Nheko::Nheko()
     connect(
       UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged);
     connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile);
+    connect(this, &Nheko::joinRoom, ChatPage::instance(), &ChatPage::joinRoom);
 }
 
 void
@@ -96,13 +97,6 @@ Nheko::openCreateRoomDialog() const
       [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); });
 }
 
-void
-Nheko::openJoinRoomDialog() const
-{
-    MainWindow::instance()->openJoinRoomDialog(
-      [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
-}
-
 void
 Nheko::reparent(QWindow *win) const
 {
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 64aad941a4cdbeb157af9ebb130e3e2fa6f55f79..c70813c5d7184f6455db92150b7df7c92d90fbc6 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -50,7 +50,6 @@ public:
     Q_INVOKABLE void showUserSettingsPage() const;
     Q_INVOKABLE void logout() const;
     Q_INVOKABLE void openCreateRoomDialog() const;
-    Q_INVOKABLE void openJoinRoomDialog() const;
     Q_INVOKABLE void reparent(QWindow *win) const;
 
 public slots:
@@ -61,6 +60,8 @@ signals:
     void profileChanged();
 
     void openLogoutDialog();
+    void openJoinRoomDialog();
+    void joinRoom(QString roomId);
 
 private:
     QScopedPointer<UserProfile> currentUser_;