diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6b26b2e5d99ccf3d76753e4cdac57a58577bb9ae..f77d997822f7b54492ff68eb3728c5b7fd808b26 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -281,11 +281,9 @@ set(SRC_FILES
 	src/dialogs/CreateRoom.cpp
 	src/dialogs/FallbackAuth.cpp
 	src/dialogs/ImageOverlay.cpp
-	src/dialogs/InviteUsers.cpp
 	src/dialogs/JoinRoom.cpp
 	src/dialogs/LeaveRoom.cpp
 	src/dialogs/Logout.cpp
-	src/dialogs/MemberList.cpp
 	src/dialogs/PreviewUploadOverlay.cpp
 	src/dialogs/ReCaptcha.cpp
 	src/dialogs/ReadReceipts.cpp
@@ -346,11 +344,12 @@ set(SRC_FILES
 	src/CompletionProxyModel.cpp
 	src/DeviceVerificationFlow.cpp
 	src/EventAccessors.cpp
-	src/InviteeItem.cpp
+	src/InviteesModel.cpp
 	src/Logging.cpp
 	src/LoginPage.cpp
 	src/MainWindow.cpp
 	src/MatrixClient.cpp
+	src/MemberList.cpp
 	src/MxcImageProvider.cpp
 	src/Olm.cpp
 	src/RegisterPage.cpp
@@ -492,11 +491,9 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/dialogs/CreateRoom.h
 	src/dialogs/FallbackAuth.h
 	src/dialogs/ImageOverlay.h
-	src/dialogs/InviteUsers.h
 	src/dialogs/JoinRoom.h
 	src/dialogs/LeaveRoom.h
 	src/dialogs/Logout.h
-	src/dialogs/MemberList.h
 	src/dialogs/PreviewUploadOverlay.h
 	src/dialogs/RawMessage.h
 	src/dialogs/ReCaptcha.h
@@ -554,9 +551,10 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/Clipboard.h
 	src/CompletionProxyModel.h
 	src/DeviceVerificationFlow.h
-	src/InviteeItem.h
+	src/InviteesModel.h
 	src/LoginPage.h
 	src/MainWindow.h
+	src/MemberList.h
 	src/MxcImageProvider.h
 	src/RegisterPage.h
 	src/SSOHandler.h
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 333fb11d2afc7600eb3719cf45e1b33046adb291..00fc3216131e088bc227f8917112126eb059e649 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -70,7 +70,7 @@ Popup {
     onCompleterNameChanged: {
         if (completerName) {
             if (completerName == "user")
-                completer = TimelineManager.completerFor(completerName, room.roomId());
+                completer = TimelineManager.completerFor(completerName, room.roomId);
             else
                 completer = TimelineManager.completerFor(completerName);
             completer.setSearchString("");
diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml
new file mode 100644
index 0000000000000000000000000000000000000000..dbe8bb0707d90e665973436c14e85a0a96ec520b
--- /dev/null
+++ b/resources/qml/InviteDialog.qml
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: inviteDialogRoot
+
+    property string roomId
+    property string plainRoomName
+    property InviteesModel invitees
+
+    function addInvite() {
+        if (inviteeEntry.isValidMxid) {
+            invitees.addUser(inviteeEntry.text);
+            inviteeEntry.clear();
+        }
+    }
+
+    function cleanUpAndClose() {
+        if (inviteeEntry.isValidMxid)
+            addInvite();
+
+        invitees.accept();
+        close();
+    }
+
+    title: qsTr("Invite users to ") + plainRoomName
+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    height: 380
+    width: 340
+    palette: Nheko.colors
+    color: Nheko.colors.window
+
+    Shortcut {
+        sequence: "Ctrl+Enter"
+        onActivated: cleanUpAndClose()
+    }
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: inviteDialogRoot.close()
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+        spacing: Nheko.paddingMedium
+
+        Label {
+            text: qsTr("User ID to invite")
+            Layout.fillWidth: true
+        }
+
+        RowLayout {
+            spacing: Nheko.paddingMedium
+
+            MatrixTextField {
+                id: inviteeEntry
+
+                property bool isValidMxid: text.match("@.+?:.{3,}")
+
+                backgroundColor: Nheko.colors.window
+                placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.")
+                Layout.fillWidth: true
+                onAccepted: {
+                    if (isValidMxid)
+                        addInvite();
+
+                }
+                Component.onCompleted: forceActiveFocus()
+                Keys.onShortcutOverride: event.accepted = ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers & Qt.ControlModifier))
+                Keys.onPressed: {
+                    if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers === Qt.ControlModifier))
+                        cleanUpAndClose();
+
+                }
+            }
+
+            Button {
+                text: qsTr("Add")
+                enabled: inviteeEntry.isValidMxid
+                onClicked: addInvite()
+            }
+
+        }
+
+        ListView {
+            id: inviteesList
+
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            model: invitees
+
+            delegate: RowLayout {
+                spacing: Nheko.paddingMedium
+
+                Avatar {
+                    width: Nheko.avatarSize
+                    height: Nheko.avatarSize
+                    userid: model.mxid
+                    url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    displayName: model.displayName
+                    onClicked: Rooms.currentRoom.openUserProfile(model.mxid)
+                }
+
+                ColumnLayout {
+                    spacing: Nheko.paddingSmall
+
+                    Label {
+                        text: model.displayName
+                        color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
+                        font.pointSize: fontMetrics.font.pointSize
+                    }
+
+                    Label {
+                        text: model.mxid
+                        color: Nheko.colors.buttonText
+                        font.pointSize: fontMetrics.font.pointSize * 0.9
+                    }
+
+                    Item {
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: buttons
+
+        Button {
+            text: qsTr("Invite")
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+            enabled: invitees.count > 0
+            onClicked: cleanUpAndClose()
+        }
+
+        Button {
+            text: qsTr("Cancel")
+            DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
+            onClicked: inviteDialogRoot.close()
+        }
+
+    }
+
+}
diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index 3c660bac1d6c4c4cdee94370e38442b88afbfead..80732b277ad4bb43de544f2b81a137a192f48835 100644
--- a/resources/qml/MatrixTextField.qml
+++ b/resources/qml/MatrixTextField.qml
@@ -10,6 +10,8 @@ import im.nheko 1.0
 TextField {
     id: input
 
+    property alias backgroundColor: backgroundRect.color
+
     palette: Nheko.colors
     color: Nheko.colors.text
 
@@ -62,6 +64,8 @@ TextField {
     }
 
     background: Rectangle {
+        id: backgroundRect
+
         color: Nheko.colors.base
     }
 
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 415d67a7baf284144c0fca1bd819c8686195906f..c135aff9bb5d3b69eedaf511959bf08c7c24f993 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -331,7 +331,7 @@ Rectangle {
             image: ":/icons/icons/ui/sticky-note-solid.svg"
             ToolTip.visible: hovered
             ToolTip.text: qsTr("Stickers")
-            onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId(), function(row) {
+            onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) {
                 room.input.sticker(stickerPopup.model.sourceModel, row);
                 TimelineManager.focusMessageInput();
             })
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index a1ce8d7e9dbdf26e9dc39cca5c10f8342a637d08..9dac583037e9d4bd9909885c307f72fe18086edf 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -33,8 +33,8 @@ Page {
 
         Connections {
             onActiveTimelineChanged: {
-                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain);
-                console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId()));
+                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
+                console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId));
             }
             target: TimelineManager
         }
@@ -133,7 +133,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId)
+                    when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
 
                     PropertyChanges {
                         target: roomItem
@@ -147,7 +147,7 @@ Page {
                 },
                 State {
                     name: "selected"
-                    when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId
+                    when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId
 
                     PropertyChanges {
                         target: roomItem
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml
new file mode 100644
index 0000000000000000000000000000000000000000..3758cb0bbce6b171833e960148158b09e0b4e3e1
--- /dev/null
+++ b/resources/qml/RoomMembers.qml
@@ -0,0 +1,149 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./ui"
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: roomMembersRoot
+
+    property MemberList members
+
+    title: qsTr("Members of ") + members.roomName
+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    height: 650
+    width: 420
+    minimumHeight: 420
+    palette: Nheko.colors
+    color: Nheko.colors.window
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: roomMembersRoot.close()
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+        spacing: Nheko.paddingMedium
+
+        Avatar {
+            id: roomAvatar
+
+            width: 130
+            height: width
+            displayName: members.roomName
+            Layout.alignment: Qt.AlignHCenter
+            url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
+            onClicked: Rooms.currentRoom.openRoomSettings(members.roomId)
+        }
+
+        ElidedLabel {
+            font.pixelSize: fontMetrics.font.pixelSize * 2
+            fullText: members.memberCount + (members.memberCount === 1 ? qsTr(" person in ") : qsTr(" people in ")) + members.roomName
+            Layout.alignment: Qt.AlignHCenter
+            elideWidth: parent.width - Nheko.paddingMedium
+        }
+
+        ImageButton {
+            Layout.alignment: Qt.AlignHCenter
+            image: ":/icons/icons/ui/add-square-button.png"
+            hoverEnabled: true
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Invite more people")
+            onClicked: Rooms.currentRoom.openInviteUsers()
+        }
+
+        ScrollView {
+            palette: Nheko.colors
+            padding: Nheko.paddingMedium
+            ScrollBar.horizontal.visible: false
+            Layout.fillHeight: true
+            Layout.minimumHeight: 200
+            Layout.fillWidth: true
+
+            ListView {
+                id: memberList
+
+                clip: true
+                spacing: Nheko.paddingMedium
+                boundsBehavior: Flickable.StopAtBounds
+                model: members
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+
+                delegate: RowLayout {
+                    spacing: Nheko.paddingMedium
+
+                    Avatar {
+                        width: Nheko.avatarSize
+                        height: Nheko.avatarSize
+                        userid: model.mxid
+                        url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                        displayName: model.displayName
+                        onClicked: Rooms.currentRoom.openUserProfile(model.mxid)
+                    }
+
+                    ColumnLayout {
+                        spacing: Nheko.paddingSmall
+
+                        Label {
+                            text: model.displayName
+                            color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
+                            font.pointSize: fontMetrics.font.pointSize
+                        }
+
+                        Label {
+                            text: model.mxid
+                            color: Nheko.colors.buttonText
+                            font.pointSize: fontMetrics.font.pointSize * 0.9
+                        }
+
+                        Item {
+                            Layout.fillHeight: true
+                            Layout.fillWidth: true
+                        }
+
+                    }
+
+                }
+
+                footer: Item {
+                    width: parent.width
+                    visible: (members.numUsersLoaded < members.memberCount) && members.loadingMoreMembers
+
+                    // use the default height if it's visible, otherwise no height at all
+                    height: membersLoadingSpinner.height
+                    anchors.margins: Nheko.paddingMedium
+
+                    Spinner {
+                        id: membersLoadingSpinner
+
+                        anchors.centerIn: parent
+                        height: visible ? 35 : 0
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok
+        onAccepted: roomMembersRoot.close()
+    }
+
+}
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index c852b8377466990280c17f0d02a7468a92150647..2701edf9f8b61a026dd045cb9402d83956586d28 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -98,7 +98,7 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomName
-                font.pixelSize: 24
+                font.pixelSize: fontMetrics.font.pixelSize * 2
                 Layout.alignment: Qt.AlignHCenter
             }
 
@@ -264,7 +264,7 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomId
-                font.pixelSize: 14
+                font.pixelSize: fontMetrics.font.pixelSize * 1.2
                 Layout.alignment: Qt.AlignRight
             }
 
@@ -274,16 +274,16 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomVersion
-                font.pixelSize: 14
+                font.pixelSize: fontMetrics.font.pixelSize * 1.2
                 Layout.alignment: Qt.AlignRight
             }
 
         }
 
-        Button {
-            Layout.alignment: Qt.AlignRight
-            text: qsTr("OK")
-            onClicked: close()
+        DialogButtonBox {
+            Layout.fillWidth: true
+            standardButtons: DialogButtonBox.Ok
+            onAccepted: close()
         }
 
     }
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 5316e20dea1b46be2b66284dd2b277a5e9582a2c..f71c18e2093f8f1a1dd1b233a14cadde9a248c00 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -47,6 +47,14 @@ Page {
 
     }
 
+    Component {
+        id: roomMembersComponent
+
+        RoomMembers {
+        }
+
+    }
+
     Component {
         id: mobileCallInviteDialog
 
@@ -63,6 +71,22 @@ Page {
 
     }
 
+    Component {
+        id: deviceVerificationDialog
+
+        DeviceVerification {
+        }
+
+    }
+
+    Component {
+        id: inviteDialog
+
+        InviteDialog {
+        }
+
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -82,14 +106,6 @@ Page {
         onActivated: Rooms.previousRoom()
     }
 
-    Component {
-        id: deviceVerificationDialog
-
-        DeviceVerification {
-        }
-
-    }
-
     Connections {
         target: TimelineManager
         onNewDeviceVerificationRequest: {
@@ -116,6 +132,31 @@ Page {
         }
     }
 
+    Connections {
+        target: Rooms.currentRoom
+        onOpenRoomMembersDialog: {
+            var membersDialog = roomMembersComponent.createObject(timelineRoot, {
+                "members": members,
+                "roomName": Rooms.currentRoom.roomName
+            });
+            membersDialog.show();
+        }
+        onOpenRoomSettingsDialog: {
+            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
+                "roomSettings": settings
+            });
+            roomSettings.show();
+        }
+        onOpenInviteUsersDialog: {
+            var dialog = inviteDialog.createObject(timelineRoot, {
+                "roomId": Rooms.currentRoom.roomId,
+                "plainRoomName": Rooms.currentRoom.plainRoomName,
+                "invitees": invitees
+            });
+            dialog.show();
+        }
+    }
+
     ChatPage {
         anchors.fill: parent
     }
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 148a5817766800a8fce2b59c73dac6cc80be3b60..c5cc69a6636be12d971b5c828e6cf4a4a7dc3901 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -246,17 +246,7 @@ Item {
 
     NhekoDropArea {
         anchors.fill: parent
-        roomid: room ? room.roomId() : ""
-    }
-
-    Connections {
-        target: room
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
+        roomid: room ? room.roomId : ""
     }
 
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 58aba0c721c0cada8d03b199b351dee899e6b0aa..48491f8477dc3ddc39dfe276561b6e4498287ce6 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -111,17 +111,17 @@ Rectangle {
                 Platform.MenuItem {
                     visible: room ? room.permissions.canInvite() : false
                     text: qsTr("Invite users")
-                    onTriggered: TimelineManager.openInviteUsersDialog()
+                    onTriggered: Rooms.currentRoom.openInviteUsers()
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Members")
-                    onTriggered: TimelineManager.openMemberListDialog(room.roomId())
+                    onTriggered: Rooms.currentRoom.openRoomMembers()
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
+                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId)
                 }
 
                 Platform.MenuItem {
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index c64ae887cd2af36b7b40bea6110ca92085bb84ba..a98c2a8bca05ea3d8205207f4b570a246765eaa2 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -232,7 +232,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId())
+                formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId)
             }
 
         }
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 5f56485346b0b4a3723af29d90073511b9b0aff0..97932cc948e9f1331e08be54ecdebe41c6a842d1 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -88,7 +88,7 @@ Popup {
                 onClicked: {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
-                        CallManager.sendInvite(room.roomId(), CallType.VOICE);
+                        CallManager.sendInvite(room.roomId, CallType.VOICE);
                         close();
                     }
                 }
@@ -102,7 +102,7 @@ Popup {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
                         Settings.camera = cameraCombo.currentText;
-                        CallManager.sendInvite(room.roomId(), CallType.VIDEO);
+                        CallManager.sendInvite(room.roomId, CallType.VIDEO);
                         close();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index a10057b206fa0c3c485450b763189c7262ead26a..8cd43b1c40edd88f06b1bcd012ba56ed9d69605f 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -136,7 +136,7 @@ Popup {
                         Settings.screenSharePiP = pipCheckBox.checked;
                         Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
                         Settings.screenShareHideCursor = hideCursorCheckBox.checked;
-                        CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
+                        CallManager.sendInvite(room.roomId, CallType.SCREEN, windowCombo.currentIndex);
                         close();
                     }
                 }
diff --git a/resources/res.qrc b/resources/res.qrc
index e9479e5784cf6af23380da4c15de92676355ef33..f8c040e40013c1bb580c19ec13e4c6a90036bbcc 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -9,7 +9,6 @@
         <file>icons/ui/do-not-disturb-rounded-sign@2x.png</file>
         <file>icons/ui/round-remove-button.png</file>
         <file>icons/ui/round-remove-button@2x.png</file>
-
         <file>icons/ui/double-tick-indicator.png</file>
         <file>icons/ui/double-tick-indicator@2x.png</file>
         <file>icons/ui/lock.png</file>
@@ -55,22 +54,17 @@
         <file>icons/ui/pause-symbol@2x.png</file>
         <file>icons/ui/remove-symbol.png</file>
         <file>icons/ui/remove-symbol@2x.png</file>
-
         <file>icons/ui/world.png</file>
         <file>icons/ui/world@2x.png</file>
-
         <file>icons/ui/tag.png</file>
         <file>icons/ui/tag@2x.png</file>
         <file>icons/ui/star.png</file>
         <file>icons/ui/star@2x.png</file>
         <file>icons/ui/lowprio.png</file>
         <file>icons/ui/lowprio@2x.png</file>
-
         <file>icons/ui/edit.png</file>
         <file>icons/ui/edit@2x.png</file>
-    
         <file>icons/ui/mail-reply.png</file>
-
         <file>icons/ui/place-call.png</file>
         <file>icons/ui/end-call.png</file>
         <file>icons/ui/microphone-mute.png</file>
@@ -78,7 +72,6 @@
         <file>icons/ui/screen-share.png</file>
         <file>icons/ui/toggle-camera-view.png</file>
         <file>icons/ui/video-call.png</file>
-
         <file>icons/emoji-categories/people.png</file>
         <file>icons/emoji-categories/people@2x.png</file>
         <file>icons/emoji-categories/nature.png</file>
@@ -99,16 +92,12 @@
     <qresource prefix="/logos">
         <file>nheko.png</file>
         <file>nheko.svg</file>
-
         <file>splash.png</file>
         <file>splash@2x.png</file>
-
         <file>register.png</file>
         <file>register@2x.png</file>
-
         <file>login.png</file>
         <file>login@2x.png</file>
-
         <file>nheko-512.png</file>
         <file>nheko-256.png</file>
         <file>nheko-128.png</file>
@@ -185,6 +174,8 @@
         <file>qml/components/AdaptiveLayout.qml</file>
         <file>qml/components/AdaptiveLayoutElement.qml</file>
         <file>qml/components/FlatButton.qml</file>
+        <file>qml/RoomMembers.qml</file>
+        <file>qml/InviteDialog.qml</file>
     </qresource>
     <qresource prefix="/media">
         <file>media/ring.ogg</file>
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 10a91557e835447c7ddf694fe451f8f572e6408b..6b8c1e10d40d3ae317c4ba131b502553fc52aa99 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -116,29 +116,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 
         connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
 
-        connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
-                const auto room_id = currentRoom().toStdString();
-
-                for (int ii = 0; ii < users.size(); ++ii) {
-                        QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
-                                const auto user = users.at(ii);
-
-                                http::client()->invite_user(
-                                  room_id,
-                                  user.toStdString(),
-                                  [this, user](const mtx::responses::RoomInvite &,
-                                               mtx::http::RequestErr err) {
-                                          if (err) {
-                                                  emit showNotification(
-                                                    tr("Failed to invite user: %1").arg(user));
-                                                  return;
-                                          }
-
-                                          emit showNotification(tr("Invited user: %1").arg(user));
-                                  });
-                        });
-                }
-        });
+        connect(
+          view_manager_,
+          &TimelineViewManager::inviteUsers,
+          this,
+          [this](QString roomId, QStringList users) {
+                  for (int ii = 0; ii < users.size(); ++ii) {
+                          QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
+                                  const auto user = users.at(ii);
+
+                                  http::client()->invite_user(
+                                    roomId.toStdString(),
+                                    user.toStdString(),
+                                    [this, user](const mtx::responses::RoomInvite &,
+                                                 mtx::http::RequestErr err) {
+                                            if (err) {
+                                                    emit showNotification(
+                                                      tr("Failed to invite user: %1").arg(user));
+                                                    return;
+                                            }
+
+                                            emit showNotification(tr("Invited user: %1").arg(user));
+                                    });
+                          });
+                  }
+          });
 
         connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
         connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
diff --git a/src/InviteeItem.cpp b/src/InviteeItem.cpp
deleted file mode 100644
index 27f02560035c6663e7c6374f3516977513b9690d..0000000000000000000000000000000000000000
--- a/src/InviteeItem.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QPushButton>
-
-#include "InviteeItem.h"
-
-constexpr int SidePadding = 10;
-
-InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent)
-  : QWidget{parent}
-  , user_{QString::fromStdString(user.to_string())}
-{
-        auto topLayout_ = new QHBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0);
-
-        name_          = new QLabel(user_, this);
-        removeUserBtn_ = new QPushButton(tr("Remove"), this);
-
-        topLayout_->addWidget(name_);
-        topLayout_->addWidget(removeUserBtn_, 0, Qt::AlignRight);
-
-        connect(removeUserBtn_, &QPushButton::clicked, this, &InviteeItem::removeItem);
-}
diff --git a/src/InviteeItem.h b/src/InviteeItem.h
deleted file mode 100644
index 014541ea751ae570d6b5e81d3ddea6818e7a9d7d..0000000000000000000000000000000000000000
--- a/src/InviteeItem.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QWidget>
-
-#include <mtx/identifiers.hpp>
-
-class QPushButton;
-class QLabel;
-
-class InviteeItem : public QWidget
-{
-        Q_OBJECT
-
-public:
-        InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr);
-
-        QString userID() { return user_; }
-
-signals:
-        void removeItem();
-
-private:
-        QString user_;
-
-        QLabel *name_;
-        QPushButton *removeUserBtn_;
-};
diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..27b2116f020aed1d46e46ee3a29dc1a32384d92f
--- /dev/null
+++ b/src/InviteesModel.cpp
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "InviteesModel.h"
+
+#include "Cache.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "mtx/responses/profile.hpp"
+
+InviteesModel::InviteesModel(QObject *parent)
+  : QAbstractListModel{parent}
+{}
+
+void
+InviteesModel::addUser(QString mxid)
+{
+        beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count());
+
+        auto invitee        = new Invitee{mxid, this};
+        auto indexOfInvitee = invitees_.count();
+        connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() {
+                emit dataChanged(index(indexOfInvitee), index(indexOfInvitee));
+        });
+
+        invitees_.push_back(invitee);
+
+        endInsertRows();
+        emit countChanged();
+}
+
+QHash<int, QByteArray>
+InviteesModel::roleNames() const
+{
+        return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}};
+}
+
+QVariant
+InviteesModel::data(const QModelIndex &index, int role) const
+{
+        if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0)
+                return {};
+
+        switch (role) {
+        case Mxid:
+                return invitees_[index.row()]->mxid_;
+        case DisplayName:
+                return invitees_[index.row()]->displayName_;
+        case AvatarUrl:
+                return invitees_[index.row()]->avatarUrl_;
+        default:
+                return {};
+        }
+}
+
+QStringList
+InviteesModel::mxids()
+{
+        QStringList mxidList;
+        for (int i = 0; i < invitees_.length(); ++i)
+                mxidList.push_back(invitees_[i]->mxid_);
+        return mxidList;
+}
+
+Invitee::Invitee(const QString &mxid, QObject *parent)
+  : QObject{parent}
+  , mxid_{mxid}
+{
+        http::client()->get_profile(
+          mxid_.toStdString(),
+          [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to retrieve profile info");
+                          emit userInfoLoaded();
+                          return;
+                  }
+
+                  displayName_ = QString::fromStdString(res.display_name);
+                  avatarUrl_   = QString::fromStdString(res.avatar_url);
+
+                  emit userInfoLoaded();
+          });
+}
diff --git a/src/InviteesModel.h b/src/InviteesModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..a4e19ebbf89a517bf3dd394b312a1e777e433483
--- /dev/null
+++ b/src/InviteesModel.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef INVITEESMODEL_H
+#define INVITEESMODEL_H
+
+#include <QAbstractListModel>
+#include <QVector>
+
+class Invitee : public QObject
+{
+        Q_OBJECT
+
+public:
+        Invitee(const QString &mxid, QObject *parent = nullptr);
+
+signals:
+        void userInfoLoaded();
+
+private:
+        const QString mxid_;
+        QString displayName_;
+        QString avatarUrl_;
+
+        friend class InviteesModel;
+};
+
+class InviteesModel : public QAbstractListModel
+{
+        Q_OBJECT
+
+        Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+
+public:
+        enum Roles
+        {
+                Mxid,
+                DisplayName,
+                AvatarUrl,
+        };
+
+        InviteesModel(QObject *parent = nullptr);
+
+        Q_INVOKABLE void addUser(QString mxid);
+
+        QHash<int, QByteArray> roleNames() const override;
+        int rowCount(const QModelIndex & = QModelIndex()) const override
+        {
+                return (int)invitees_.size();
+        }
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+        QStringList mxids();
+
+signals:
+        void accept();
+        void countChanged();
+
+private:
+        QVector<Invitee *> invitees_;
+};
+
+#endif // INVITEESMODEL_H
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ed337ca4a4fb5b3da56f095f0e0871520b8b2d21..c0486d01d821b7094228aff6664d9f4b892a2122 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -21,6 +21,7 @@
 #include "LoginPage.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
+#include "MemberList.h"
 #include "RegisterPage.h"
 #include "TrayIcon.h"
 #include "UserSettingsPage.h"
@@ -32,11 +33,9 @@
 #include "ui/SnackBar.h"
 
 #include "dialogs/CreateRoom.h"
-#include "dialogs/InviteUsers.h"
 #include "dialogs/JoinRoom.h"
 #include "dialogs/LeaveRoom.h"
 #include "dialogs/Logout.h"
-#include "dialogs/MemberList.h"
 #include "dialogs/ReadReceipts.h"
 
 MainWindow *MainWindow::instance_ = nullptr;
@@ -310,14 +309,6 @@ MainWindow::hasActiveUser()
                settings.contains(prefix + "auth/user_id");
 }
 
-void
-MainWindow::openMemberListDialog(const QString &room_id)
-{
-        auto dialog = new dialogs::MemberList(room_id, this);
-
-        showDialog(dialog);
-}
-
 void
 MainWindow::openLeaveRoomDialog(const QString &room_id)
 {
@@ -341,18 +332,6 @@ MainWindow::showOverlayProgressBar()
         showSolidOverlayModal(spinner_);
 }
 
-void
-MainWindow::openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback)
-{
-        auto dialog = new dialogs::InviteUsers(this);
-        connect(dialog, &dialogs::InviteUsers::sendInvites, this, [callback](QStringList invitees) {
-                if (!invitees.isEmpty())
-                        callback(invitees);
-        });
-
-        showDialog(dialog);
-}
-
 void
 MainWindow::openJoinRoomDialog(std::function<void(const QString &room_id)> callback)
 {
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 3571f07955a8b994b9058e3e4e64229c097ce3c1..6d62545c4462a77509abb6cc244b4d9d5110bbcd 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -65,7 +65,6 @@ public:
           std::function<void(const mtx::requests::CreateRoom &request)> callback);
         void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
         void openLogoutDialog();
-        void openMemberListDialog(const QString &room_id);
         void openReadReceiptsDialog(const QString &event_id);
 
         void hideOverlay();
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..415e3b57bdb91acb6135f7d4feec4fb48ecd05c8
--- /dev/null
+++ b/src/MemberList.cpp
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <QAbstractSlider>
+#include <QLabel>
+#include <QListWidgetItem>
+#include <QPainter>
+#include <QPushButton>
+#include <QScrollBar>
+#include <QShortcut>
+#include <QStyleOption>
+#include <QVBoxLayout>
+
+#include "MemberList.h"
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Config.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "timeline/TimelineViewManager.h"
+#include "ui/Avatar.h"
+
+MemberList::MemberList(const QString &room_id, QWidget *parent)
+  : QAbstractListModel{parent}
+  , room_id_{room_id}
+{
+        try {
+                info_ = cache::singleRoomInfo(room_id_.toStdString());
+        } catch (const lmdb::error &) {
+                nhlog::db()->warn("failed to retrieve room info from cache: {}",
+                                  room_id_.toStdString());
+        }
+
+        try {
+                auto members = cache::getMembers(room_id_.toStdString());
+                addUsers(members);
+                numUsersLoaded_ = members.size();
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
+        }
+}
+
+void
+MemberList::addUsers(const std::vector<RoomMember> &members)
+{
+        beginInsertRows(
+          QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1);
+
+        for (const auto &member : members)
+                m_memberList.push_back(
+                  {member,
+                   ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl(
+                     member.user_id)});
+
+        endInsertRows();
+}
+
+QHash<int, QByteArray>
+MemberList::roleNames() const
+{
+        return {
+          {Mxid, "mxid"},
+          {DisplayName, "displayName"},
+          {AvatarUrl, "avatarUrl"},
+        };
+}
+
+QVariant
+MemberList::data(const QModelIndex &index, int role) const
+{
+        if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0)
+                return {};
+
+        switch (role) {
+        case Mxid:
+                return m_memberList[index.row()].first.user_id;
+        case DisplayName:
+                return m_memberList[index.row()].first.display_name;
+        case AvatarUrl:
+                return m_memberList[index.row()].second;
+        default:
+                return {};
+        }
+}
+
+bool
+MemberList::canFetchMore(const QModelIndex &) const
+{
+        const size_t numMembers = rowCount();
+        if (numMembers > 1 && numMembers < info_.member_count)
+                return true;
+        else
+                return false;
+}
+
+void
+MemberList::fetchMore(const QModelIndex &)
+{
+        loadingMoreMembers_ = true;
+        emit loadingMoreMembersChanged();
+
+        auto members = cache::getMembers(room_id_.toStdString(), rowCount());
+        addUsers(members);
+        numUsersLoaded_ += members.size();
+        emit numUsersLoadedChanged();
+
+        loadingMoreMembers_ = false;
+        emit loadingMoreMembersChanged();
+}
diff --git a/src/MemberList.h b/src/MemberList.h
new file mode 100644
index 0000000000000000000000000000000000000000..070666a29ff08c59b056fb4254011ba89b588ab4
--- /dev/null
+++ b/src/MemberList.h
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "CacheStructs.h"
+#include <QAbstractListModel>
+
+class MemberList : public QAbstractListModel
+{
+        Q_OBJECT
+
+        Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+        Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged)
+        Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+        Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged)
+        Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged)
+        Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged)
+
+public:
+        enum Roles
+        {
+                Mxid,
+                DisplayName,
+                AvatarUrl,
+        };
+        MemberList(const QString &room_id, QWidget *parent = nullptr);
+
+        QHash<int, QByteArray> roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override
+        {
+                Q_UNUSED(parent)
+                return static_cast<int>(m_memberList.size());
+        }
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+        QString roomName() const { return QString::fromStdString(info_.name); }
+        int memberCount() const { return info_.member_count; }
+        QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); }
+        QString roomId() const { return room_id_; }
+        int numUsersLoaded() const { return numUsersLoaded_; }
+        bool loadingMoreMembers() const { return loadingMoreMembers_; }
+
+signals:
+        void roomNameChanged();
+        void memberCountChanged();
+        void avatarUrlChanged();
+        void roomIdChanged();
+        void numUsersLoadedChanged();
+        void loadingMoreMembersChanged();
+
+public slots:
+        void addUsers(const std::vector<RoomMember> &users);
+
+protected:
+        bool canFetchMore(const QModelIndex &) const override;
+        void fetchMore(const QModelIndex &) override;
+
+private:
+        QVector<QPair<RoomMember, QString>> m_memberList;
+        QString room_id_;
+        RoomInfo info_;
+        int numUsersLoaded_{0};
+        bool loadingMoreMembers_{false};
+};
diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp
deleted file mode 100644
index 9dd6085f7b4b6a247e81162324b3e086947cbcae..0000000000000000000000000000000000000000
--- a/src/dialogs/InviteUsers.cpp
+++ /dev/null
@@ -1,158 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QDebug>
-#include <QIcon>
-#include <QLabel>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QPushButton>
-#include <QStyleOption>
-#include <QTimer>
-#include <QVBoxLayout>
-
-#include "dialogs/InviteUsers.h"
-
-#include "Config.h"
-#include "InviteeItem.h"
-#include "ui/TextField.h"
-
-#include <mtx/identifiers.hpp>
-
-using namespace dialogs;
-
-InviteUsers::InviteUsers(QWidget *parent)
-  : QFrame(parent)
-{
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        setMinimumWidth(conf::window::minModalWidth);
-        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(0);
-        buttonLayout->setMargin(0);
-
-        confirmBtn_ = new QPushButton("Invite", this);
-        confirmBtn_->setDefault(true);
-        cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
-        buttonLayout->addStretch(1);
-        buttonLayout->setSpacing(15);
-        buttonLayout->addWidget(cancelBtn_);
-        buttonLayout->addWidget(confirmBtn_);
-
-        inviteeInput_ = new TextField(this);
-        inviteeInput_->setLabel(tr("User ID to invite"));
-
-        inviteeList_ = new QListWidget;
-        inviteeList_->setFrameStyle(QFrame::NoFrame);
-        inviteeList_->setSelectionMode(QAbstractItemView::NoSelection);
-        inviteeList_->setAttribute(Qt::WA_MacShowFocusRect, 0);
-        inviteeList_->setSpacing(5);
-
-        errorLabel_ = new QLabel(this);
-        errorLabel_->setAlignment(Qt::AlignCenter);
-
-        layout->addWidget(inviteeInput_);
-        layout->addWidget(errorLabel_);
-        layout->addWidget(inviteeList_);
-        layout->addLayout(buttonLayout);
-
-        connect(inviteeInput_, &TextField::returnPressed, this, &InviteUsers::addUser);
-        connect(confirmBtn_, &QPushButton::clicked, [this]() {
-                if (!inviteeInput_->text().trimmed().isEmpty()) {
-                        addUser();
-                }
-
-                emit sendInvites(invitedUsers());
-
-                inviteeInput_->clear();
-                inviteeList_->clear();
-                errorLabel_->hide();
-
-                emit close();
-        });
-
-        connect(cancelBtn_, &QPushButton::clicked, [this]() {
-                inviteeInput_->clear();
-                inviteeList_->clear();
-                errorLabel_->hide();
-
-                emit close();
-        });
-}
-
-void
-InviteUsers::addUser()
-{
-        auto user_id = inviteeInput_->text();
-
-        try {
-                namespace ids = mtx::identifiers;
-                auto user     = ids::parse<ids::User>(user_id.toStdString());
-
-                auto item    = new QListWidgetItem(inviteeList_);
-                auto invitee = new InviteeItem(user, this);
-
-                item->setSizeHint(invitee->minimumSizeHint());
-                item->setFlags(Qt::NoItemFlags);
-                item->setTextAlignment(Qt::AlignCenter);
-
-                inviteeList_->setItemWidget(item, invitee);
-
-                connect(invitee, &InviteeItem::removeItem, this, [this, item]() {
-                        emit removeInvitee(item);
-                });
-
-                errorLabel_->hide();
-                inviteeInput_->clear();
-        } catch (std::exception &e) {
-                errorLabel_->setText(e.what());
-                errorLabel_->show();
-        }
-}
-
-void
-InviteUsers::removeInvitee(QListWidgetItem *item)
-{
-        int row     = inviteeList_->row(item);
-        auto widget = inviteeList_->takeItem(row);
-
-        inviteeList_->removeItemWidget(widget);
-}
-
-QStringList
-InviteUsers::invitedUsers() const
-{
-        QStringList users;
-
-        for (int ii = 0; ii < inviteeList_->count(); ++ii) {
-                auto item    = inviteeList_->item(ii);
-                auto widget  = inviteeList_->itemWidget(item);
-                auto invitee = qobject_cast<InviteeItem *>(widget);
-
-                if (invitee)
-                        users << invitee->userID();
-                else
-                        qDebug() << "Cast InviteeItem failed";
-        }
-
-        return users;
-}
-
-void
-InviteUsers::showEvent(QShowEvent *event)
-{
-        inviteeInput_->setFocus();
-
-        QFrame::showEvent(event);
-}
diff --git a/src/dialogs/InviteUsers.h b/src/dialogs/InviteUsers.h
deleted file mode 100644
index e40183c1442d2df361720d68944e24de5201a788..0000000000000000000000000000000000000000
--- a/src/dialogs/InviteUsers.h
+++ /dev/null
@@ -1,45 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-#include <QStringList>
-
-class QPushButton;
-class QLabel;
-class TextField;
-class QListWidget;
-class QListWidgetItem;
-
-namespace dialogs {
-
-class InviteUsers : public QFrame
-{
-        Q_OBJECT
-public:
-        explicit InviteUsers(QWidget *parent = nullptr);
-
-protected:
-        void showEvent(QShowEvent *event) override;
-
-signals:
-        void sendInvites(QStringList invitees);
-
-private slots:
-        void removeInvitee(QListWidgetItem *item);
-
-private:
-        void addUser();
-        QStringList invitedUsers() const;
-
-        QPushButton *confirmBtn_;
-        QPushButton *cancelBtn_;
-
-        TextField *inviteeInput_;
-        QLabel *errorLabel_;
-
-        QListWidget *inviteeList_;
-};
-} // dialogs
diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
deleted file mode 100644
index 21eb72b014e6a158baf28039c3ff89829e3a589c..0000000000000000000000000000000000000000
--- a/src/dialogs/MemberList.cpp
+++ /dev/null
@@ -1,146 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QAbstractSlider>
-#include <QLabel>
-#include <QListWidgetItem>
-#include <QPainter>
-#include <QPushButton>
-#include <QScrollBar>
-#include <QShortcut>
-#include <QStyleOption>
-#include <QVBoxLayout>
-
-#include "dialogs/MemberList.h"
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-using namespace dialogs;
-
-MemberItem::MemberItem(const RoomMember &member, QWidget *parent)
-  : QWidget(parent)
-{
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setMargin(0);
-
-        textLayout_ = new QVBoxLayout;
-        textLayout_->setMargin(0);
-        textLayout_->setSpacing(0);
-
-        avatar_ = new Avatar(this, 44);
-        avatar_->setLetter(utils::firstChar(member.display_name));
-
-        avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id);
-
-        QFont nameFont;
-        nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-
-        userId_   = new QLabel(member.user_id, this);
-        userName_ = new QLabel(member.display_name, this);
-        userName_->setFont(nameFont);
-
-        textLayout_->addWidget(userName_);
-        textLayout_->addWidget(userId_);
-
-        topLayout_->addWidget(avatar_);
-        topLayout_->addLayout(textLayout_, 1);
-}
-
-void
-MemberItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-MemberList::MemberList(const QString &room_id, QWidget *parent)
-  : QFrame(parent)
-  , room_id_{room_id}
-{
-        setAutoFillBackground(true);
-        setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
-        setWindowModality(Qt::WindowModal);
-        setAttribute(Qt::WA_DeleteOnClose, true);
-
-        auto layout = new QVBoxLayout(this);
-        layout->setSpacing(conf::modals::WIDGET_SPACING);
-        layout->setMargin(conf::modals::WIDGET_MARGIN);
-
-        list_ = new QListWidget;
-        list_->setFrameStyle(QFrame::NoFrame);
-        list_->setSelectionMode(QAbstractItemView::NoSelection);
-        list_->setSpacing(5);
-
-        QFont largeFont;
-        largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
-        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-        setMinimumHeight(list_->sizeHint().height() * 2);
-        setMinimumWidth(std::max(list_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
-                                 QFontMetrics(largeFont).averageCharWidth() * 30 -
-                                   2 * conf::modals::WIDGET_MARGIN));
-
-        QFont font;
-        font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
-        topLabel_ = new QLabel(tr("Room members"), this);
-        topLabel_->setAlignment(Qt::AlignCenter);
-        topLabel_->setFont(font);
-
-        auto okBtn = new QPushButton(tr("OK"), this);
-
-        auto buttonLayout = new QHBoxLayout();
-        buttonLayout->setSpacing(15);
-        buttonLayout->addStretch(1);
-        buttonLayout->addWidget(okBtn);
-
-        layout->addWidget(topLabel_);
-        layout->addWidget(list_);
-        layout->addLayout(buttonLayout);
-
-        list_->clear();
-
-        connect(list_->verticalScrollBar(), &QAbstractSlider::valueChanged, this, [this](int pos) {
-                if (pos != list_->verticalScrollBar()->maximum())
-                        return;
-
-                const size_t numMembers = list_->count() - 1;
-
-                if (numMembers > 0)
-                        addUsers(cache::getMembers(room_id_.toStdString(), numMembers));
-        });
-
-        try {
-                addUsers(cache::getMembers(room_id_.toStdString()));
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
-        }
-
-        auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
-        connect(closeShortcut, &QShortcut::activated, this, &MemberList::close);
-        connect(okBtn, &QPushButton::clicked, this, &MemberList::close);
-}
-
-void
-MemberList::addUsers(const std::vector<RoomMember> &members)
-{
-        for (const auto &member : members) {
-                auto user = new MemberItem(member, this);
-                auto item = new QListWidgetItem;
-
-                item->setSizeHint(user->minimumSizeHint());
-                item->setFlags(Qt::NoItemFlags);
-                item->setTextAlignment(Qt::AlignCenter);
-
-                list_->insertItem(list_->count() - 1, item);
-                list_->setItemWidget(item, user);
-        }
-}
diff --git a/src/dialogs/MemberList.h b/src/dialogs/MemberList.h
deleted file mode 100644
index b822eec80faa39ff7c46fff6e64fb84d5018f19d..0000000000000000000000000000000000000000
--- a/src/dialogs/MemberList.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-#include <QListWidget>
-
-class Avatar;
-class QPushButton;
-class QHBoxLayout;
-class QLabel;
-class QVBoxLayout;
-
-struct RoomMember;
-
-template<class T>
-class QSharedPointer;
-
-namespace dialogs {
-
-class MemberItem : public QWidget
-{
-        Q_OBJECT
-
-public:
-        MemberItem(const RoomMember &member, QWidget *parent);
-
-protected:
-        void paintEvent(QPaintEvent *) override;
-
-private:
-        QHBoxLayout *topLayout_;
-        QVBoxLayout *textLayout_;
-
-        Avatar *avatar_;
-
-        QLabel *userName_;
-        QLabel *userId_;
-};
-
-class MemberList : public QFrame
-{
-        Q_OBJECT
-public:
-        MemberList(const QString &room_id, QWidget *parent = nullptr);
-
-public slots:
-        void addUsers(const std::vector<RoomMember> &users);
-
-private:
-        QString room_id_;
-        QLabel *topLabel_;
-        QListWidget *list_;
-};
-} // dialogs
diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp
index 1eaab468b87083f9728291ba1c22eb69ae8187af..e495704514e851bda0c9eba21b2685dc2de67552 100644
--- a/src/timeline/Permissions.cpp
+++ b/src/timeline/Permissions.cpp
@@ -8,9 +8,9 @@
 #include "MatrixClient.h"
 #include "TimelineModel.h"
 
-Permissions::Permissions(TimelineModel *parent)
+Permissions::Permissions(QString roomId, QObject *parent)
   : QObject(parent)
-  , room(parent)
+  , roomId_(roomId)
 {
         invalidate();
 }
@@ -19,7 +19,7 @@ void
 Permissions::invalidate()
 {
         pl = cache::client()
-               ->getStateEvent<mtx::events::state::PowerLevels>(room->roomId().toStdString())
+               ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
                .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
                .content;
 }
diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h
index f7e6f389d2a260f31ebb3c4c9ebfadc100b4b58d..7aab1ddb83bafb6ef227188e6130595cab133613 100644
--- a/src/timeline/Permissions.h
+++ b/src/timeline/Permissions.h
@@ -15,7 +15,7 @@ class Permissions : public QObject
         Q_OBJECT
 
 public:
-        Permissions(TimelineModel *parent);
+        Permissions(QString roomId, QObject *parent = nullptr);
 
         Q_INVOKABLE bool canInvite();
         Q_INVOKABLE bool canBan();
@@ -28,6 +28,6 @@ public:
         void invalidate();
 
 private:
-        TimelineModel *room;
+        QString roomId_;
         mtx::events::state::PowerLevels pl;
 };
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 7b3f07297f63b7cfa992590e941dbd562fee5d67..66d931fdeea0217d2920d0580408ccd6fbce2ce0 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -25,6 +25,7 @@
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
+#include "MemberList.h"
 #include "MxcImageProvider.h"
 #include "Olm.h"
 #include "TimelineViewManager.h"
@@ -317,6 +318,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
   , events(room_id.toStdString(), this)
   , room_id_(room_id)
   , manager_(manager)
+  , permissions_{room_id}
 {
         lastMessage_.timestamp = 0;
 
@@ -325,6 +327,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
                 this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
         this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
 
+        // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
+        // needs to be
+        connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
+
         connect(
           this,
           &TimelineModel::redactionFailed,
@@ -1061,11 +1067,28 @@ TimelineModel::openUserProfile(QString userid)
 }
 
 void
-TimelineModel::openRoomSettings()
+TimelineModel::openRoomMembers()
+{
+        MemberList *memberList = new MemberList(roomId());
+        emit openRoomMembersDialog(memberList);
+}
+
+void
+TimelineModel::openRoomSettings(QString room_id)
 {
-        RoomSettings *settings = new RoomSettings(roomId(), this);
+        RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this);
         connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
-        openRoomSettingsDialog(settings);
+        emit openRoomSettingsDialog(settings);
+}
+
+void
+TimelineModel::openInviteUsers(QString roomId)
+{
+        InviteesModel *model = new InviteesModel{this};
+        connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
+                emit manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids());
+        });
+        emit openInviteUsersDialog(model);
 }
 
 void
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3c80ade8345f6383b7d1cf3e602e30efcce49602..0d1eb1f9b73f1fdfe80d660767d4980fbeadecb8 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -17,6 +17,8 @@
 #include "CacheStructs.h"
 #include "EventStore.h"
 #include "InputBar.h"
+#include "InviteesModel.h"
+#include "MemberList.h"
 #include "Permissions.h"
 #include "ui/RoomSettings.h"
 #include "ui/UserProfile.h"
@@ -158,7 +160,9 @@ class TimelineModel : public QAbstractListModel
         Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
         Q_PROPERTY(
           bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
+        Q_PROPERTY(QString roomId READ roomId CONSTANT)
         Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+        Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
         Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
         Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
         Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
@@ -235,7 +239,9 @@ public:
         Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
         Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
         Q_INVOKABLE void openUserProfile(QString userid);
-        Q_INVOKABLE void openRoomSettings();
+        Q_INVOKABLE void openRoomMembers();
+        Q_INVOKABLE void openRoomSettings(QString room_id = QString());
+        Q_INVOKABLE void openInviteUsers(QString roomId = QString());
         Q_INVOKABLE void editAction(QString id);
         Q_INVOKABLE void replyAction(QString id);
         Q_INVOKABLE void readReceiptsAction(QString id) const;
@@ -352,7 +358,9 @@ signals:
         void lastMessageChanged();
         void notificationsChanged();
 
+        void openRoomMembersDialog(MemberList *members);
         void openRoomSettingsDialog(RoomSettings *settings);
+        void openInviteUsersDialog(InviteesModel *invitees);
 
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
         void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
@@ -360,6 +368,7 @@ signals:
 
         void encryptionChanged();
         void roomNameChanged();
+        void plainRoomNameChanged();
         void roomTopicChanged();
         void roomAvatarUrlChanged();
         void roomMemberCountChanged();
@@ -389,7 +398,7 @@ private:
         TimelineViewManager *manager_;
 
         InputBar input_{this};
-        Permissions permissions_{this};
+        Permissions permissions_;
 
         QTimer showEventTimer{this};
         QString eventIdToShow;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3e69f92b79c81ade617196cd1f9cdcb734cb740c..64493e5b5b2c8317541c474e09123ab5c76fb018 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -20,6 +20,7 @@
 #include "DeviceVerificationFlow.h"
 #include "EventAccessors.h"
 #include "ImagePackModel.h"
+#include "InviteesModel.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
@@ -174,6 +175,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           0,
           "UserProfileModel",
           "UserProfile needs to be instantiated on the C++ side");
+        qmlRegisterUncreatableType<MemberList>(
+          "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side");
         qmlRegisterUncreatableType<RoomSettings>(
           "im.nheko",
           1,
@@ -182,6 +185,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "Room Settings needs to be instantiated on the C++ side");
         qmlRegisterUncreatableType<TimelineModel>(
           "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side");
+        qmlRegisterUncreatableType<InviteesModel>(
+          "im.nheko",
+          1,
+          0,
+          "InviteesModel",
+          "InviteesModel needs to be instantiated on the C++ side");
 
         static auto self = this;
         qmlRegisterSingletonType<MainWindow>(
@@ -421,17 +430,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
         });
 }
 
-void
-TimelineViewManager::openInviteUsersDialog()
-{
-        MainWindow::instance()->openInviteUsersDialog(
-          [this](const QStringList &invitees) { emit inviteUsers(invitees); });
-}
-void
-TimelineViewManager::openMemberListDialog(QString roomid) const
-{
-        MainWindow::instance()->openMemberListDialog(roomid);
-}
 void
 TimelineViewManager::openLeaveRoomDialog(QString roomid) const
 {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 15b4f523d045b1ddb1ee0c2c4413e30c2ee861f3..945ba2d560470848738b304273384227af8a6d45 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -65,8 +65,6 @@ public:
         Q_INVOKABLE QString userStatus(QString id) const;
 
         Q_INVOKABLE void focusMessageInput();
-        Q_INVOKABLE void openInviteUsersDialog();
-        Q_INVOKABLE void openMemberListDialog(QString roomid) const;
         Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
         Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
 
@@ -81,7 +79,9 @@ signals:
         void replyingEventChanged(QString replyingEvent);
         void replyClosed();
         void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
-        void inviteUsers(QStringList users);
+        void inviteUsers(QString roomId, QStringList users);
+        void showRoomList();
+        void narrowViewChanged();
         void focusChanged();
         void focusInput();
         void openImageOverlayInternalCb(QString eventId, QImage img);