summary refs log tree commit diff
path: root/resources
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2023-06-02 01:45:24 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2023-06-08 22:32:08 +0200
commit5aee8d609a3fcca63bb9a0f983a77b45eebfefe7 (patch)
tree1f270d525e4e5b5db381d19675467620593368cd /resources
parentlint (diff)
downloadnheko-5aee8d609a3fcca63bb9a0f983a77b45eebfefe7.tar.xz
Format qml
Diffstat (limited to 'resources')
-rw-r--r--resources/qml/Avatar.qml61
-rw-r--r--resources/qml/ChatPage.qml70
-rw-r--r--resources/qml/CommunitiesList.qml172
-rw-r--r--resources/qml/Completer.qml212
-rw-r--r--resources/qml/ElidedLabel.qml9
-rw-r--r--resources/qml/EncryptionIndicator.qml55
-rw-r--r--resources/qml/ForwardCompleter.qml69
-rw-r--r--resources/qml/ImageButton.qml15
-rw-r--r--resources/qml/MatrixText.qml18
-rw-r--r--resources/qml/MatrixTextField.qml124
-rw-r--r--resources/qml/MessageInput.qml279
-rw-r--r--resources/qml/MessageInputWarning.qml20
-rw-r--r--resources/qml/MessageView.qml875
-rw-r--r--resources/qml/PrivacyScreen.qml36
-rw-r--r--resources/qml/QuickSwitcher.qml55
-rw-r--r--resources/qml/Reactions.qml76
-rw-r--r--resources/qml/ReplyPopup.qml64
-rw-r--r--resources/qml/RoomList.qml1081
-rw-r--r--resources/qml/Root.qml392
-rw-r--r--resources/qml/SelfVerificationCheck.qml166
-rw-r--r--resources/qml/StatusIndicator.qml24
-rw-r--r--resources/qml/TimelineRow.qml358
-rw-r--r--resources/qml/TimelineView.qml275
-rw-r--r--resources/qml/ToggleButton.qml79
-rw-r--r--resources/qml/TopBar.qml382
-rw-r--r--resources/qml/TypingIndicator.qml10
-rw-r--r--resources/qml/UploadBox.qml67
27 files changed, 2353 insertions, 2691 deletions
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 8302f8fa..53124f28 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -11,45 +11,44 @@ import im.nheko 1.0
 AbstractButton {
     id: avatar
 
-    property string url
-    property string userid
-    property string roomid
+    property alias color: bg.color
+    property bool crop: true
     property string displayName
+    property string roomid
     property alias textColor: label.color
-    property bool crop: true
-    property alias color: bg.color
+    property string url
+    property string userid
 
-    width: 48
     height: 48
+    width: 48
+
     background: Rectangle {
         id: bg
-        radius: Settings.avatarCircles ? height / 2 : height / 8
+
         color: palette.alternateBase
+        radius: Settings.avatarCircles ? height / 2 : height / 8
     }
 
     Label {
         id: label
 
-        enabled: false
-
         anchors.fill: parent
+        color: palette.text
+        enabled: false
+        font.pixelSize: avatar.height / 2
+        horizontalAlignment: Text.AlignHCenter
         text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
         textFormat: Text.RichText
-        font.pixelSize: avatar.height / 2
         verticalAlignment: Text.AlignVCenter
-        horizontalAlignment: Text.AlignHCenter
         visible: img.status != Image.Ready && !Settings.useIdenticon
-        color: palette.text
     }
-
     Image {
         id: identicon
 
         anchors.fill: parent
-        visible: Settings.useIdenticon && img.status != Image.Ready
         source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""
+        visible: Settings.useIdenticon && img.status != Image.Ready
     }
-
     Image {
         id: img
 
@@ -58,8 +57,6 @@ AbstractButton {
         fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
         mipmap: true
         smooth: true
-        sourceSize.width: avatar.width * Screen.devicePixelRatio
-        sourceSize.height: avatar.height * Screen.devicePixelRatio
         source: if (avatar.url.startsWith('image://')) {
             return avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale");
         } else if (avatar.url.startsWith(':/')) {
@@ -67,20 +64,12 @@ AbstractButton {
         } else {
             return "";
         }
-
+        sourceSize.height: avatar.height * Screen.devicePixelRatio
+        sourceSize.width: avatar.width * Screen.devicePixelRatio
     }
-
     Rectangle {
         id: onlineIndicator
 
-        anchors.bottom: avatar.bottom
-        anchors.right: avatar.right
-        visible: !!userid
-        height: avatar.height / 6
-        width: height
-        radius: Settings.avatarCircles ? height / 2 : height / 8
-        color: updatePresence()
-
         function updatePresence() {
             switch (Presence.userPresence(userid)) {
             case "online":
@@ -94,22 +83,28 @@ AbstractButton {
             }
         }
 
-        Connections {
-            target: Presence
+        anchors.bottom: avatar.bottom
+        anchors.right: avatar.right
+        color: updatePresence()
+        height: avatar.height / 6
+        radius: Settings.avatarCircles ? height / 2 : height / 8
+        visible: !!userid
+        width: height
 
+        Connections {
             function onPresenceChanged(id) {
-                if (id == userid) onlineIndicator.color = onlineIndicator.updatePresence();
+                if (id == userid)
+                    onlineIndicator.color = onlineIndicator.updatePresence();
             }
+
+            target: Presence
         }
     }
-
     CursorShape {
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
     }
-
     Ripple {
         color: Qt.rgba(palette.alternateBase.r, palette.alternateBase.g, palette.alternateBase.b, 0.5)
     }
-
 }
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 564c093d..2803e97d 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -17,16 +17,16 @@ Rectangle {
     color: palette.window
 
     ColumnLayout {
-        spacing: 0
         anchors.fill: parent
+        spacing: 0
 
         Rectangle {
             id: offlineIndicator
 
+            Layout.fillWidth: true
+            Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium
             color: Nheko.theme.error
             visible: !TimelineManager.isConnected
-            Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium
-            Layout.fillWidth: true
             z: 1
 
             Label {
@@ -36,18 +36,9 @@ Rectangle {
                 text: qsTr("No network connection")
             }
         }
-
         AdaptiveLayout {
             id: adaptiveView
 
-            Layout.fillWidth: true
-            Layout.fillHeight: true
-            singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
-            pageIndex: 1
-
-            Component.onCompleted: initializePageIndex()
-            onSinglePageModeChanged: initializePageIndex()
-
             function initializePageIndex() {
                 if (!singlePageMode)
                     adaptiveView.pageIndex = 0;
@@ -57,67 +48,67 @@ Rectangle {
                     adaptiveView.pageIndex = 1;
             }
 
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            pageIndex: 1
+            singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
+
+            Component.onCompleted: initializePageIndex()
+            onSinglePageModeChanged: initializePageIndex()
+
             Connections {
-                target: Rooms
                 function onCurrentRoomChanged() {
                     adaptiveView.initializePageIndex();
                 }
-            }
 
+                target: Rooms
+            }
             AdaptiveLayoutElement {
                 id: communityListC
 
-                visible: Settings.groupView
-                minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
                 collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium
-                preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth
                 maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium
+                minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
+                preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth
+                visible: Settings.groupView
 
                 CommunitiesList {
                     id: communitiesList
 
                     collapsed: parent.collapsed
                 }
-
                 Binding {
-                    target: Settings
+                    delayed: true
                     property: 'communityListWidth'
+                    restoreMode: Binding.RestoreBindingOrValue
+                    target: Settings
                     value: communityListC.preferredWidth
                     when: !adaptiveView.singlePageMode
-                    delayed: true
-                    restoreMode: Binding.RestoreBindingOrValue
                 }
-
             }
-
             AdaptiveLayoutElement {
                 id: roomListC
 
-                minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2
-                preferredWidth: (Settings.roomListWidth == - 1)
-                    ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2)
-                    : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth)
-                maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2
                 collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium
+                maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2
+                minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2
+                preferredWidth: (Settings.roomListWidth == -1) ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2) : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth)
 
                 RoomList {
                     id: roomlist
 
-                    height: adaptiveView.height
                     collapsed: parent.collapsed
+                    height: adaptiveView.height
                 }
-
                 Binding {
-                    target: Settings
+                    delayed: true
                     property: 'roomListWidth'
+                    restoreMode: Binding.RestoreBindingOrValue
+                    target: Settings
                     value: roomListC.preferredWidth
                     when: !adaptiveView.singlePageMode
-                    delayed: true
-                    restoreMode: Binding.RestoreBindingOrValue
                 }
-
             }
-
             AdaptiveLayoutElement {
                 id: timlineViewC
 
@@ -127,25 +118,20 @@ Rectangle {
                     id: timeline
 
                     privacyScreen: privacyScreen
-                    showBackButton: adaptiveView.singlePageMode
                     room: Rooms.currentRoom
                     roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null
+                    showBackButton: adaptiveView.singlePageMode
                 }
-
             }
-
         }
-
     }
-
     PrivacyScreen {
         id: privacyScreen
 
         anchors.fill: parent
-        visible: Settings.privacyScreen
         screenTimeout: Settings.privacyScreenTimeout
         timelineRoot: adaptiveView
+        visible: Settings.privacyScreen
         windowTarget: MainWindow
     }
-
 }
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index a210a4bb..62a29a2d 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -13,19 +13,24 @@ import im.nheko 1.0
 
 Page {
     id: communitySidebar
+
     //leftPadding: Nheko.paddingSmall
     //rightPadding: Nheko.paddingSmall
     property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6)
     property bool collapsed: false
 
+    background: Rectangle {
+        color: Nheko.theme.sidebarBackground
+    }
+
     // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
     Connections {
         function onHideMenu() {
-            communityContextMenu.close()
+            communityContextMenu.close();
         }
+
         target: MainWindow
     }
-
     ListView {
         id: communitiesList
 
@@ -36,195 +41,180 @@ Page {
 
         ScrollBar.vertical: ScrollBar {
             id: scrollbar
-            parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null
-        }
-
-        Platform.Menu {
-            id: communityContextMenu
-
-            property string tagId
-            property bool hidden
-            property bool muted
-
-            function show(id_, hidden_, muted_) {
-                tagId = id_;
-                hidden = hidden_;
-                muted = muted_;
-                open();
-            }
-
-            Platform.MenuItem {
-                text: qsTr("Do not show notification counts for this community or tag.")
-                checkable: true
-                checked: communityContextMenu.muted
-                onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
-            }
-
-            Platform.MenuItem {
-                text: qsTr("Hide rooms with this tag or from this community by default.")
-                checkable: true
-                checked: communityContextMenu.hidden
-                onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
-            }
 
+            parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null
         }
-
         delegate: ItemDelegate {
             id: communityItem
 
             property color backgroundColor: palette.window
-            property color importantText: palette.text
-            property color unimportantText: palette.buttonText
             property color bubbleBackground: palette.highlight
             property color bubbleText: palette.highlightedText
+            property color importantText: palette.text
             required property var model
+            property color unimportantText: palette.buttonText
 
+            ToolTip.delay: Nheko.tooltipDelay
+            ToolTip.text: model.tooltip
+            ToolTip.visible: hovered && collapsed
             height: avatarSize + 2 * Nheko.paddingMedium
-            width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
             state: "normal"
-            ToolTip.visible: hovered && collapsed
-            ToolTip.text: model.tooltip
-            ToolTip.delay: Nheko.tooltipDelay
-            onClicked: Communities.setCurrentTagId(model.id)
-            onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
+            width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
+
+            background: Rectangle {
+                color: communityItem.backgroundColor
+            }
             states: [
                 State {
                     name: "highlight"
                     when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id)
 
                     PropertyChanges {
-                        target: communityItem
                         backgroundColor: palette.dark
-                        importantText: palette.brightText
-                        unimportantText: palette.brightText
                         bubbleBackground: palette.highlight
                         bubbleText: palette.highlightedText
+                        importantText: palette.brightText
+                        target: communityItem
+                        unimportantText: palette.brightText
                     }
-
                 },
                 State {
                     name: "selected"
                     when: Communities.currentTagId == model.id
 
                     PropertyChanges {
-                        target: communityItem
                         backgroundColor: palette.highlight
-                        importantText: palette.highlightedText
-                        unimportantText: palette.highlightedText
                         bubbleBackground: palette.highlightedText
                         bubbleText: palette.highlight
+                        importantText: palette.highlightedText
+                        target: communityItem
+                        unimportantText: palette.highlightedText
                     }
-
                 }
             ]
 
+            onClicked: Communities.setCurrentTagId(model.id)
+            onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
+
             Item {
                 anchors.fill: parent
 
                 TapHandler {
                     acceptedButtons: Qt.RightButton
-                    onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
-                    gesturePolicy: TapHandler.ReleaseWithinBounds
                     acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
-                }
+                    gesturePolicy: TapHandler.ReleaseWithinBounds
 
+                    onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
+                }
             }
-
             RowLayout {
                 id: r
-                spacing: Nheko.paddingMedium
+
                 anchors.fill: parent
-                anchors.margins: Nheko.paddingMedium
                 anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
+                anchors.margins: Nheko.paddingMedium
+                spacing: Nheko.paddingMedium
 
                 ImageButton {
-                    visible: !communitySidebar.collapsed && model.collapsible
+                    Layout.alignment: Qt.AlignVCenter
                     Layout.preferredHeight: fontMetrics.lineSpacing
                     Layout.preferredWidth: fontMetrics.lineSpacing
-                    Layout.alignment: Qt.AlignVCenter
-                    height: fontMetrics.lineSpacing
-                    width: fontMetrics.lineSpacing
-                    image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
+                    ToolTip.visible: hovered
+                    height: fontMetrics.lineSpacing
                     hoverEnabled: true
+                    image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
+                    visible: !communitySidebar.collapsed && model.collapsible
+                    width: fontMetrics.lineSpacing
 
                     onClicked: model.collapsed = !model.collapsed
                 }
-
                 Item {
                     Layout.preferredWidth: fontMetrics.lineSpacing
                     visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces
                 }
-
                 Avatar {
                     id: avatar
 
-                    enabled: false
                     Layout.alignment: Qt.AlignVCenter
+                    color: communityItem.backgroundColor
+                    displayName: model.displayName
+                    enabled: false
                     height: avatarSize
-                    width: avatarSize
+                    roomid: model.id
                     url: {
                         if (model.avatarUrl.startsWith("mxc://"))
                             return model.avatarUrl.replace("mxc://", "image://MxcImage/");
                         else
                             return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
                     }
-                    roomid: model.id
-                    displayName: model.displayName
-                    color: communityItem.backgroundColor
+                    width: avatarSize
 
                     NotificationBubble {
-                        notificationCount: model.unreadMessages
-                        hasLoudNotification: model.hasLoudNotification
+                        anchors.bottom: avatar.bottom
+                        anchors.margins: -Nheko.paddingSmall
+                        anchors.right: avatar.right
                         bubbleBackgroundColor: communityItem.bubbleBackground
                         bubbleTextColor: communityItem.bubbleText
                         font.pixelSize: fontMetrics.font.pixelSize * 0.6
+                        hasLoudNotification: model.hasLoudNotification
                         mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
-                        anchors.right: avatar.right
-                        anchors.bottom: avatar.bottom
-                        anchors.margins: -Nheko.paddingSmall
+                        notificationCount: model.unreadMessages
                     }
-
                 }
-
                 ElidedLabel {
-                    visible: !communitySidebar.collapsed
                     Layout.alignment: Qt.AlignVCenter
-                    color: communityItem.importantText
                     Layout.fillWidth: true
+                    color: communityItem.importantText
                     elideWidth: width
                     fullText: model.displayName
                     textFormat: Text.PlainText
+                    visible: !communitySidebar.collapsed
                 }
-
                 Item {
                     Layout.fillWidth: true
                 }
-
                 NotificationBubble {
-                    notificationCount: model.unreadMessages
-                    hasLoudNotification: model.hasLoudNotification
+                    Layout.alignment: Qt.AlignRight
+                    Layout.leftMargin: Nheko.paddingSmall
                     bubbleBackgroundColor: communityItem.bubbleBackground
                     bubbleTextColor: communityItem.bubbleText
+                    hasLoudNotification: model.hasLoudNotification
                     mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
-                    Layout.alignment: Qt.AlignRight
-                    Layout.leftMargin: Nheko.paddingSmall
+                    notificationCount: model.unreadMessages
                 }
-
             }
+        }
 
-            background: Rectangle {
-                color: communityItem.backgroundColor
+        Platform.Menu {
+            id: communityContextMenu
+
+            property bool hidden
+            property bool muted
+            property string tagId
+
+            function show(id_, hidden_, muted_) {
+                tagId = id_;
+                hidden = hidden_;
+                muted = muted_;
+                open();
             }
 
-        }
+            Platform.MenuItem {
+                checkable: true
+                checked: communityContextMenu.muted
+                text: qsTr("Do not show notification counts for this community or tag.")
 
-    }
+                onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
+            }
+            Platform.MenuItem {
+                checkable: true
+                checked: communityContextMenu.hidden
+                text: qsTr("Hide rooms with this tag or from this community by default.")
 
-    background: Rectangle {
-        color: Nheko.theme.sidebarBackground
+                onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
+            }
+        }
     }
-
 }
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 02dccfc9..00141d4d 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -11,117 +11,102 @@ import im.nheko 1.0
 Control {
     id: popup
 
-    property alias currentIndex: listView.currentIndex
-    property string roomId
-    property string completerName
-    property var completer
-    property bool bottomToTop: true
-    property bool fullWidth: false
-    property bool centerRowContent: true
     property int avatarHeight: 24
     property int avatarWidth: 24
+    property bool bottomToTop: true
+    property bool centerRowContent: true
+    property var completer
+    property string completerName
+    property alias count: listView.count
+    property alias currentIndex: listView.currentIndex
+    property bool fullWidth: false
+    property string roomId
     property int rowMargin: 0
     property int rowSpacing: Nheko.paddingSmall
-    property alias count: listView.count
 
     signal completionClicked(string completion)
     signal completionSelected(string id)
 
-    function up() {
-        if (bottomToTop)
-            down_();
+    function changeCompleter() {
+        if (completerName) {
+            completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId));
+            completer.setSearchString("");
+        } else {
+            completer = undefined;
+        }
+        currentIndex = -1;
+    }
+    function currentCompletion() {
+        if (currentIndex > -1 && currentIndex < listView.count)
+            return completer.completionAt(currentIndex);
         else
-            up_();
+            return null;
     }
-
     function down() {
         if (bottomToTop)
             up_();
         else
             down_();
     }
-
-    function up_() {
-        currentIndex = currentIndex - 1;
-        if (currentIndex == -2)
-            currentIndex = listView.count - 1;
-
-    }
-
     function down_() {
         currentIndex = currentIndex + 1;
         if (currentIndex >= listView.count)
             currentIndex = -1;
-
-    }
-
-    function currentCompletion() {
-        if (currentIndex > -1 && currentIndex < listView.count)
-            return completer.completionAt(currentIndex);
-        else
-            return null;
     }
-
     function finishCompletion() {
         if (popup.completerName == "room")
             popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
         else if (popup.completerName == "user")
             popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid);
-
     }
-
-    function changeCompleter() {
-        if (completerName) {
-            completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId :  room.roomId));
-            completer.setSearchString("");
-        } else {
-            completer = undefined;
-        }
-        currentIndex = -1
+    function up() {
+        if (bottomToTop)
+            down_();
+        else
+            up_();
+    }
+    function up_() {
+        currentIndex = currentIndex - 1;
+        if (currentIndex == -2)
+            currentIndex = listView.count - 1;
     }
-    onCompleterNameChanged: changeCompleter()
-    onRoomIdChanged: changeCompleter()
 
     bottomPadding: 1
     leftPadding: 1
-    topPadding: 1
     rightPadding: 1
+    topPadding: 1
 
+    background: Rectangle {
+        border.color: palette.mid
+        color: palette.base
+    }
     contentItem: ListView {
         id: listView
 
-        // If we have fewer than 7 items, just use the list view's content height.  
+        clip: true
+        displayMarginBeginning: height / 2
+        displayMarginEnd: height / 2
+        highlightFollowsCurrentItem: true
+
+        // If we have fewer than 7 items, just use the list view's content height.
         // Otherwise, we want to show 7 items.  Each item consists of row spacing between rows, row margins
         // on each side of a row, 1px of padding above the first item and below the last item, and nominally
         // some kind of content height.  avatarHeight is used for just about every delegate, so we're using
         // that until we find something better.  Put is all together and you have the formula below!
-        implicitHeight: Math.min(contentHeight, 6*rowSpacing + 7*(popup.avatarHeight + 2*rowMargin))
-        clip: true 
-
-        Timer {
-            id: deadTimer
-            interval: 50
-        }
-
-        onContentYChanged: deadTimer.restart()
+        implicitHeight: Math.min(contentHeight, 6 * rowSpacing + 7 * (popup.avatarHeight + 2 * rowMargin))
 
         // Broken, see https://bugreports.qt.io/browse/QTBUG-102811
         //reuseItems: true
         implicitWidth: listView.contentItem.childrenRect.width
         model: completer
-        verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
-        spacing: rowSpacing
         pixelAligned: true
-        highlightFollowsCurrentItem: true
-
-        displayMarginBeginning: height / 2
-        displayMarginEnd: height / 2
+        spacing: rowSpacing
+        verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
 
         delegate: Rectangle {
             property variant modelData: model
 
             ListView.delayRemove: true
-
             color: model.index == popup.currentIndex ? palette.highlight : palette.base
             height: (chooser.child?.implicitHeight ?? 0) + 2 * popup.rowMargin
             implicitWidth: fullWidth ? ListView.view.width : chooser.child.implicitWidth + 4
@@ -131,26 +116,27 @@ Control {
 
                 anchors.fill: parent
                 hoverEnabled: true
-                onPositionChanged: if (!listView.moving && !deadTimer.running) popup.currentIndex = model.index
+
                 onClicked: {
-                     popup.completionClicked(completer.completionAt(model.index));
-                     if (popup.completerName == "room")
-                         popup.completionSelected(model.roomid);
-                     else if (popup.completerName == "user")
-                         popup.completionSelected(model.userid);
+                    popup.completionClicked(completer.completionAt(model.index));
+                    if (popup.completerName == "room")
+                        popup.completionSelected(model.roomid);
+                    else if (popup.completerName == "user")
+                        popup.completionSelected(model.userid);
                 }
+                onPositionChanged: if (!listView.moving && !deadTimer.running)
+                    popup.currentIndex = model.index
             }
             Ripple {
                 color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.5)
             }
-
             DelegateChooser {
                 id: chooser
 
-                roleValue: popup.completerName
                 anchors.fill: parent
                 anchors.margins: popup.rowMargin
                 enabled: false
+                roleValue: popup.completerName
 
                 DelegateChoice {
                     roleValue: "user"
@@ -162,28 +148,23 @@ Control {
                         spacing: rowSpacing
 
                         Avatar {
-                            height: popup.avatarHeight
-                            width: popup.avatarWidth
                             displayName: model.displayName
-                            userid: model.userid
-                            url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
                             enabled: false
+                            height: popup.avatarHeight
+                            url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                            userid: model.userid
+                            width: popup.avatarWidth
                         }
-
                         Label {
-                            text: model.displayName
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
+                            text: model.displayName
                         }
-
                         Label {
-                            text: "(" + model.userid + ")"
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
+                            text: "(" + model.userid + ")"
                         }
-
                     }
-
                 }
-
                 DelegateChoice {
                     roleValue: "emoji"
 
@@ -194,39 +175,33 @@ Control {
                         spacing: rowSpacing
 
                         Label {
-                            visible: !!model.unicode
-                            text: model.unicode
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
                             font: Settings.emojiFont
+                            text: model.unicode
+                            visible: !!model.unicode
                         }
-
                         Avatar {
-                            visible: !model.unicode
-                            height: popup.avatarHeight
-                            width: popup.avatarWidth
+                            crop: false
                             displayName: model.shortcode
+                            enabled: false
+                            height: popup.avatarHeight
                             //userid: model.shortcode
                             url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/")
-                            enabled: false
-                            crop: false
+                            visible: !model.unicode
+                            width: popup.avatarWidth
                         }
-
                         Label {
                             Layout.leftMargin: Nheko.paddingSmall
                             Layout.rightMargin: Nheko.paddingSmall
-                            text: model.shortcode
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
+                            text: model.shortcode
                         }
-
                         Label {
-                            text: "(" + model.packname + ")"
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
+                            text: "(" + model.packname + ")"
                         }
-
                     }
-
                 }
-
                 DelegateChoice {
                     roleValue: "command"
 
@@ -237,20 +212,16 @@ Control {
                         spacing: rowSpacing
 
                         Label {
-                            text: model.name
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
                             font.bold: true
+                            text: model.name
                         }
-
                         Label {
-                            text: model.description
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
+                            text: model.description
                         }
-
                     }
-
                 }
-
                 DelegateChoice {
                     roleValue: "room"
 
@@ -261,26 +232,22 @@ Control {
                         spacing: rowSpacing
 
                         Avatar {
-                            height: popup.avatarHeight
-                            width: popup.avatarWidth
                             displayName: model.roomName
+                            enabled: false
+                            height: popup.avatarHeight
                             roomid: model.roomid
                             url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
-                            enabled: false
+                            width: popup.avatarWidth
                         }
-
                         Label {
-                            text: model.roomName
-                            font.pixelSize: popup.avatarHeight * 0.5
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
                             font.italic: model.isTombstoned
+                            font.pixelSize: popup.avatarHeight * 0.5
+                            text: model.roomName
                             textFormat: Text.RichText
                         }
-
                     }
-
                 }
-
                 DelegateChoice {
                     roleValue: "roomAliases"
 
@@ -291,41 +258,38 @@ Control {
                         spacing: rowSpacing
 
                         Avatar {
-                            height: popup.avatarHeight
-                            width: popup.avatarWidth
                             displayName: model.roomName
+                            enabled: false
+                            height: popup.avatarHeight
                             roomid: model.roomid
                             url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
-                            enabled: false
+                            width: popup.avatarWidth
                         }
-
                         Label {
-                            text: model.roomName
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
                             font.italic: model.isTombstoned
+                            text: model.roomName
                             textFormat: Text.RichText
                         }
-
                         Label {
-                            text: "(" + model.roomAlias + ")"
                             color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
+                            text: "(" + model.roomAlias + ")"
                             textFormat: Text.RichText
                         }
-
                     }
-
                 }
-
             }
-
         }
 
-    }
+        onContentYChanged: deadTimer.restart()
 
+        Timer {
+            id: deadTimer
 
-    background: Rectangle {
-        color: palette.base
-        border.color: palette.mid
+            interval: 50
+        }
     }
 
+    onCompleterNameChanged: changeCompleter()
+    onRoomIdChanged: changeCompleter()
 }
diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml
index 2d53faff..153d7c33 100644
--- a/resources/qml/ElidedLabel.qml
+++ b/resources/qml/ElidedLabel.qml
@@ -9,21 +9,20 @@ import im.nheko 1.0
 Label {
     id: root
 
-    property alias fullText: metrics.text
     property alias elideWidth: metrics.elideWidth
+    property alias fullText: metrics.text
     property int fullTextWidth: Math.ceil(metrics.advanceWidth)
 
     color: palette.text
-    text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText)
-    maximumLineCount: 1
     elide: Text.ElideRight
+    maximumLineCount: 1
+    text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText)
     textFormat: Text.PlainText
 
     TextMetrics {
         id: metrics
 
-        font.pointSize: root.font.pointSize
         elide: Text.ElideRight
+        font.pointSize: root.font.pointSize
     }
-
 }
diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml
index c675fb52..fb9dc7b5 100644
--- a/resources/qml/EncryptionIndicator.qml
+++ b/resources/qml/EncryptionIndicator.qml
@@ -11,32 +11,40 @@ Image {
     id: stateImg
 
     property bool encrypted: false
-    property int trust: Crypto.Unverified
-    property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg"
-    property color unencryptedColor: Nheko.theme.error
-    property color unencryptedHoverColor: unencryptedColor
     property bool hovered: ma.hovered
-
     property string sourceUrl: {
         if (!encrypted)
-        return "image://colorimage/" + unencryptedIcon + "?";
-
+            return "image://colorimage/" + unencryptedIcon + "?";
         switch (trust) {
-            case Crypto.Verified:
+        case Crypto.Verified:
             return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?";
-            case Crypto.TOFU:
+        case Crypto.TOFU:
             return "image://colorimage/:/icons/icons/ui/shield-filled.svg?";
-            case Crypto.Unverified:
+        case Crypto.Unverified:
             return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?";
-            default:
+        default:
             return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?";
         }
     }
+    property int trust: Crypto.Unverified
+    property color unencryptedColor: Nheko.theme.error
+    property color unencryptedHoverColor: unencryptedColor
+    property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg"
 
-    width: 16
+    ToolTip.text: {
+        if (!encrypted)
+            return qsTr("This message is not encrypted!");
+        switch (trust) {
+        case Crypto.Verified:
+            return qsTr("Encrypted by a verified device");
+        case Crypto.TOFU:
+            return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
+        default:
+            return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
+        }
+    }
+    ToolTip.visible: stateImg.hovered
     height: 16
-    sourceSize.height: height
-    sourceSize.width: width
     source: {
         if (encrypted) {
             switch (trust) {
@@ -51,23 +59,12 @@ Image {
             return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor);
         }
     }
-    ToolTip.visible: stateImg.hovered
-    ToolTip.text: {
-        if (!encrypted)
-            return qsTr("This message is not encrypted!");
-
-        switch (trust) {
-        case Crypto.Verified:
-            return qsTr("Encrypted by a verified device");
-        case Crypto.TOFU:
-            return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
-        default:
-            return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
-        }
-    }
+    sourceSize.height: height
+    sourceSize.width: width
+    width: 16
 
     HoverHandler {
         id: ma
-    }
 
+    }
 }
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index a5787189..cc48c46f 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -16,13 +16,21 @@ Popup {
         mid = mid_in;
     }
 
-    x: Math.round(parent.width / 2 - width / 2)
-    y: Math.round(parent.height / 4)
+    leftPadding: 10
     modal: true
     parent: Overlay.overlay
-    width: timelineRoot.width * 0.8
-    leftPadding: 10
     rightPadding: 10
+    width: timelineRoot.width * 0.8
+    x: Math.round(parent.width / 2 - width / 2)
+    y: Math.round(parent.height / 4)
+
+    Overlay.modal: Rectangle {
+        color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7)
+    }
+    background: Rectangle {
+        color: palette.window
+    }
+
     onOpened: {
         roomTextInput.forceActiveFocus();
     }
@@ -35,46 +43,40 @@ Popup {
         Label {
             id: titleLabel
 
-            text: qsTr("Forward Message")
-            font.bold: true
             bottomPadding: 10
             color: palette.text
+            font.bold: true
+            text: qsTr("Forward Message")
         }
-
         Reply {
             id: replyPreview
 
-            property var modelData: room ? room.getDump(mid, "") : {
-            }
+            property var modelData: room ? room.getDump(mid, "") : {}
 
-            width: parent.width
-
-            userColor: TimelineManager.userColor(modelData.userId, palette.window)
             blurhash: modelData.blurhash ?? ""
             body: modelData.body ?? ""
-            formattedBody: modelData.formattedBody ?? ""
+            encryptionError: modelData.encryptionError ?? ""
             eventId: modelData.eventId ?? ""
             filename: modelData.filename ?? ""
             filesize: modelData.filesize ?? ""
+            formattedBody: modelData.formattedBody ?? ""
+            isOnlyEmoji: modelData.isOnlyEmoji ?? false
+            originalWidth: modelData.originalWidth ?? 0
             proportionalHeight: modelData.proportionalHeight ?? 1
             type: modelData.type ?? MtxEvent.UnknownMessage
             typeString: modelData.typeString ?? ""
             url: modelData.url ?? ""
-            originalWidth: modelData.originalWidth ?? 0
-            isOnlyEmoji: modelData.isOnlyEmoji ?? false
+            userColor: TimelineManager.userColor(modelData.userId, palette.window)
             userId: modelData.userId ?? ""
             userName: modelData.userName ?? ""
-            encryptionError: modelData.encryptionError ?? ""
+            width: parent.width
         }
-
         MatrixTextField {
             id: roomTextInput
 
-            width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
             color: palette.text
-            onTextEdited: {
-                completerPopup.completer.searchString = text;
-            }
+            width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
+
             Keys.onPressed: {
                 if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) {
                     event.accepted = true;
@@ -90,43 +92,32 @@ Popup {
                     event.accepted = true;
                 }
             }
+            onTextEdited: {
+                completerPopup.completer.searchString = text;
+            }
         }
-
         Completer {
             id: completerPopup
 
-            width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
-            completerName: "room"
-            fullWidth: true
-            centerRowContent: false
             avatarHeight: 24
             avatarWidth: 24
             bottomToTop: false
+            centerRowContent: false
+            completerName: "room"
+            fullWidth: true
+            width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
         }
-
     }
-
     Connections {
         function onCompletionSelected(id) {
             room.forwardMessage(messageContextMenu.eventId, id);
             forwardMessagePopup.close();
         }
-
         function onCountChanged() {
             if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
                 completerPopup.currentIndex = 0;
-
         }
 
         target: completerPopup
     }
-
-    background: Rectangle {
-        color: palette.window
-    }
-
-    Overlay.modal: Rectangle {
-        color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7)
-    }
-
 }
diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index ecb402c7..4115cd0a 100644
--- a/resources/qml/ImageButton.qml
+++ b/resources/qml/ImageButton.qml
@@ -10,38 +10,35 @@ import im.nheko 1.0 // for cursor shape
 AbstractButton {
     id: button
 
-    property alias cursor: mouseArea.cursorShape
-    property string image: undefined
-    property color highlightColor: palette.highlight
     property color buttonTextColor: palette.buttonText
     property bool changeColorOnHover: true
+    property alias cursor: mouseArea.cursorShape
+    property color highlightColor: palette.highlight
+    property string image: undefined
     property bool ripple: true
 
     focusPolicy: Qt.NoFocus
-    width: 16
     height: 16
+    width: 16
 
     Image {
         id: buttonImg
 
         // Workaround, can't get icon.source working for now...
         anchors.fill: parent
+        fillMode: Image.PreserveAspectFit
         source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
         sourceSize.height: button.height
         sourceSize.width: button.width
-        fillMode: Image.PreserveAspectFit
     }
-
     CursorShape {
         id: mouseArea
 
         anchors.fill: parent
         cursorShape: Qt.PointingHandCursor
     }
-
     Ripple {
-        enabled: button.ripple
         color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
+        enabled: button.ripple
     }
-
 }
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 96303a2b..057a632f 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -11,22 +11,23 @@ TextEdit {
 
     property alias cursorShape: cs.cursorShape
 
-    textFormat: TextEdit.RichText
-    readOnly: true
-    focus: false
-    wrapMode: Text.Wrap
-    selectByMouse: !Settings.mobileMode
+    ToolTip.text: hoveredLink
+    ToolTip.visible: hoveredLink || false
     // this always has to be enabled, otherwise you can't click links anymore!
     //enabled: selectByMouse
     color: palette.text
-    onLinkActivated: Nheko.openLink(link)
-    ToolTip.visible: hoveredLink || false
-    ToolTip.text: hoveredLink
+    focus: false
+    readOnly: true
+    selectByMouse: !Settings.mobileMode
+    textFormat: TextEdit.RichText
+    wrapMode: Text.Wrap
+
     // Setting a tooltip delay makes the hover text empty .-.
     //ToolTip.delay: Nheko.tooltipDelay
     Component.onCompleted: {
         TimelineManager.fixImageRendering(r.textDocument, r);
     }
+    onLinkActivated: Nheko.openLink(link)
 
     CursorShape {
         id: cs
@@ -34,5 +35,4 @@ TextEdit {
         anchors.fill: parent
         cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
     }
-
 }
diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index f1ff2836..7209a5aa 100644
--- a/resources/qml/MatrixTextField.qml
+++ b/resources/qml/MatrixTextField.qml
@@ -7,67 +7,63 @@ import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
 import im.nheko 1.0
 
-
 ColumnLayout {
     id: c
+
     property color backgroundColor: palette.base
     property alias color: labelC.color
-    property alias textPadding: input.padding
-    property alias text: input.text
+    property alias echoMode: input.echoMode
+    property alias font: input.font
+    property var hasClear: false
     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
-    property var hasClear: false
-
-    Timer {
-        id: timer
-        interval: 350
-        onTriggered: editingFinished()
-    }
-
-    onTextChanged: timer.restart()
+    property alias text: input.text
+    property alias textPadding: input.padding
 
-    signal textEdited
     signal accepted
     signal editingFinished
-
-    function forceActiveFocus() {
-        input.forceActiveFocus();
-    }
+    signal textEdited
 
     function clear() {
         input.clear();
     }
+    function forceActiveFocus() {
+        input.forceActiveFocus();
+    }
 
     ToolTip.delay: Nheko.tooltipDelay
     ToolTip.visible: hover.hovered
-
     spacing: 0
 
+    onTextChanged: timer.restart()
+
+    Timer {
+        id: timer
+
+        interval: 350
+
+        onTriggered: editingFinished()
+    }
     Item {
+        Layout.bottomMargin: Nheko.paddingSmall
         Layout.fillWidth: true
-        Layout.preferredHeight: labelC.contentHeight
         Layout.margins: input.padding
-        Layout.bottomMargin: Nheko.paddingSmall
+        Layout.preferredHeight: labelC.contentHeight
         visible: labelC.text
-
         z: 1
 
         Label {
             id: labelC
 
-            y: contentHeight + input.padding + Nheko.paddingSmall
-            enabled: false
-
             color: palette.text
+            enabled: false
+            font.letterSpacing: input.font.pixelSize * 0.02
             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" : ""
+            width: parent.width
+            y: contentHeight + input.padding + Nheko.paddingSmall
 
             states: State {
                 name: "focused"
@@ -76,50 +72,40 @@ ColumnLayout {
                     target: labelC
                     y: 0
                 }
-
                 PropertyChanges {
-                    target: input
                     opacity: 1
+                    target: input
                 }
-
             }
-
             transitions: Transition {
                 from: ""
-                to: "focused"
                 reversible: true
+                to: "focused"
 
                 NumberAnimation {
-                    target: labelC
-                    properties: "y"
+                    alwaysRunToEnd: true
                     duration: 210
                     easing.type: Easing.InCubic
-                    alwaysRunToEnd: true
+                    properties: "y"
+                    target: labelC
                 }
-
                 NumberAnimation {
-                    target: input
-                    properties: "opacity"
+                    alwaysRunToEnd: true
                     duration: 210
                     easing.type: Easing.InCubic
-                    alwaysRunToEnd: true
+                    properties: "opacity"
+                    target: input
                 }
-
             }
         }
     }
-
     TextField {
         id: input
-        Layout.fillWidth: true
 
+        Layout.fillWidth: true
         color: labelC.color
-        opacity: labelC.text ? 0 : 1
         focus: true
-
-        onTextEdited: c.textEdited()
-        onAccepted: c.accepted()
-        onEditingFinished: c.editingFinished()
+        opacity: labelC.text ? 0 : 1
 
         background: Rectangle {
             id: backgroundRect
@@ -127,44 +113,46 @@ ColumnLayout {
             color: labelC.text ? "transparent" : backgroundColor
         }
 
+        onAccepted: c.accepted()
+        onEditingFinished: c.editingFinished()
+        onTextEdited: c.textEdited()
+
         ImageButton {
             id: clearText
 
+            focusPolicy: Qt.NoFocus
+            hoverEnabled: true
+            image: ":/icons/icons/ui/round-remove-button.svg"
             visible: c.hasClear && searchField.text !== ''
 
-            image: ":/icons/icons/ui/round-remove-button.svg"
-            focusPolicy: Qt.NoFocus
             onClicked: {
-                searchField.clear()
+                searchField.clear();
                 topBar.searchString = "";
             }
-            hoverEnabled: true
+
             anchors {
-                top: parent.top
                 bottom: parent.bottom
                 right: parent.right
                 rightMargin: Nheko.paddingSmall
+                top: parent.top
             }
         }
-
     }
-
     Rectangle {
         id: blueBar
 
         Layout.fillWidth: true
-
         color: palette.highlight
         height: 1
 
         Rectangle {
             id: blackBar
 
-            anchors.top: parent.top
             anchors.horizontalCenter: parent.horizontalCenter
-            height: parent.height*2
-            width: 0
+            anchors.top: parent.top
             color: palette.text
+            height: parent.height * 2
+            width: 0
 
             states: State {
                 name: "focused"
@@ -174,31 +162,25 @@ ColumnLayout {
                     target: blackBar
                     width: blueBar.width
                 }
-
             }
-
             transitions: Transition {
                 from: ""
-                to: "focused"
                 reversible: true
-
+                to: "focused"
 
                 NumberAnimation {
-                    target: blackBar
-                    properties: "width"
+                    alwaysRunToEnd: true
                     duration: 310
                     easing.type: Easing.InCubic
-                    alwaysRunToEnd: true
+                    properties: "width"
+                    target: blackBar
                 }
-
             }
-
         }
-
     }
-
     HoverHandler {
         id: hover
+
         enabled: c.ToolTip.text
     }
 }
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 6220249b..e196b06d 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -14,60 +14,54 @@ import im.nheko 1.0
 Rectangle {
     id: inputBar
 
+    property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
     readonly property string text: messageInput.text
 
-    color: palette.window
     Layout.fillWidth: true
-    Layout.preferredHeight: row.implicitHeight
     Layout.minimumHeight: 40
-    property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
-
+    Layout.preferredHeight: row.implicitHeight
+    color: palette.window
 
     Component {
         id: placeCallDialog
 
         PlaceCall {
         }
-
     }
-
     Component {
         id: screenShareDialog
 
         ScreenShare {
         }
-
     }
-
     RowLayout {
         id: row
 
-        visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
         anchors.fill: parent
         spacing: 0
+        visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
 
         ImageButton {
-            visible: CallManager.callsSupported && showAllButtons
-            opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
             Layout.alignment: Qt.AlignBottom
-            hoverEnabled: true
-            width: 22
+            Layout.margins: 8
+            ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
+            ToolTip.visible: hovered
             height: 22
+            hoverEnabled: true
             image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
-            ToolTip.visible: hovered
-            ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
-            Layout.margins: 8
+            opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
+            visible: CallManager.callsSupported && showAllButtons
+            width: 22
+
             onClicked: {
                 if (room) {
                     if (CallManager.haveCallInvite) {
-                        return ;
+                        return;
                     } else if (CallManager.isOnCall) {
                         CallManager.hangUp();
-                    } 
-                    else if(CallManager.isOnCallOnOtherDevice) {
+                    } else if (CallManager.isOnCallOnOtherDevice) {
                         return;
-                    }
-                    else {
+                    } else {
                         var dialog = placeCallDialog.createObject(timelineRoot);
                         dialog.open();
                         timelineRoot.destroyOnClose(dialog);
@@ -75,18 +69,18 @@ Rectangle {
                 }
             }
         }
-
         ImageButton {
-            visible: showAllButtons
             Layout.alignment: Qt.AlignBottom
-            hoverEnabled: true
-            width: 22
+            Layout.margins: 8
+            ToolTip.text: qsTr("Send a file")
+            ToolTip.visible: hovered
             height: 22
+            hoverEnabled: true
             image: ":/icons/icons/ui/attach.svg"
-            Layout.margins: 8
+            visible: showAllButtons
+            width: 22
+
             onClicked: room.input.openFileSelection()
-            ToolTip.visible: hovered
-            ToolTip.text: qsTr("Send a file")
 
             Rectangle {
                 anchors.fill: parent
@@ -98,112 +92,67 @@ Rectangle {
                     height: parent.height / 2
                     running: parent.visible
                 }
-
             }
-
         }
-
         ScrollView {
             id: textInput
 
             Layout.alignment: Qt.AlignVCenter
+            Layout.fillWidth: true
             Layout.maximumHeight: Window.height / 4
             Layout.minimumHeight: fontMetrics.lineSpacing
             Layout.preferredHeight: contentHeight
-            Layout.fillWidth: true
-
             ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
-
             contentWidth: availableWidth
 
             TextArea {
                 id: messageInput
 
                 property int completerTriggeredAt: 0
+                property string lastChar
 
                 function insertCompletion(completion) {
                     messageInput.remove(completerTriggeredAt, cursorPosition);
                     messageInput.insert(cursorPosition, completion);
                 }
-
                 function openCompleter(pos, type) {
-                    if (popup.opened) return;
+                    if (popup.opened)
+                        return;
                     completerTriggeredAt = pos;
                     completer.completerName = type;
                     popup.open();
-                    completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
+                    completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
                 }
-
                 function positionCursorAtEnd() {
                     cursorPosition = messageInput.length;
                 }
-
                 function positionCursorAtStart() {
                     cursorPosition = 0;
                 }
 
-                selectByMouse: true
+                background: null
+                bottomPadding: 8
+                color: palette.text
+                focus: true
+                leftPadding: inputBar.showAllButtons ? 0 : 8
+                padding: 0
                 placeholderText: qsTr("Write a message...")
                 placeholderTextColor: palette.buttonText
-                color: palette.text
-                width: textInput.width 
+                selectByMouse: true
+                topPadding: 8
                 verticalAlignment: TextEdit.AlignVCenter
+                width: textInput.width
                 wrapMode: TextEdit.Wrap
-                padding: 0
-                topPadding: 8
-                bottomPadding: 8
-                leftPadding: inputBar.showAllButtons? 0 : 8
-                focus: true
-                property string lastChar
-                onTextChanged: {
-                    if (room)
-                        room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
-                    forceActiveFocus();
-                    if (cursorPosition > 0)
-                        lastChar = text.charAt(cursorPosition-1)
-                    else
-                        lastChar = ''
-                    if (lastChar == '@') {
-                        messageInput.openCompleter(selectionStart-1, "user");
-                    } else if (lastChar == ':') {
-                        messageInput.openCompleter(selectionStart-1, "emoji");
-                    } else if (lastChar == '#') {
-                        messageInput.openCompleter(selectionStart-1, "roomAliases");
-                    } else if (lastChar == "/" && cursorPosition == 1) {
-                        messageInput.openCompleter(selectionStart-1, "command");
-                    }
-                }
-                onCursorPositionChanged: {
-                    if (!room)
-                        return ;
-
-                    room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
-                    if (popup.opened && cursorPosition <= completerTriggeredAt)
-                        popup.close();
 
-                    if (popup.opened)
-                        completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
-
-                }
-                onPreeditTextChanged: {
-                    if (popup.opened)
-                        completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
-                }
-                onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
-                onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
-                // Ensure that we get escape key press events first.
-                Keys.onShortcutOverride: (event) => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space))
-                Keys.onPressed: (event) => {
+                Keys.onPressed: event => {
                     if (event.matches(StandardKey.Paste)) {
                         event.accepted = room.input.tryPasteAttachment(false);
                     } else if (event.key == Qt.Key_Space) {
                         // close popup if user enters space after colon
                         if (cursorPosition == completerTriggeredAt + 1)
                             popup.close();
-
                         if (popup.opened && completer.count <= 0)
                             popup.close();
-
                     } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
                         messageInput.clear();
                     } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
@@ -218,8 +167,8 @@ Rectangle {
                         completer.completerName = "";
                         popup.close();
                     } else if (event.matches(StandardKey.InsertLineSeparator)) {
-                        if (popup.opened) popup.close();
-
+                        if (popup.opened)
+                            popup.close();
                         if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
                             room.input.send();
                             event.accepted = true;
@@ -253,16 +202,16 @@ Rectangle {
                                 console.log('"' + t + '"');
                                 if (t == '@') {
                                     messageInput.openCompleter(pos, "user");
-                                    return ;
+                                    return;
                                 } else if (t == ' ' || t == '\t') {
                                     messageInput.openCompleter(pos + 1, "user");
-                                    return ;
+                                    return;
                                 } else if (t == ':') {
                                     messageInput.openCompleter(pos, "emoji");
-                                    return ;
+                                    return;
                                 } else if (t == '~') {
                                     messageInput.openCompleter(pos, "customEmoji");
-                                    return ;
+                                    return;
                                 }
                                 pos = pos - 1;
                             }
@@ -312,21 +261,53 @@ Rectangle {
                         }
                     }
                 }
-                background: null
+                // Ensure that we get escape key press events first.
+                Keys.onShortcutOverride: event => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space))
+                onCursorPositionChanged: {
+                    if (!room)
+                        return;
+                    room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
+                    if (popup.opened && cursorPosition <= completerTriggeredAt)
+                        popup.close();
+                    if (popup.opened)
+                        completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
+                }
+                onPreeditTextChanged: {
+                    if (popup.opened)
+                        completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
+                }
+                onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
+                onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
+                onTextChanged: {
+                    if (room)
+                        room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
+                    forceActiveFocus();
+                    if (cursorPosition > 0)
+                        lastChar = text.charAt(cursorPosition - 1);
+                    else
+                        lastChar = '';
+                    if (lastChar == '@') {
+                        messageInput.openCompleter(selectionStart - 1, "user");
+                    } else if (lastChar == ':') {
+                        messageInput.openCompleter(selectionStart - 1, "emoji");
+                    } else if (lastChar == '#') {
+                        messageInput.openCompleter(selectionStart - 1, "roomAliases");
+                    } else if (lastChar == "/" && cursorPosition == 1) {
+                        messageInput.openCompleter(selectionStart - 1, "command");
+                    }
+                }
 
                 Connections {
                     function onRoomChanged() {
                         messageInput.clear();
                         if (room)
                             messageInput.append(room.input.text);
-
                         completer.completerName = "";
                         messageInput.forceActiveFocus();
                     }
 
                     target: timelineView
                 }
-
                 Connections {
                     function onCompletionClicked(completion) {
                         messageInput.insertCompletion(completion);
@@ -334,43 +315,39 @@ Rectangle {
 
                     target: completer
                 }
-
                 Popup {
                     id: popup
 
-                    x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
-                    y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
-
                     background: null
                     padding: 0
-
-                    Completer {
-                        anchors.fill: parent
-                        id: completer
-                        rowMargin: 2
-                        rowSpacing: 0
-                    }
+                    x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
+                    y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
 
                     enter: Transition {
                         NumberAnimation {
-                            property: "opacity"
+                            duration: 100
                             from: 0
+                            property: "opacity"
                             to: 1
-                            duration: 100
                         }
-
                     }
-
                     exit: Transition {
                         NumberAnimation {
-                            property: "opacity"
+                            duration: 100
                             from: 1
+                            property: "opacity"
                             to: 0
-                            duration: 100
                         }
                     }
-                }
 
+                    Completer {
+                        id: completer
+
+                        anchors.fill: parent
+                        rowMargin: 2
+                        rowSpacing: 0
+                    }
+                }
                 Connections {
                     function onTextChanged(newText) {
                         messageInput.text = newText;
@@ -380,16 +357,13 @@ Rectangle {
                     ignoreUnknownSignals: true
                     target: room ? room.input : null
                 }
-
                 Connections {
-                    function onReplyChanged() {
+                    function onEditChanged() {
                         messageInput.forceActiveFocus();
                     }
-
-                    function onEditChanged() {
+                    function onReplyChanged() {
                         messageInput.forceActiveFocus();
                     }
-
                     function onThreadChanged() {
                         messageInput.forceActiveFocus();
                     }
@@ -397,7 +371,6 @@ Rectangle {
                     ignoreUnknownSignals: true
                     target: room
                 }
-
                 Connections {
                     function onFocusInput() {
                         messageInput.forceActiveFocus();
@@ -405,59 +378,56 @@ Rectangle {
 
                     target: TimelineManager
                 }
-
                 MouseArea {
+                    acceptedButtons: Qt.MiddleButton
                     // workaround for wrong cursor shape on some platforms
                     anchors.fill: parent
-                    acceptedButtons: Qt.MiddleButton
                     cursorShape: Qt.IBeamCursor
-                    onPressed: (mouse) => mouse.accepted = room.input.tryPasteAttachment(true)
-                }
 
+                    onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true)
+                }
             }
-
         }
-
         ImageButton {
             id: stickerButton
-            visible: showAllButtons
 
             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
             Layout.margins: 8
-            hoverEnabled: true
-            width: 22
+            ToolTip.text: qsTr("Stickers")
+            ToolTip.visible: hovered
             height: 22
+            hoverEnabled: true
             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) {
-                room.input.sticker(row);
-                TimelineManager.focusMessageInput();
-            })
+            visible: showAllButtons
+            width: 22
+
+            onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) {
+                    room.input.sticker(row);
+                    TimelineManager.focusMessageInput();
+                })
 
             StickerPicker {
                 id: stickerPopup
 
                 emoji: false
             }
-
         }
-
         ImageButton {
             id: emojiButton
 
             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
             Layout.margins: 8
-            hoverEnabled: true
-            width: 22
+            ToolTip.text: qsTr("Emoji")
+            ToolTip.visible: hovered
             height: 22
+            hoverEnabled: true
             image: ":/icons/icons/ui/smile.svg"
-            ToolTip.visible: hovered
-            ToolTip.text: qsTr("Emoji")
-            onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function(plaintext, markdown) {
-                messageInput.insert(messageInput.cursorPosition, markdown);
-                TimelineManager.focusMessageInput();
-            })
+            width: 22
+
+            onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) {
+                    messageInput.insert(messageInput.cursorPosition, markdown);
+                    TimelineManager.focusMessageInput();
+                })
 
             StickerPicker {
                 id: emojiPopup
@@ -465,28 +435,25 @@ Rectangle {
                 emoji: true
             }
         }
-
         ImageButton {
             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
             Layout.margins: 8
-            hoverEnabled: true
-            width: 22
-            height: 22
-            image: ":/icons/icons/ui/send.svg"
             Layout.rightMargin: 8
-            ToolTip.visible: hovered
             ToolTip.text: qsTr("Send")
+            ToolTip.visible: hovered
+            height: 22
+            hoverEnabled: true
+            image: ":/icons/icons/ui/send.svg"
+            width: 22
+
             onClicked: {
                 room.input.send();
             }
         }
-
     }
-
     Text {
         anchors.centerIn: parent
-        visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
         text: qsTr("You don't have permission to send messages in this room")
+        visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
     }
-
 }
diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml
index be73df2a..4d5578b3 100644
--- a/resources/qml/MessageInputWarning.qml
+++ b/resources/qml/MessageInputWarning.qml
@@ -10,37 +10,35 @@ import im.nheko 1.0
 Rectangle {
     id: warningRoot
 
-    required property string text
     property color bubbleColor: Nheko.theme.error
+    required property string text
 
-    implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
-    height: implicitHeight
     Layout.fillWidth: true
     color: palette.window // required to hide the timeline behind this warning
+    height: implicitHeight
+    implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
 
     Rectangle {
         id: warningRect
 
-        visible: warningRoot.visible
+        anchors.fill: parent
+        anchors.margins: visible ? Nheko.paddingSmall : 0
+        border.color: bubbleColor
+        border.width: 1
         // TODO: Qt.alpha() would make more sense but it wasn't working...
         color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3)
-        border.width: 1
-        border.color: bubbleColor
         radius: 3
-        anchors.fill: parent
-        anchors.margins: visible ? Nheko.paddingSmall : 0
+        visible: warningRoot.visible
         z: 3
 
         Label {
             id: warningDisplay
 
             anchors.left: parent.left
-            anchors.verticalCenter: parent.verticalCenter
             anchors.margins: Nheko.paddingSmall
+            anchors.verticalCenter: parent.verticalCenter
             text: warningRoot.text
             textFormat: Text.PlainText
         }
-
     }
-
 }
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 158bc236..57bfe216 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -14,89 +14,269 @@ import QtQuick.Layouts 1.2
 import QtQuick.Window 2.13
 import im.nheko 1.0
 
-
 Item {
     id: chatRoot
-    property int padding: Nheko.paddingMedium
 
     property int availableWidth: width
-
+    property int padding: Nheko.paddingMedium
     property string searchString: ""
 
     // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
     Connections {
         function onHideMenu() {
-            messageContextMenu.close()
-            replyContextMenu.close()
+            messageContextMenu.close();
+            replyContextMenu.close();
         }
+
         target: MainWindow
     }
-
     ScrollBar {
         id: scrollbar
-        parent: chat.parent
-        anchors.top: parent.top
-        anchors.right: parent.right
+
         anchors.bottom: parent.bottom
+        anchors.right: parent.right
+        anchors.top: parent.top
+        parent: chat.parent
     }
     ListView {
         id: chat
 
-        anchors.fill: parent
-
-        property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive? scrollbar.width : 0)
-
+        property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0)
         readonly property alias filteringInProgress: filteredTimeline.filteringInProgress
 
-        displayMarginBeginning: height / 2
-        displayMarginEnd: height / 2
-
-        TimelineFilter {
-            id: filteredTimeline
-            source: room
-            filterByThread: room ? room.thread : ""
-            filterByContent: chatRoot.searchString
-        }
-
-        model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
+        ScrollBar.vertical: scrollbar
+        anchors.fill: parent
+        anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0
         // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
         //onModelChanged: if (room) room.sendReset()
         //reuseItems: true
         boundsBehavior: Flickable.StopAtBounds
+        displayMarginBeginning: height / 2
+        displayMarginEnd: height / 2
+        model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
         //pixelAligned: true
         spacing: 2
         verticalLayoutDirection: ListView.BottomToTop
+
+        delegate: Item {
+            id: wrapper
+
+            required property string blurhash
+            required property string body
+            required property string callType
+            required property var day
+            required property string duration
+            required property int encryptionError
+            required property string eventId
+            required property string filename
+            required property string filesize
+            required property string formattedBody
+            required property int index
+            required property bool isEditable
+            required property bool isEdited
+            required property bool isEncrypted
+            required property bool isOnlyEmoji
+            required property bool isSender
+            required property bool isStateEvent
+            required property int notificationlevel
+            required property int originalWidth
+            property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day)
+            property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent)
+            property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId)
+            required property double proportionalHeight
+            required property var reactions
+            required property int relatedEventCacheBuster
+            required property string replyTo
+            required property string roomName
+            required property string roomTopic
+            property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+            required property int status
+            required property string threadId
+            required property string thumbnailUrl
+            required property var timestamp
+            required property int trustlevel
+            required property int type
+            required property string typeString
+            required property string url
+            required property string userId
+            required property string userName
+
+            ListView.delayRemove: true
+            anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+            height: section.active ? section.height + timelinerow.height : timelinerow.height
+            width: chat.delegateMaxWidth
+
+            Loader {
+                id: section
+
+                property var day: wrapper.day
+                property bool isSender: wrapper.isSender
+                property bool isStateEvent: wrapper.isStateEvent
+                property int parentWidth: parent.width
+                property var previousMessageDay: wrapper.previousMessageDay
+                property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
+                property string previousMessageUserId: wrapper.previousMessageUserId
+                property date timestamp: wrapper.timestamp
+                property string userId: wrapper.userId
+                property string userName: wrapper.userName
+
+                active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
+                //asynchronous: true
+                sourceComponent: sectionHeader
+                visible: status == Loader.Ready
+                z: 4
+            }
+            TimelineRow {
+                id: timelinerow
+
+                blurhash: wrapper.blurhash
+                body: wrapper.body
+                callType: wrapper.callType
+                duration: wrapper.duration
+                encryptionError: wrapper.encryptionError
+                eventId: chat.model, wrapper.eventId
+                filename: wrapper.filename
+                filesize: wrapper.filesize
+                formattedBody: wrapper.formattedBody
+                index: wrapper.index
+                isEditable: wrapper.isEditable
+                isEdited: wrapper.isEdited
+                isEncrypted: wrapper.isEncrypted
+                isOnlyEmoji: wrapper.isOnlyEmoji
+                isSender: wrapper.isSender
+                isStateEvent: wrapper.isStateEvent
+                notificationlevel: wrapper.notificationlevel
+                originalWidth: wrapper.originalWidth
+                proportionalHeight: wrapper.proportionalHeight
+                reactions: wrapper.reactions
+                relatedEventCacheBuster: wrapper.relatedEventCacheBuster
+                replyTo: wrapper.replyTo
+                roomName: wrapper.roomName
+                roomTopic: wrapper.roomTopic
+                status: wrapper.status
+                threadId: wrapper.threadId
+                thumbnailUrl: wrapper.thumbnailUrl
+                timestamp: wrapper.timestamp
+                trustlevel: wrapper.trustlevel
+                type: chat.model, wrapper.type
+                typeString: wrapper.typeString
+                url: wrapper.url
+                userId: wrapper.userId
+                userName: wrapper.userName
+                y: section.visible && section.active ? section.y + section.height : 0
+
+                background: Rectangle {
+                    id: scrollHighlight
+
+                    color: palette.highlight
+                    enabled: false
+                    opacity: 0
+                    visible: true
+                    z: 1
+
+                    states: State {
+                        name: "revealed"
+                        when: wrapper.scrolledToThis
+                    }
+                    transitions: Transition {
+                        from: ""
+                        to: "revealed"
+
+                        SequentialAnimation {
+                            PropertyAnimation {
+                                duration: 500
+                                easing.type: Easing.InOutQuad
+                                from: 0
+                                properties: "opacity"
+                                target: scrollHighlight
+                                to: 1
+                            }
+                            PropertyAnimation {
+                                duration: 500
+                                easing.type: Easing.InOutQuad
+                                from: 1
+                                properties: "opacity"
+                                target: scrollHighlight
+                                to: 0
+                            }
+                            ScriptAction {
+                                script: room.eventShown()
+                            }
+                        }
+                    }
+                }
+
+                onHoveredChanged: {
+                    if (!Settings.mobileMode && hovered) {
+                        if (!messageActions.hovered) {
+                            messageActions.attached = timelinerow;
+                            messageActions.model = timelinerow;
+                        }
+                    }
+                }
+            }
+            Connections {
+                function onMovementEnded() {
+                    if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
+                        chat.model.currentIndex = index;
+                }
+
+                target: chat
+            }
+        }
+        footer: Item {
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.margins: Nheko.paddingLarge
+            // hacky, but works
+            height: loadingSpinner.height + 2 * Nheko.paddingLarge
+            visible: (room && room.paginationInProgress) || chat.filteringInProgress
+
+            Spinner {
+                id: loadingSpinner
+
+                anchors.centerIn: parent
+                anchors.margins: Nheko.paddingLarge
+                foreground: palette.mid
+                running: (room && room.paginationInProgress) || chat.filteringInProgress
+                z: 3
+            }
+        }
+
+        Window.onActiveChanged: readTimer.running = Window.active
         onCountChanged: {
             // Mark timeline as read
-            if (atYEnd && room) model.currentIndex = 0;
+            if (atYEnd && room)
+                model.currentIndex = 0;
         }
 
-        ScrollBar.vertical: scrollbar
-
-        anchors.rightMargin: scrollbar.interactive? scrollbar.width : 0
+        TimelineFilter {
+            id: filteredTimeline
 
+            filterByContent: chatRoot.searchString
+            filterByThread: room ? room.thread : ""
+            source: room
+        }
         Control {
             id: messageActions
 
             property Item attached: null
-            property alias model: row.model
             // use comma to update on scroll
             property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
-            padding: Nheko.paddingSmall
+            property alias model: row.model
 
             hoverEnabled: true
+            padding: Nheko.paddingSmall
             visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
             x: attached ? attachedPos.x : 0
             y: attached ? attachedPos.y + Nheko.paddingSmall : 0
             z: 10
 
             background: Rectangle {
-                color: palette.window
                 border.color: palette.buttonText
                 border.width: 1
+                color: palette.window
                 radius: padding
             }
-
             contentItem: RowLayout {
                 id: row
 
@@ -111,174 +291,166 @@ Item {
                     delegate: AbstractButton {
                         id: button
 
-                        required property string modelData
-
-                        property color highlightColor: palette.highlight
                         property color buttonTextColor: palette.buttonText
+                        property color highlightColor: palette.highlight
+                        required property string modelData
                         property bool showImage: modelData.startsWith("mxc://")
 
                         //Layout.preferredHeight: fontMetrics.height
                         Layout.alignment: Qt.AlignBottom
-
                         focusPolicy: Qt.NoFocus
-                        width: showImage ? 16 : buttonText.implicitWidth
                         height: showImage ? 16 : buttonText.implicitHeight
-                        implicitWidth: showImage ? 16 : buttonText.implicitWidth
                         implicitHeight: showImage ? 16 : buttonText.implicitHeight
+                        implicitWidth: showImage ? 16 : buttonText.implicitWidth
+                        width: showImage ? 16 : buttonText.implicitWidth
+
+                        onClicked: {
+                            room.input.reaction(row.model.eventId, modelData);
+                            TimelineManager.focusMessageInput();
+                        }
 
                         Label {
                             id: buttonText
 
-                            visible: !button.showImage
-
                             anchors.centerIn: parent
-                            padding: 0
-                            text: button.modelData
                             color: button.hovered ? button.highlightColor : button.buttonTextColor
                             font.family: Settings.emojiFont
-                            verticalAlignment: Text.AlignVCenter
                             horizontalAlignment: Text.AlignHCenter
+                            padding: 0
+                            text: button.modelData
+                            verticalAlignment: Text.AlignVCenter
+                            visible: !button.showImage
                         }
-
                         Image {
                             id: buttonImg
 
                             // Workaround, can't get icon.source working for now...
                             anchors.fill: parent
+                            fillMode: Image.PreserveAspectFit
                             source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
                             sourceSize.height: button.height
                             sourceSize.width: button.width
-                            fillMode: Image.PreserveAspectFit
                         }
-
                         CursorShape {
                             anchors.fill: parent
                             cursorShape: Qt.PointingHandCursor
                         }
-
                         Ripple {
                             color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
                         }
-
-                        onClicked: {
-                            room.input.reaction(row.model.eventId, modelData);
-                            TimelineManager.focusMessageInput();
-                        }
                     }
-
                 }
-
                 ImageButton {
-                    visible: !!row.model && row.model.isEditable
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Edit")
+                    ToolTip.visible: hovered
                     buttonTextColor: palette.buttonText
-                    width: 16
                     hoverEnabled: true
                     image: ":/icons/icons/ui/edit.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Edit")
+                    visible: !!row.model && row.model.isEditable
+                    width: 16
+
                     onClicked: {
-                        if (row.model.isEditable) room.edit = row.model.eventId;
+                        if (row.model.isEditable)
+                            room.edit = row.model.eventId;
                     }
                 }
-
                 ImageButton {
                     id: reactButton
 
-                    visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
-                    width: 16
-                    hoverEnabled: true
-                    image: ":/icons/icons/ui/smile-add.svg"
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: qsTr("React")
-                    onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function(plaintext, markdown) {
-                        var event_id = row.model ? row.model.eventId : "";
-                        room.input.reaction(event_id, plaintext);
-                        TimelineManager.focusMessageInput();
-                    })
-                }
+                    ToolTip.visible: hovered
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/smile-add.svg"
+                    visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
+                    width: 16
 
+                    onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) {
+                            var event_id = row.model ? row.model.eventId : "";
+                            room.input.reaction(event_id, plaintext);
+                            TimelineManager.focusMessageInput();
+                        })
+                }
                 ImageButton {
-                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
-                    width: 16
-                    hoverEnabled: true
-                    image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
+                    ToolTip.visible: hovered
+                    hoverEnabled: true
+                    image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
+                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+                    width: 16
+
                     onClicked: room.thread = (row.model.threadId || row.model.eventId)
                 }
-
                 ImageButton {
-                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
-                    width: 16
-                    hoverEnabled: true
-                    image: ":/icons/icons/ui/reply.svg"
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: qsTr("Reply")
+                    ToolTip.visible: hovered
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/reply.svg"
+                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+                    width: 16
+
                     onClicked: room.reply = row.model.eventId
                 }
-
                 ImageButton {
-                    visible: !!row.model && filteredTimeline.filterByContent
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Go to message")
+                    ToolTip.visible: hovered
                     buttonTextColor: palette.buttonText
-                    width: 16
                     hoverEnabled: true
                     image: ":/icons/icons/ui/go-to.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Go to message")
+                    visible: !!row.model && filteredTimeline.filterByContent
+                    width: 16
+
                     onClicked: {
                         topBar.searchString = "";
                         room.showEvent(row.model.eventId);
                     }
                 }
-
                 ImageButton {
                     id: optionsButton
 
-                    width: 16
-                    hoverEnabled: true
-                    image: ":/icons/icons/ui/options.svg"
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: qsTr("Options")
+                    ToolTip.visible: hovered
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/options.svg"
+                    width: 16
+
                     onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
                 }
-
             }
-
         }
-
         Shortcut {
             sequence: StandardKey.MoveToPreviousPage
+
             onActivated: {
                 chat.contentY = chat.contentY - chat.height * 0.9;
                 chat.returnToBounds();
             }
         }
-
         Shortcut {
             sequence: StandardKey.MoveToNextPage
+
             onActivated: {
                 chat.contentY = chat.contentY + chat.height * 0.9;
                 chat.returnToBounds();
             }
         }
-
         Shortcut {
             sequence: StandardKey.Cancel
+
             onActivated: {
-                if(room.input.uploads.length > 0)
+                if (room.input.uploads.length > 0)
                     room.input.declineUploads();
-                else if(room.reply)
+                else if (room.reply)
                     room.reply = undefined;
                 else if (room.edit)
                     room.edit = undefined;
                 else
-                    room.thread = undefined
+                    room.thread = undefined;
                 TimelineManager.focusMessageInput();
             }
         }
@@ -287,19 +459,20 @@ Item {
         // Better solution welcome.
         Shortcut {
             sequence: "Alt+Up"
+
             onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
         }
-
         Shortcut {
             sequence: "Alt+Down"
+
             onActivated: {
                 var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
                 room.reply = idx >= 0 ? room.indexToId(idx) : null;
             }
         }
-
         Shortcut {
             sequence: "Alt+F"
+
             onActivated: {
                 if (room.reply) {
                     var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
@@ -310,355 +483,157 @@ Item {
                 }
             }
         }
-
         Shortcut {
             sequence: "Ctrl+E"
+
             onActivated: {
                 room.edit = room.reply;
             }
         }
-
-        Window.onActiveChanged: readTimer.running = Window.active
-
         Timer {
             id: readTimer
 
+            interval: 1000
+
             // force current read index to update
             onTriggered: {
                 if (room)
-                room.setCurrentIndex(room.currentIndex);
-
+                    room.setCurrentIndex(room.currentIndex);
             }
-            interval: 1000
         }
-
         Component {
             id: sectionHeader
 
             Column {
-                topPadding: userName_.visible? 4: 0
-                bottomPadding: Settings.bubbles? (isSender && previousMessageDay == day? 0 : 2) : 3
+                bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
+                height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent ? 0 : userName.height + 8)
                 spacing: 8
+                topPadding: userName_.visible ? 4 : 0
                 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
-                height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent? 0 : userName.height +8 )
 
                 Label {
                     id: dateBubble
 
                     anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                    visible: room && previousMessageDay !== day
-                    text: room ? room.formatDateSeparator(timestamp) : ""
                     color: palette.text
                     height: Math.round(fontMetrics.height * 1.4)
-                    width: contentWidth * 1.2
                     horizontalAlignment: Text.AlignHCenter
+                    text: room ? room.formatDateSeparator(timestamp) : ""
                     verticalAlignment: Text.AlignVCenter
+                    visible: room && previousMessageDay !== day
+                    width: contentWidth * 1.2
 
                     background: Rectangle {
-                        radius: parent.height / 2
                         color: palette.window
+                        radius: parent.height / 2
                     }
-
                 }
-
                 Row {
+                    id: userInfo
+
+                    property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
+
                     height: userName_.height
                     spacing: 8
                     visible: !isStateEvent && (!isSender || !Settings.bubbles)
-                    id: userInfo
 
                     Avatar {
                         id: messageUserAvatar
 
-                        width: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1)
-                        height: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1)
-                        url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
+                        ToolTip.delay: Nheko.tooltipDelay
+                        ToolTip.text: userid
+                        ToolTip.visible: messageUserAvatar.hovered
                         displayName: userName
+                        height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
+                        url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
                         userid: userId
+                        width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
+
                         onClicked: room.openUserProfile(userId)
-                        ToolTip.visible: messageUserAvatar.hovered
-                        ToolTip.delay: Nheko.tooltipDelay
-                        ToolTip.text: userid
                     }
-
                     Connections {
                         function onRoomAvatarUrlChanged() {
                             messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
                         }
-
                         function onScrollToIndex(index) {
                             chat.positionViewAtIndex(index, ListView.Center);
                         }
 
                         target: room
                     }
-                    property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
                     AbstractButton {
                         id: userNameButton
+
+                        ToolTip.delay: Nheko.tooltipDelay
+                        ToolTip.text: userId
+                        ToolTip.visible: hovered
+                        leftInset: 0
+                        leftPadding: 0
+                        rightInset: 0
+                        rightPadding: 0
+
                         contentItem: ElidedLabel {
                             id: userName_
-                            fullText: userName
+
                             color: TimelineManager.userColor(userId, palette.base)
+                            elideWidth: Math.min(userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3), userName_.fullTextWidth)
+                            fullText: userName
                             textFormat: Text.RichText
-                            elideWidth: Math.min(userInfo.remainingWidth-Math.min(statusMsg.implicitWidth,userInfo.remainingWidth/3), userName_.fullTextWidth)
                         }
-                        ToolTip.visible: hovered
-                        ToolTip.delay: Nheko.tooltipDelay
-                        ToolTip.text: userId
+
                         onClicked: room.openUserProfile(userId)
-                        leftInset: 0
-                        rightInset: 0
-                        leftPadding: 0
-                        rightPadding: 0
 
                         CursorShape {
                             anchors.fill: parent
                             cursorShape: Qt.PointingHandCursor
                         }
-
                     }
-
                     Label {
                         id: statusMsg
+
+                        property string userStatus: Presence.userStatus(userId)
+
+                        ToolTip.delay: Nheko.tooltipDelay
+                        ToolTip.text: qsTr("%1's status message").arg(userName)
+                        ToolTip.visible: statusMsgHoverHandler.hovered
                         anchors.baseline: userNameButton.baseline
                         color: palette.buttonText
-                        text: userStatus.replace(/\n/g, " ")
-                        textFormat: Text.PlainText
                         elide: Text.ElideRight
-                        width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing)
                         font.italic: true
                         font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8)
-                        ToolTip.text: qsTr("%1's status message").arg(userName)
-                        ToolTip.visible: statusMsgHoverHandler.hovered
-                        ToolTip.delay: Nheko.tooltipDelay
+                        text: userStatus.replace(/\n/g, " ")
+                        textFormat: Text.PlainText
+                        width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing)
 
                         HoverHandler {
                             id: statusMsgHoverHandler
-                        }
 
-                        property string userStatus: Presence.userStatus(userId)
+                        }
                         Connections {
-                            target: Presence
                             function onPresenceChanged(id) {
-                                if (id == userId) statusMsg.userStatus = Presence.userStatus(userId);
-                            }
-                        }
-                    }
-
-                }
-
-            }
-
-        }
-
-        delegate: Item {
-            id: wrapper
-
-            required property double proportionalHeight
-            required property int type
-            required property string typeString
-            required property int originalWidth
-            required property string blurhash
-            required property string body
-            required property string formattedBody
-            required property string eventId
-            required property string filename
-            required property string filesize
-            required property string url
-            required property string thumbnailUrl
-            required property string duration
-            required property bool isOnlyEmoji
-            required property bool isSender
-            required property bool isEncrypted
-            required property bool isEditable
-            required property bool isEdited
-            required property bool isStateEvent
-            property bool previousMessageIsStateEvent:  (index + 1) >= chat.count ? true : chat.model.dataByIndex(index+1, Room.IsStateEvent)
-            required property string replyTo
-            required property string threadId
-            required property string userId
-            required property string roomTopic
-            required property string roomName
-            required property string callType
-            required property var reactions
-            required property int trustlevel
-            required property int notificationlevel
-            required property int encryptionError
-            required property var timestamp
-            required property int status
-            required property int index
-            required property int relatedEventCacheBuster
-            required property var day
-            property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index+1, Room.UserId)
-            property var previousMessageDay:  (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index+1, Room.Day)
-            required property string userName
-            property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
-
-            anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-            width: chat.delegateMaxWidth
-            height: section.active ? section.height + timelinerow.height : timelinerow.height
-            ListView.delayRemove: true
-
-            Loader {
-                id: section
-
-                property int parentWidth: parent.width
-                property string userId: wrapper.userId
-                property string previousMessageUserId: wrapper.previousMessageUserId
-                property var day: wrapper.day
-                property var previousMessageDay: wrapper.previousMessageDay
-                property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
-                property bool isStateEvent: wrapper.isStateEvent
-                property bool isSender: wrapper.isSender
-                property string userName: wrapper.userName
-                property date timestamp: wrapper.timestamp
-
-                z: 4
-                active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
-                //asynchronous: true
-                sourceComponent: sectionHeader
-                visible: status == Loader.Ready
-            }
-
-            TimelineRow {
-                id: timelinerow
-
-                proportionalHeight: wrapper.proportionalHeight
-                type: chat.model, wrapper.type
-                typeString: wrapper.typeString
-                originalWidth: wrapper.originalWidth
-                blurhash: wrapper.blurhash
-                body: wrapper.body
-                formattedBody: wrapper.formattedBody
-                eventId: chat.model, wrapper.eventId
-                filename: wrapper.filename
-                filesize: wrapper.filesize
-                url: wrapper.url
-                thumbnailUrl: wrapper.thumbnailUrl
-                duration: wrapper.duration
-                isOnlyEmoji: wrapper.isOnlyEmoji
-                isSender: wrapper.isSender
-                isEncrypted: wrapper.isEncrypted
-                isEditable: wrapper.isEditable
-                isEdited: wrapper.isEdited
-                isStateEvent: wrapper.isStateEvent
-                replyTo: wrapper.replyTo
-                threadId: wrapper.threadId
-                userId: wrapper.userId
-                userName: wrapper.userName
-                roomTopic: wrapper.roomTopic
-                roomName: wrapper.roomName
-                callType: wrapper.callType
-                reactions: wrapper.reactions
-                trustlevel: wrapper.trustlevel
-                notificationlevel: wrapper.notificationlevel
-                encryptionError: wrapper.encryptionError
-                timestamp: wrapper.timestamp
-                status: wrapper.status
-                index: wrapper.index
-                relatedEventCacheBuster: wrapper.relatedEventCacheBuster
-                y: section.visible && section.active ? section.y + section.height : 0
-
-                onHoveredChanged: {
-                    if (!Settings.mobileMode && hovered) {
-                        if (!messageActions.hovered) {
-                            messageActions.attached = timelinerow;
-                            messageActions.model = timelinerow;
-                        }
-                    }
-                }
-                background: Rectangle {
-                    id: scrollHighlight
-
-                    opacity: 0
-                    visible: true
-                    z: 1
-                    enabled: false
-                    color: palette.highlight
-
-                    states: State {
-                        name: "revealed"
-                        when: wrapper.scrolledToThis
-                    }
-
-                    transitions: Transition {
-                        from: ""
-                        to: "revealed"
-
-                        SequentialAnimation {
-                            PropertyAnimation {
-                                target: scrollHighlight
-                                properties: "opacity"
-                                easing.type: Easing.InOutQuad
-                                from: 0
-                                to: 1
-                                duration: 500
-                            }
-
-                            PropertyAnimation {
-                                target: scrollHighlight
-                                properties: "opacity"
-                                easing.type: Easing.InOutQuad
-                                from: 1
-                                to: 0
-                                duration: 500
-                            }
-
-                            ScriptAction {
-                                script: room.eventShown()
+                                if (id == userId)
+                                    statusMsg.userStatus = Presence.userStatus(userId);
                             }
 
+                            target: Presence
                         }
-
                     }
-
                 }
             }
-
-            Connections {
-                function onMovementEnded() {
-                    if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
-                    chat.model.currentIndex = index;
-
-                }
-
-                target: chat
-            }
-
-        }
-
-        footer: Item {
-            anchors.horizontalCenter: parent.horizontalCenter
-            anchors.margins: Nheko.paddingLarge
-            visible: (room && room.paginationInProgress) || chat.filteringInProgress
-            // hacky, but works
-            height: loadingSpinner.height + 2 * Nheko.paddingLarge
-
-            Spinner {
-                id: loadingSpinner
-
-                anchors.centerIn: parent
-                anchors.margins: Nheko.paddingLarge
-                running: (room && room.paginationInProgress) || chat.filteringInProgress
-                foreground: palette.mid
-                z: 3
-            }
-
         }
     }
-
     Platform.Menu {
         id: messageContextMenu
 
         property string eventId
-        property string threadId
-        property string link
-        property string text
         property int eventType
-        property bool isEncrypted
         property bool isEditable
+        property bool isEncrypted
         property bool isSender
+        property string link
+        property string text
+        property string threadId
 
         function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
             eventId = eventId_;
@@ -668,104 +643,106 @@ Item {
             isEditable = isEditable_;
             isSender = isSender_;
             if (text_)
-            text = text_;
+                text = text_;
             else
-            text = "";
+                text = "";
             if (link_)
-            link = link_;
+                link = link_;
             else
-            link = "";
+                link = "";
             if (showAt_)
-            open(showAt_);
+                open(showAt_);
             else
-            open();
+                open();
         }
 
         Component {
             id: removeReason
+
             InputDialog {
                 id: removeReasonDialog
 
                 property string eventId
 
-                title: qsTr("Reason for removal")
                 prompt: qsTr("Enter reason for removal or hit enter for no reason:")
-                onAccepted: function(text) {
+                title: qsTr("Reason for removal")
+
+                onAccepted: function (text) {
                     room.redactEvent(eventId, text);
                 }
             }
         }
-
         Platform.MenuItem {
-             visible: filteredTimeline.filterByContent
-             enabled: visible
-             text: qsTr("Go to &message")
-             onTriggered: function() {
+            enabled: visible
+            text: qsTr("Go to &message")
+            visible: filteredTimeline.filterByContent
+
+            onTriggered: function () {
                 topBar.searchString = "";
                 room.showEvent(messageContextMenu.eventId);
             }
-         }
-
+        }
         Platform.MenuItem {
-            visible: messageContextMenu.text
             enabled: visible
             text: qsTr("&Copy")
+            visible: messageContextMenu.text
+
             onTriggered: Clipboard.text = messageContextMenu.text
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.link
             enabled: visible
             text: qsTr("Copy &link location")
+            visible: messageContextMenu.link
+
             onTriggered: Clipboard.text = messageContextMenu.link
         }
-
         Platform.MenuItem {
             id: reactionOption
 
-            visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
             text: qsTr("Re&act")
-            onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function(plaintext, markdown) {
-                room.input.reaction(messageContextMenu.eventId, plaintext);
-                TimelineManager.focusMessageInput();
-            })
-        }
+            visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
 
+            onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
+                    room.input.reaction(messageContextMenu.eventId, plaintext);
+                    TimelineManager.focusMessageInput();
+                })
+        }
         Platform.MenuItem {
-            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
             text: qsTr("Repl&y")
+            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+
             onTriggered: room.reply = (messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
             enabled: visible
             text: qsTr("&Edit")
+            visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
+
             onTriggered: room.edit = (messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
             enabled: visible
             text: qsTr("&Thread")
+            visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
+
             onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
             enabled: visible
             text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
+            visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
+
             onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
             text: qsTr("&Read receipts")
+
             onTriggered: room.showReadReceipts(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
             text: qsTr("&Forward")
+            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
+
             onTriggered: {
                 var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
                 forwardMess.setMessageEventId(messageContextMenu.eventId);
@@ -773,28 +750,27 @@ Item {
                 timelineRoot.destroyOnClose(forwardMess);
             }
         }
-
         Platform.MenuItem {
             text: qsTr("&Mark as read")
         }
-
         Platform.MenuItem {
             text: qsTr("View raw message")
+
             onTriggered: room.viewRawMessage(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
-            visible: messageContextMenu.isEncrypted
             enabled: visible
             text: qsTr("View decrypted raw message")
+            // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
+            visible: messageContextMenu.isEncrypted
+
             onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
             text: qsTr("Remo&ve message")
-            onTriggered: function() {
+            visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
+
+            onTriggered: function () {
                 var dialog = removeReason.createObject(timelineRoot);
                 dialog.eventId = messageContextMenu.eventId;
                 dialog.show();
@@ -802,44 +778,40 @@ Item {
                 timelineRoot.destroyOnClose(dialog);
             }
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
             enabled: visible
             text: qsTr("&Save as")
+            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
+
             onTriggered: room.saveMedia(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
             enabled: visible
             text: qsTr("&Open in external program")
+            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
+
             onTriggered: room.openMedia(messageContextMenu.eventId)
         }
-
         Platform.MenuItem {
-            visible: messageContextMenu.eventId
             enabled: visible
             text: qsTr("Copy link to eve&nt")
+            visible: messageContextMenu.eventId
+
             onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
         }
-
     }
-
     Component {
         id: forwardCompleterComponent
 
         ForwardCompleter {
         }
-
     }
-
     Platform.Menu {
         id: replyContextMenu
 
-        property string text
-        property string link
         property string eventId
+        property string link
+        property string text
 
         function show(text_, link_, eventId_) {
             text = text_;
@@ -849,85 +821,100 @@ Item {
         }
 
         Platform.MenuItem {
-            visible: replyContextMenu.text
             enabled: visible
             text: qsTr("&Copy")
+            visible: replyContextMenu.text
+
             onTriggered: Clipboard.text = replyContextMenu.text
         }
-
         Platform.MenuItem {
-            visible: replyContextMenu.link
             enabled: visible
             text: qsTr("Copy &link location")
+            visible: replyContextMenu.link
+
             onTriggered: Clipboard.text = replyContextMenu.link
         }
-
         Platform.MenuItem {
-            visible: true
             enabled: visible
             text: qsTr("&Go to quoted message")
+            visible: true
+
             onTriggered: room.showEvent(replyContextMenu.eventId)
         }
-
     }
     RoundButton {
         id: toEndButton
-        anchors {
-            bottom: parent.bottom
-            right: scrollbar.left
-            bottomMargin: Nheko.paddingMedium+(fullWidth-width)/2
-            rightMargin: Nheko.paddingMedium+(fullWidth-width)/2
-        }
+
         property int fullWidth: 40
-        width: 0
-        height: width
-        radius: width/2
-        onClicked: function() { chat.positionViewAtBeginning(); TimelineManager.focusMessageInput(); }
+
         flat: true
+        height: width
         hoverEnabled: true
+        radius: width / 2
+        width: 0
 
         background: Rectangle {
-            color: toEndButton.down ? palette.highlight : palette.button
-            opacity: enabled ? 1 : 0.3
             border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
             border.width: 1
+            color: toEndButton.down ? palette.highlight : palette.button
+            opacity: enabled ? 1 : 0.3
             radius: toEndButton.radius
         }
-
         states: [
             State {
                 name: ""
-                PropertyChanges { target: toEndButton; width: 0 }
+
+                PropertyChanges {
+                    target: toEndButton
+                    width: 0
+                }
             },
             State {
                 name: "shown"
                 when: !chat.atYEnd
-                PropertyChanges { target: toEndButton; width: toEndButton.fullWidth }
+
+                PropertyChanges {
+                    target: toEndButton
+                    width: toEndButton.fullWidth
+                }
             }
         ]
-
-        Image {
-            id: buttonImg
-            anchors.fill: parent
-            anchors.margins: Nheko.paddingMedium
-            source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
-            fillMode: Image.PreserveAspectFit
-        }
-
         transitions: Transition {
             from: ""
-            to: "shown"
             reversible: true
+            to: "shown"
 
             SequentialAnimation {
-                PauseAnimation { duration: 500 }
+                PauseAnimation {
+                    duration: 500
+                }
                 PropertyAnimation {
-                    target: toEndButton
-                    properties: "width"
-                    easing.type: Easing.InOutQuad
                     duration: 200
+                    easing.type: Easing.InOutQuad
+                    properties: "width"
+                    target: toEndButton
                 }
             }
         }
+
+        onClicked: function () {
+            chat.positionViewAtBeginning();
+            TimelineManager.focusMessageInput();
+        }
+
+        anchors {
+            bottom: parent.bottom
+            bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
+            right: scrollbar.left
+            rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
+        }
+        Image {
+            id: buttonImg
+
+            anchors.fill: parent
+            anchors.margins: Nheko.paddingMedium
+            fillMode: Image.PreserveAspectFit
+            source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
+        }
     }
 }
diff --git a/resources/qml/PrivacyScreen.qml b/resources/qml/PrivacyScreen.qml
index da196667..a3539df7 100644
--- a/resources/qml/PrivacyScreen.qml
+++ b/resources/qml/PrivacyScreen.qml
@@ -11,9 +11,8 @@ Item {
     id: privacyScreen
 
     readonly property bool active: Settings.privacyScreen && screenSaver.state === "Visible"
-    property var timelineRoot
     property int screenTimeout
-
+    property var timelineRoot
     required property var windowTarget
 
     Connections {
@@ -24,29 +23,28 @@ Item {
             } else {
                 if (timelineRoot.visible)
                     screenSaverTimer.start();
-
             }
         }
 
         target: windowTarget
     }
-
     Timer {
         id: screenSaverTimer
 
         interval: screenTimeout * 1000
         running: !windowTarget.active
+
         onTriggered: {
             screenSaver.state = "Visible";
         }
     }
-
     Item {
         id: screenSaver
 
-        state: "Invisible"
         anchors.fill: parent
+        state: "Invisible"
         visible: false
+
         states: [
             State {
                 name: "Visible"
@@ -55,20 +53,18 @@ Item {
                     target: screenSaver
                     visible: true
                 }
-
                 PropertyChanges {
-                    target: screenSaver
                     opacity: 1
+                    target: screenSaver
                 }
             },
             State {
                 name: "Invisible"
 
                 PropertyChanges {
-                    target: screenSaver
                     opacity: 0
+                    target: screenSaver
                 }
-
                 PropertyChanges {
                     target: screenSaver
                     visible: false
@@ -78,39 +74,33 @@ Item {
         transitions: [
             Transition {
                 from: "Invisible"
-                to: "Visible"
                 reversible: true
+                to: "Visible"
 
                 SequentialAnimation {
                     NumberAnimation {
-                        target: screenSaver
-                        property: "visible"
                         duration: 0
+                        property: "visible"
+                        target: screenSaver
                     }
-
                     NumberAnimation {
-                        target: screenSaver
-                        property: "opacity"
                         duration: 300
                         easing.type: Easing.Linear
+                        property: "opacity"
+                        target: screenSaver
                     }
-
                 }
-
             }
         ]
 
         MultiEffect {
             id: blur
 
-            blurEnabled: true
-
             anchors.fill: parent
-            source: timelineRoot
             blur: 1.0
+            blurEnabled: true
             blurMax: 32
+            source: timelineRoot
         }
-
     }
-
 }
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index 5878b391..9ccefdec 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -11,33 +11,36 @@ Popup {
     id: quickSwitcher
 
     property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
+    property int textMargin: Nheko.paddingSmall
 
     background: null
-    width: Math.min(Math.max(Math.round(parent.width / 2),450),parent.width) // limiting width to parent.width/2 can be a bit narrow
-    x: Math.round(parent.width / 2 - contentWidth / 2)
-    y: Math.round(parent.height / 4)
-    modal: true
     closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+    modal: true
     parent: Overlay.overlay
+    width: Math.min(Math.max(Math.round(parent.width / 2), 450), parent.width) // limiting width to parent.width/2 can be a bit narrow
+    x: Math.round(parent.width / 2 - contentWidth / 2)
+    y: Math.round(parent.height / 4)
+
+    Overlay.modal: Rectangle {
+        color: "#aa1E1E1E"
+    }
+
+    onClosed: TimelineManager.focusMessageInput()
     onOpened: {
         roomTextInput.forceActiveFocus();
     }
-    onClosed: TimelineManager.focusMessageInput()
-    property int textMargin: Nheko.paddingSmall
 
-    Column{
+    Column {
         anchors.fill: parent
         spacing: 1
 
         MatrixTextField {
             id: roomTextInput
 
-            width: parent.width
-            font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
             color: palette.text
-            onTextEdited: {
-                completerPopup.completer.searchString = text;
-            }
+            font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
+            width: parent.width
+
             Keys.onPressed: {
                 if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) {
                     event.accepted = true;
@@ -45,49 +48,43 @@ Popup {
                 } else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) {
                     event.accepted = true;
                     if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
-                    completerPopup.up();
+                        completerPopup.up();
                     else
-                    completerPopup.down();
+                        completerPopup.down();
                 } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                     completerPopup.finishCompletion();
                     event.accepted = true;
                 }
             }
+            onTextEdited: {
+                completerPopup.completer.searchString = text;
+            }
         }
-
         Completer {
             id: completerPopup
 
-            visible: roomTextInput.text.length > 0
-            width: parent.width
-            completerName: "room"
-            bottomToTop: false
-            fullWidth: true
             avatarHeight: quickSwitcher.textHeight
             avatarWidth: quickSwitcher.textHeight
+            bottomToTop: false
             centerRowContent: false
+            completerName: "room"
+            fullWidth: true
             rowMargin: Math.round(quickSwitcher.textMargin / 2)
             rowSpacing: quickSwitcher.textMargin
+            visible: roomTextInput.text.length > 0
+            width: parent.width
         }
     }
-
     Connections {
         function onCompletionSelected(id) {
             Rooms.setCurrentRoom(id);
             quickSwitcher.close();
         }
-
         function onCountChanged() {
             if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
-            completerPopup.currentIndex = 0;
-
+                completerPopup.currentIndex = 0;
         }
 
         target: completerPopup
     }
-
-    Overlay.modal: Rectangle {
-        color: "#aa1E1E1E"
-    }
-
 }
diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml
index caee708e..5ab58beb 100644
--- a/resources/qml/Reactions.qml
+++ b/resources/qml/Reactions.qml
@@ -11,10 +11,11 @@ import im.nheko 1.0
 Flow {
     id: reactionFlow
 
+    property string eventId
+
     // lower-contrast colors to avoid distracting from text & to enhance hover effect
     property color gentleHighlight: Qt.hsla(palette.highlight.hslHue, palette.highlight.hslSaturation, palette.highlight.hslLightness, 0.8)
     property color gentleText: Qt.hsla(palette.text.hslHue, palette.text.hslSaturation, palette.text.hslLightness, 0.6)
-    property string eventId
     property alias reactions: repeater.model
 
     spacing: 4
@@ -25,40 +26,39 @@ Flow {
         delegate: AbstractButton {
             id: reaction
 
-            hoverEnabled: true
-            ToolTip.visible: hovered
             ToolTip.delay: Nheko.tooltipDelay
-            onClicked: {
-                console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
-                room.input.reaction(reactionFlow.eventId, modelData.key);
-            }
-            Component.onCompleted: {
-                ToolTip.text = Qt.binding(function() {
-                    if (textMetrics.elidedText === textMetrics.text) {
-                        return modelData.users;
-                    }
-                    return modelData.displayKey + "\n" + modelData.users;
-                })
-            }
-                leftPadding: textMetrics.height / 2
-                rightPadding: textMetrics.height / 2
+            ToolTip.visible: hovered
+            hoverEnabled: true
+            leftPadding: textMetrics.height / 2
+            rightPadding: textMetrics.height / 2
 
+            background: Rectangle {
+                anchors.centerIn: parent
+                border.color: reaction.hovered ? palette.text : gentleText
+                border.width: 1
+                color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window)
+                implicitHeight: reaction.implicitHeight
+                implicitWidth: reaction.implicitWidth
+                radius: reaction.height / 2
+            }
             contentItem: Row {
                 spacing: textMetrics.height / 4
 
                 TextMetrics {
                     id: textMetrics
 
-                    font.family: Settings.emojiFont
                     elide: Text.ElideRight
                     elideWidth: 150
+                    font.family: Settings.emojiFont
                     text: modelData.displayKey
                 }
-
                 Text {
                     id: reactionText
 
                     anchors.baseline: reactionCounter.baseline
+                    color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.text
+                    font.family: Settings.emojiFont
+                    maximumLineCount: 1
                     text: {
                         // When an emoji font is selected that doesn't have …, it is dropped from elidedText. So we add it back.
                         if (textMetrics.elidedText !== modelData.displayKey) {
@@ -68,51 +68,45 @@ Flow {
                         }
                         return textMetrics.elidedText;
                     }
-                    font.family: Settings.emojiFont
-                    color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.text
-                    maximumLineCount: 1
                     visible: !modelData.key.startsWith("mxc://")
                 }
                 Image {
                     anchors.verticalCenter: divider.verticalCenter
+                    fillMode: Image.PreserveAspectFit
                     height: textMetrics.height
-                    width: textMetrics.height
                     source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : ""
                     visible: modelData.key.startsWith("mxc://")
-                    fillMode: Image.PreserveAspectFit
+                    width: textMetrics.height
                 }
-
                 Rectangle {
                     id: divider
 
+                    color: reaction.hovered ? palette.text : gentleText
                     height: Math.floor(reactionCounter.implicitHeight * 1.4)
                     width: 1
-                    color: reaction.hovered ? palette.text: gentleText
                 }
-
                 Text {
                     id: reactionCounter
 
                     anchors.verticalCenter: divider.verticalCenter
-                    text: modelData.count
+                    color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.windowText
                     font: reaction.font
-                    color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.windowText
+                    text: modelData.count
                 }
-
             }
 
-            background: Rectangle {
-                anchors.centerIn: parent
-                implicitWidth: reaction.implicitWidth
-                implicitHeight: reaction.implicitHeight
-                border.color: reaction.hovered ? palette.text: gentleText
-                color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window)
-                border.width: 1
-                radius: reaction.height / 2
+            Component.onCompleted: {
+                ToolTip.text = Qt.binding(function () {
+                        if (textMetrics.elidedText === textMetrics.text) {
+                            return modelData.users;
+                        }
+                        return modelData.displayKey + "\n" + modelData.users;
+                    });
+            }
+            onClicked: {
+                console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
+                room.input.reaction(reactionFlow.eventId, modelData.key);
             }
-
         }
-
     }
-
 }
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 6fceb4e5..ce24297c 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -12,91 +12,89 @@ Rectangle {
     id: replyPopup
 
     Layout.fillWidth: true
-    visible: room && (room.reply || room.edit || room.thread)
+    color: palette.window
     // Height of child, plus margins, plus border
     implicitHeight: (room && room.reply ? replyPreview.height : Math.max(closeEditButton.height, closeThreadButton.height)) + Nheko.paddingSmall
-    color: palette.window
+    visible: room && (room.reply || room.edit || room.thread)
     z: 3
 
     Reply {
         id: replyPreview
 
-        property var modelData: room ? room.getDump(room.reply, room.id) : {
-        }
+        property var modelData: room ? room.getDump(room.reply, room.id) : {}
 
-        visible: room && room.reply
         anchors.left: parent.left
-        anchors.leftMargin: replyPopup.width < 450? Nheko.paddingSmall : (CallManager.callsSupported? 2*(22+16) : 1*(22+16))
+        anchors.leftMargin: replyPopup.width < 450 ? Nheko.paddingSmall : (CallManager.callsSupported ? 2 * (22 + 16) : 1 * (22 + 16))
         anchors.right: parent.right
-        anchors.rightMargin: replyPopup.width < 450? 2*(22+16) : 3*(22+16)
+        anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16)
         anchors.top: parent.top
         anchors.topMargin: Nheko.paddingSmall
-        userColor: TimelineManager.userColor(modelData.userId, palette.window)
         blurhash: modelData.blurhash ?? ""
         body: modelData.body ?? ""
-        formattedBody: modelData.formattedBody ?? ""
+        encryptionError: modelData.encryptionError ?? 0
         eventId: modelData.eventId ?? ""
         filename: modelData.filename ?? ""
         filesize: modelData.filesize ?? ""
+        formattedBody: modelData.formattedBody ?? ""
+        isOnlyEmoji: modelData.isOnlyEmoji ?? false
+        originalWidth: modelData.originalWidth ?? 0
         proportionalHeight: modelData.proportionalHeight ?? 1
         type: modelData.type ?? MtxEvent.UnknownMessage
         typeString: modelData.typeString ?? ""
         url: modelData.url ?? ""
-        originalWidth: modelData.originalWidth ?? 0
-        isOnlyEmoji: modelData.isOnlyEmoji ?? false
+        userColor: TimelineManager.userColor(modelData.userId, palette.window)
         userId: modelData.userId ?? ""
         userName: modelData.userName ?? ""
-        encryptionError: modelData.encryptionError ?? 0
+        visible: room && room.reply
         width: parent.width
     }
-
     ImageButton {
         id: closeReplyButton
 
-        visible: room && room.reply
+        ToolTip.text: qsTr("Close")
+        ToolTip.visible: closeReplyButton.hovered
+        anchors.margins: Nheko.paddingSmall
         anchors.right: replyPreview.right
         anchors.top: replyPreview.top
-        anchors.margins: Nheko.paddingSmall
-        hoverEnabled: true
-        width: 16
         height: 16
+        hoverEnabled: true
         image: ":/icons/icons/ui/dismiss.svg"
-        ToolTip.visible: closeReplyButton.hovered
-        ToolTip.text: qsTr("Close")
+        visible: room && room.reply
+        width: 16
+
         onClicked: room.reply = undefined
     }
-
     ImageButton {
         id: closeEditButton
 
-        visible: room && room.edit
-        anchors.right: closeThreadButton.left
+        ToolTip.text: qsTr("Cancel Edit")
+        ToolTip.visible: closeEditButton.hovered
         anchors.margins: 8
+        anchors.right: closeThreadButton.left
         anchors.top: parent.top
+        height: 22
         hoverEnabled: true
         image: ":/icons/icons/ui/dismiss_edit.svg"
+        visible: room && room.edit
         width: 22
-        height: 22
-        ToolTip.visible: closeEditButton.hovered
-        ToolTip.text: qsTr("Cancel Edit")
+
         onClicked: room.edit = undefined
     }
-
     ImageButton {
         id: closeThreadButton
 
-        visible: room && room.thread
-        anchors.right: parent.right
+        ToolTip.text: qsTr("Cancel Thread")
+        ToolTip.visible: closeThreadButton.hovered
         anchors.margins: 8
+        anchors.right: parent.right
         anchors.top: parent.top
-        hoverEnabled: true
         buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText
+        height: 22
+        hoverEnabled: true
         image: ":/icons/icons/ui/dismiss_thread.svg"
+        visible: room && room.thread
         width: 22
-        height: 22
-        ToolTip.visible: closeThreadButton.hovered
-        ToolTip.text: qsTr("Cancel Thread")
+
         onClicked: room.thread = undefined
     }
-
 }
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 851608b6..b41696e0 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -18,264 +18,431 @@ Page {
     property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
     property bool collapsed: false
 
-    // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
-    Connections {
-        function onHideMenu() {
-            userInfoMenu.close()
-            roomContextMenu.close()
-        }
-        target: MainWindow
-    }
-
-    Component {
-        id: roomDirectoryComponent
-
-        RoomDirectory {
-        }
-
-    }
-
-    Component {
-        id: createRoomComponent
-
-        CreateRoom {
-        }
+    background: Rectangle {
+        color: Nheko.theme.sidebarBackground
     }
+    footer: ColumnLayout {
+        spacing: 0
 
-    Component {
-        id: createDirectComponent
-
-        CreateDirect {
+        Rectangle {
+            Layout.fillWidth: true
+            color: Nheko.theme.separator
+            height: 1
         }
-    }
-
-    ListView {
-        id: roomlist
+        Pane {
+            Layout.alignment: Qt.AlignBottom
+            Layout.fillWidth: true
+            Layout.minimumHeight: 40
+            horizontalPadding: Nheko.paddingMedium
+            verticalPadding: 0
 
-        anchors.left: parent.left
-        anchors.right: parent.right
-        height: parent.height
-        model: Rooms
-        //reuseItems: true
+            background: Rectangle {
+                color: palette.window
+            }
+            contentItem: RowLayout {
+                id: buttonRow
 
-        ScrollBar.vertical: ScrollBar {
-            id: scrollbar
-            parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null
-        }
+                ImageButton {
+                    Layout.fillWidth: true
+                    Layout.margins: Nheko.paddingMedium
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Start a new chat")
+                    ToolTip.visible: hovered
+                    height: 22
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/add-square-button.svg"
+                    width: 22
 
-        Connections {
-            function onCurrentRoomChanged() {
-                if (Rooms.currentRoom)
-                    roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
+                    onClicked: roomJoinCreateMenu.open(parent)
 
-            }
+                    Platform.Menu {
+                        id: roomJoinCreateMenu
 
-            target: Rooms
-        }
+                        Platform.MenuItem {
+                            text: qsTr("Join a room")
 
-        Component {
-            id: roomWindowComponent
+                            onTriggered: Nheko.openJoinRoomDialog()
+                        }
+                        Platform.MenuItem {
+                            text: qsTr("Create a new room")
 
-            ApplicationWindow {
-                id: roomWindowW
+                            onTriggered: {
+                                var createRoom = createRoomComponent.createObject(timelineRoot);
+                                createRoom.show();
+                                timelineRoot.destroyOnClose(createRoom);
+                            }
+                        }
+                        Platform.MenuItem {
+                            text: qsTr("Start a direct chat")
 
-                property var room: null
-                property var roomPreview: null
+                            onTriggered: {
+                                var createDirect = createDirectComponent.createObject(timelineRoot);
+                                createDirect.show();
+                                timelineRoot.destroyOnClose(createDirect);
+                            }
+                        }
+                        Platform.MenuItem {
+                            text: qsTr("Create a new community")
 
-                Component.onCompleted: {
-                    MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW);
-                    Nheko.setTransientParent(roomWindowW, null);
+                            onTriggered: {
+                                var createRoom = createRoomComponent.createObject(timelineRoot, {
+                                        "space": true
+                                    });
+                                createRoom.show();
+                                timelineRoot.destroyOnClose(createRoom);
+                            }
+                        }
+                    }
                 }
-                Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW)
-
-                height: 650
-                width: 420
-                minimumWidth: 150
-                minimumHeight: 150
-                color: palette.window
-                title: room.plainRoomName
-                //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+                ImageButton {
+                    Layout.fillWidth: true
+                    Layout.margins: Nheko.paddingMedium
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Room directory")
+                    ToolTip.visible: hovered
+                    height: 22
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/room-directory.svg"
+                    visible: !collapsed
+                    width: 22
 
-                Shortcut {
-                    sequence: StandardKey.Cancel
-                    onActivated: roomWindowW.close()
+                    onClicked: {
+                        var win = roomDirectoryComponent.createObject(timelineRoot);
+                        win.show();
+                        timelineRoot.destroyOnClose(win);
+                    }
                 }
+                ImageButton {
+                    Layout.fillWidth: true
+                    Layout.margins: Nheko.paddingMedium
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Search rooms (Ctrl+K)")
+                    ToolTip.visible: hovered
+                    height: 22
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/search.svg"
+                    ripple: false
+                    visible: !collapsed
+                    width: 22
 
-                TimelineView {
-                    id: timeline
+                    onClicked: {
+                        var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml");
+                        if (component.status == Component.Ready) {
+                            var quickSwitch = component.createObject(timelineRoot);
+                            quickSwitch.open();
+                            destroyOnClosed(quickSwitch);
+                        } else {
+                            console.error("Failed to create component: " + component.errorString());
+                        }
+                    }
+                }
+                ImageButton {
+                    Layout.fillWidth: true
+                    Layout.margins: Nheko.paddingMedium
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("User settings")
+                    ToolTip.visible: hovered
+                    height: 22
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/settings.svg"
+                    ripple: false
+                    visible: !collapsed
+                    width: 22
 
-                    privacyScreen: privacyScreen
-                    anchors.fill: parent
-                    room: roomWindowW.room
-                    roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null
+                    onClicked: mainWindow.push(userSettingsPage)
                 }
+            }
+        }
+    }
+    header: ColumnLayout {
+        spacing: 0
 
-                PrivacyScreen {
-                    id: privacyScreen
+        Pane {
+            id: userInfoPanel
 
-                    anchors.fill: parent
-                    visible: Settings.privacyScreen
-                    screenTimeout: Settings.privacyScreenTimeout
-                    timelineRoot: timeline
-                    windowTarget: roomWindowW
+            function openUserProfile() {
+                Nheko.updateUserProfile();
+                var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml");
+                if (component.status == Component.Ready) {
+                    var userProfile = component.createObject(timelineRoot, {
+                            "profile": Nheko.currentUser
+                        });
+                    userProfile.show();
+                    timelineRoot.destroyOnClose(userProfile);
+                } else {
+                    console.error("Failed to create component: " + component.errorString());
                 }
+            }
 
-                onActiveChanged: { room.lastReadIdOnWindowFocus(); }
+            Layout.alignment: Qt.AlignBottom
+            Layout.fillWidth: true
+            Layout.minimumHeight: 40
+            //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
+            padding: Nheko.paddingMedium
+
+            background: Rectangle {
+                color: palette.window
             }
+            contentItem: RowLayout {
+                id: userInfoGrid
 
-        }
+                property var profile: Nheko.currentUser
 
+                spacing: Nheko.paddingMedium
 
-        Component {
-            id: nestedSpaceMenuLevel
+                Avatar {
+                    id: avatar
 
-            SpaceMenuLevel {
-                roomid: roomContextMenu.roomid
-                childMenu: rootSpaceMenu.childMenu
-            }
-        }
+                    Layout.alignment: Qt.AlignVCenter
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
+                    displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
+                    enabled: false
+                    url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
+                    userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
+                }
+                ColumnLayout {
+                    id: col
 
+                    Layout.alignment: Qt.AlignLeft
+                    Layout.fillWidth: true
+                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
+                    spacing: 0
+                    visible: !collapsed
+                    width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
 
-        Platform.Menu {
-            id: roomContextMenu
+                    ElidedLabel {
+                        Layout.alignment: Qt.AlignBottom
+                        elideWidth: col.width
+                        font.pointSize: fontMetrics.font.pointSize * 1.1
+                        font.weight: Font.DemiBold
+                        fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
+                    }
+                    ElidedLabel {
+                        Layout.alignment: Qt.AlignTop
+                        color: palette.buttonText
+                        elideWidth: col.width
+                        font.pointSize: fontMetrics.font.pointSize * 0.9
+                        fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
+                    }
+                }
+                Item {
+                }
+                ImageButton {
+                    id: logoutButton
 
-            property string roomid
-            property var tags
+                    Layout.alignment: Qt.AlignVCenter
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Logout")
+                    ToolTip.visible: hovered
+                    image: ":/icons/icons/ui/power-off.svg"
+                    visible: !collapsed
 
-            function show(roomid_, tags_) {
-                roomid = roomid_;
-                tags = tags_;
-                open();
+                    onClicked: Nheko.openLogoutDialog()
+                }
             }
 
             InputDialog {
-                id: newTag
+                id: statusDialog
 
-                title: qsTr("New tag")
-                prompt: qsTr("Enter the tag you want to use:")
-                onAccepted: function(text) {
-                    Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
+                prompt: qsTr("Enter your status message:")
+                title: qsTr("Status Message")
+
+                onAccepted: function (text) {
+                    Nheko.setStatusMessage(text);
                 }
             }
+            Platform.Menu {
+                id: userInfoMenu
 
-            Platform.MenuItem {
-                text: qsTr("Open separately")
-                onTriggered: {
-                    var roomWindow = roomWindowComponent.createObject(null, {
-                    "room": Rooms.getRoomById(roomContextMenu.roomid),
-                    "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid)
-                    });
-                    roomWindow.showNormal();
-                    destroyOnClose(roomWindow);
+                Platform.MenuItem {
+                    text: qsTr("Profile settings")
+
+                    onTriggered: userInfoPanel.openUserProfile()
                 }
-            }
+                Platform.MenuItem {
+                    text: qsTr("Set status message")
 
-            Platform.MenuItem {
-                text: qsTr("Room settings")
-                onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
+                    onTriggered: statusDialog.show()
+                }
             }
+            TapHandler {
+                acceptedButtons: Qt.LeftButton
+                gesturePolicy: TapHandler.ReleaseWithinBounds
+                margin: -Nheko.paddingSmall
 
-            Platform.MenuItem {
-                text: qsTr("Leave room")
-                onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
+                onLongPressed: userInfoMenu.open()
+                onSingleTapped: userInfoPanel.openUserProfile()
             }
+            TapHandler {
+                acceptedButtons: Qt.RightButton
+                gesturePolicy: TapHandler.ReleaseWithinBounds
+                margin: -Nheko.paddingSmall
 
-            Platform.MenuItem {
-                text: qsTr("Copy room link")
-                onTriggered: Rooms.copyLink(roomContextMenu.roomid)
+                onSingleTapped: userInfoMenu.open()
             }
+        }
+        Rectangle {
+            Layout.fillWidth: true
+            color: Nheko.theme.separator
+            height: 2
+        }
+        Rectangle {
+            id: unverifiedStuffBubble
 
-            Platform.Menu {
-                id: tagsMenu
-                title: qsTr("Tag room as:")
+            Layout.fillWidth: true
+            color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
+            implicitHeight: explanation.height + Nheko.paddingMedium * 2
+            visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
 
-                Instantiator {
-                    model: Communities.tagsWithDefault
-                    onObjectAdded: (index, object) => tagsMenu.insertItem(index, object)
-                    onObjectRemoved: (index, object) => tagsMenu.removeItem(object)
+            RowLayout {
+                id: unverifiedStuffBubbleContainer
 
-                    delegate: Platform.MenuItem {
-                        property string t: modelData
+                height: explanation.height + Nheko.paddingMedium * 2
+                spacing: 0
+                width: parent.width
 
-                        text: {
-                            switch (t) {
-                                case "m.favourite":
-                                return qsTr("Favourite");
-                                case "m.lowpriority":
-                                return qsTr("Low priority");
-                                case "m.server_notice":
-                                return qsTr("Server notice");
-                                default:
-                                return t.substring(2);
-                            }
+                Label {
+                    id: explanation
+
+                    Layout.fillWidth: true
+                    Layout.margins: Nheko.paddingMedium
+                    Layout.rightMargin: Nheko.paddingSmall
+                    color: palette.buttonText
+                    text: {
+                        switch (SelfVerificationStatus.status) {
+                        case SelfVerificationStatus.NoMasterKey:
+                            //: Cross-signing setup has not run yet.
+                            return qsTr("Encryption not set up");
+                        case SelfVerificationStatus.UnverifiedMasterKey:
+                            //: The user just signed in with this device and hasn't verified their master key.
+                            return qsTr("Unverified login");
+                        case SelfVerificationStatus.UnverifiedDevices:
+                            //: There are unverified devices signed in to this account.
+                            return qsTr("Please verify your other devices");
+                        default:
+                            return "";
                         }
-                        checkable: true
-                        checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
-                        onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
                     }
-
+                    textFormat: Text.PlainText
+                    wrapMode: Text.Wrap
                 }
+                ImageButton {
+                    id: closeUnverifiedBubble
 
-                Platform.MenuItem {
-                    text: qsTr("Create new tag...")
-                    onTriggered: newTag.show()
+                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                    Layout.rightMargin: Nheko.paddingMedium
+                    ToolTip.delay: Nheko.tooltipDelay
+                    ToolTip.text: qsTr("Close")
+                    ToolTip.visible: closeUnverifiedBubble.hovered
+                    height: fontMetrics.font.pixelSize
+                    hoverEnabled: true
+                    image: ":/icons/icons/ui/dismiss.svg"
+                    width: fontMetrics.font.pixelSize
+
+                    onClicked: unverifiedStuffBubble.visible = false
                 }
             }
+            HoverHandler {
+                id: verifyButtonHovered
 
-            SpaceMenuLevel {
-                id: rootSpaceMenu
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                enabled: !closeUnverifiedBubble.hovered
+            }
+            TapHandler {
+                acceptedButtons: Qt.LeftButton
+                enabled: !closeUnverifiedBubble.hovered
 
-                roomid: roomContextMenu.roomid
-                position: -1
-                title: qsTr("Add or remove from community...")
-                childMenu: nestedSpaceMenuLevel
+                onSingleTapped: {
+                    if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
+                        SelfVerificationStatus.verifyUnverifiedDevices();
+                    else
+                        SelfVerificationStatus.statusChanged();
+                }
             }
         }
+        Rectangle {
+            Layout.fillWidth: true
+            color: Nheko.theme.separator
+            height: 1
+            visible: unverifiedStuffBubble.visible
+        }
+    }
+
+    // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
+    Connections {
+        function onHideMenu() {
+            userInfoMenu.close();
+            roomContextMenu.close();
+        }
+
+        target: MainWindow
+    }
+    Component {
+        id: roomDirectoryComponent
+
+        RoomDirectory {
+        }
+    }
+    Component {
+        id: createRoomComponent
+
+        CreateRoom {
+        }
+    }
+    Component {
+        id: createDirectComponent
+
+        CreateDirect {
+        }
+    }
+    ListView {
+        id: roomlist
+
+        anchors.left: parent.left
+        anchors.right: parent.right
+        height: parent.height
+        model: Rooms
 
+        //reuseItems: true
+        ScrollBar.vertical: ScrollBar {
+            id: scrollbar
+
+            parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null
+        }
         delegate: ItemDelegate {
             id: roomItem
 
+            required property string avatarUrl
             property color backgroundColor: palette.window
-            property color importantText: palette.text
-            property color unimportantText: palette.buttonText
             property color bubbleBackground: palette.highlight
             property color bubbleText: palette.highlightedText
-            required property string roomName
-            required property string roomId
-            required property string avatarUrl
-            required property string time
-            required property string lastMessage
-            required property var tags
-            required property bool isInvite
-            required property bool isSpace
-            required property int notificationCount
+            required property string directChatOtherUserId
             required property bool hasLoudNotification
             required property bool hasUnreadMessages
+            property color importantText: palette.text
             required property bool isDirect
-            required property string directChatOtherUserId
-
-            Ripple {
-                color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5)
-            }
+            required property bool isInvite
+            required property bool isSpace
+            required property string lastMessage
+            required property int notificationCount
+            required property string roomId
+            required property string roomName
+            required property var tags
+            required property string time
+            property color unimportantText: palette.buttonText
 
-            height: avatarSize + 2 * Nheko.paddingMedium
-            width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
-            state: "normal"
-            ToolTip.visible: hovered && collapsed
             ToolTip.delay: Nheko.tooltipDelay
             ToolTip.text: roomName
-            onClicked: {
-                console.log("tapped " + roomId);
-
-                if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
-                    Rooms.setCurrentRoom(roomId);
-                else
-                    Rooms.resetCurrentRoom();
-            }
-            onPressAndHold: {
-                if (!isInvite)
-                    roomContextMenu.show(roomId, tags);
+            ToolTip.visible: hovered && collapsed
+            height: avatarSize + 2 * Nheko.paddingMedium
+            state: "normal"
+            width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
 
+            background: Rectangle {
+                color: backgroundColor
             }
             states: [
                 State {
@@ -283,31 +450,45 @@ Page {
                     when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
 
                     PropertyChanges {
-                        target: roomItem
                         backgroundColor: palette.dark
-                        importantText: palette.brightText
-                        unimportantText: palette.brightText
                         bubbleBackground: palette.highlight
                         bubbleText: palette.highlightedText
+                        importantText: palette.brightText
+                        target: roomItem
+                        unimportantText: palette.brightText
                     }
-
                 },
                 State {
                     name: "selected"
                     when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId
 
                     PropertyChanges {
-                        target: roomItem
                         backgroundColor: palette.highlight
-                        importantText: palette.highlightedText
-                        unimportantText: palette.highlightedText
                         bubbleBackground: palette.highlightedText
                         bubbleText: palette.highlight
+                        importantText: palette.highlightedText
+                        target: roomItem
+                        unimportantText: palette.highlightedText
                     }
-
                 }
             ]
 
+            onClicked: {
+                console.log("tapped " + roomId);
+                if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
+                    Rooms.setCurrentRoom(roomId);
+                else
+                    Rooms.resetCurrentRoom();
+            }
+            onPressAndHold: {
+                if (!isInvite)
+                    roomContextMenu.show(roomId, tags);
+            }
+
+            Ripple {
+                color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5)
+            }
+
             // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that...
             Item {
                 anchors.fill: parent
@@ -315,76 +496,71 @@ Page {
 
                 TapHandler {
                     acceptedButtons: Qt.RightButton
+                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                    gesturePolicy: TapHandler.ReleaseWithinBounds
+
                     onSingleTapped: {
                         if (!TimelineManager.isInvite)
                             roomContextMenu.show(roomId, tags);
-
                     }
-                    gesturePolicy: TapHandler.ReleaseWithinBounds
-                    acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
                 }
-
             }
-
             RowLayout {
-                spacing: Nheko.paddingMedium
                 anchors.fill: parent
                 anchors.margins: Nheko.paddingMedium
+                spacing: Nheko.paddingMedium
 
                 Avatar {
                     id: avatar
 
-                    enabled: false
                     Layout.alignment: Qt.AlignVCenter
+                    displayName: roomName
+                    enabled: false
                     height: avatarSize
-                    width: avatarSize
+                    roomid: roomId
                     url: avatarUrl.replace("mxc://", "image://MxcImage/")
-                    displayName: roomName
                     userid: isDirect ? directChatOtherUserId : ""
-                    roomid: roomId
+                    width: avatarSize
 
                     NotificationBubble {
                         id: collapsedNotificationBubble
 
-                        notificationCount: roomItem.notificationCount
-                        hasLoudNotification: roomItem.hasLoudNotification
-                        bubbleBackgroundColor: roomItem.bubbleBackground
-                        bubbleTextColor: roomItem.bubbleText
-                        anchors.right: parent.right
                         anchors.bottom: parent.bottom
                         anchors.margins: -Nheko.paddingSmall
+                        anchors.right: parent.right
+                        bubbleBackgroundColor: roomItem.bubbleBackground
+                        bubbleTextColor: roomItem.bubbleText
+                        hasLoudNotification: roomItem.hasLoudNotification
                         mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true)
+                        notificationCount: roomItem.notificationCount
                     }
-
                 }
-
                 ColumnLayout {
                     id: textContent
 
-                    visible: !collapsed
                     Layout.alignment: Qt.AlignLeft
                     Layout.fillWidth: true
                     Layout.minimumWidth: 100
-                    width: parent.width - avatar.width
                     Layout.preferredWidth: parent.width - avatar.width
                     height: avatar.height
                     spacing: Nheko.paddingSmall
+                    visible: !collapsed
+                    width: parent.width - avatar.width
 
                     NotificationBubble {
                         id: notificationBubble
 
-                        parent: isSpace ? titleRow : subtextRow
-                        notificationCount: roomItem.notificationCount
-                        hasLoudNotification: roomItem.hasLoudNotification
-                        bubbleBackgroundColor: roomItem.bubbleBackground
-                        bubbleTextColor: roomItem.bubbleText
                         Layout.alignment: Qt.AlignRight
                         Layout.leftMargin: Nheko.paddingSmall
-                        Layout.preferredWidth: implicitWidth
                         Layout.preferredHeight: implicitHeight
+                        Layout.preferredWidth: implicitWidth
+                        bubbleBackgroundColor: roomItem.bubbleBackground
+                        bubbleTextColor: roomItem.bubbleText
+                        hasLoudNotification: roomItem.hasLoudNotification
                         mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true)
+                        notificationCount: roomItem.notificationCount
+                        parent: isSpace ? titleRow : subtextRow
                     }
-
                     RowLayout {
                         id: titleRow
 
@@ -394,433 +570,216 @@ Page {
 
                         ElidedLabel {
                             id: rN
+
                             Layout.alignment: Qt.AlignBaseline
+                            Layout.fillWidth: true
                             color: roomItem.importantText
                             elideWidth: width
                             fullText: TimelineManager.htmlEscape(roomName)
                             textFormat: Text.RichText
-                            Layout.fillWidth: true
                         }
-
                         Label {
                             id: timestamp
 
-                            visible: !isInvite && !isSpace
-                            width: visible ? 0 : undefined
                             Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
-                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             color: roomItem.unimportantText
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             text: time
+                            visible: !isInvite && !isSpace
+                            width: visible ? 0 : undefined
                         }
-
                     }
-
                     RowLayout {
                         id: subtextRow
 
+                        Layout.alignment: Qt.AlignBottom
                         Layout.fillWidth: true
+                        height: visible ? 0 : undefined
                         spacing: 0
                         visible: !isSpace
-                        height: visible ? 0 : undefined
-                        Layout.alignment: Qt.AlignBottom
 
                         ElidedLabel {
+                            Layout.fillWidth: true
                             color: roomItem.unimportantText
-                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             elideWidth: width
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             fullText: TimelineManager.htmlEscape(lastMessage)
                             textFormat: Text.RichText
-                            Layout.fillWidth: true
                         }
-
                     }
-
                 }
-
             }
-
             Rectangle {
                 anchors.left: parent.left
                 anchors.verticalCenter: parent.verticalCenter
-                height: parent.height - Nheko.paddingSmall * 2
-                width: 3
                 color: palette.highlight
+                height: parent.height - Nheko.paddingSmall * 2
                 visible: hasUnreadMessages
+                width: 3
             }
-
-            background: Rectangle {
-                color: backgroundColor
-            }
-
         }
 
-    }
-
-    background: Rectangle {
-        color: Nheko.theme.sidebarBackground
-    }
-
-    header: ColumnLayout {
-        spacing: 0
-
-        Pane {
-            id: userInfoPanel
-
-            function openUserProfile() {
-                Nheko.updateUserProfile();
-                var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml")
-                if (component.status == Component.Ready) {
-                    var userProfile = component.createObject(timelineRoot, {"profile": Nheko.currentUser});
-                    userProfile.show();
-                    timelineRoot.destroyOnClose(userProfile);
-                } else {
-                    console.error("Failed to create component: " + component.errorString());
-                }
+        Connections {
+            function onCurrentRoomChanged() {
+                if (Rooms.currentRoom)
+                    roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
             }
 
+            target: Rooms
+        }
+        Component {
+            id: roomWindowComponent
 
-            Layout.fillWidth: true
-            Layout.alignment: Qt.AlignBottom
-            //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
-            padding: Nheko.paddingMedium
-            Layout.minimumHeight: 40
-
-            background: Rectangle {color: palette.window}
-
-            InputDialog {
-                id: statusDialog
-
-                title: qsTr("Status Message")
-                prompt: qsTr("Enter your status message:")
-                onAccepted: function(text) {
-                    Nheko.setStatusMessage(text);
-                }
-            }
+            ApplicationWindow {
+                id: roomWindowW
 
-            Platform.Menu {
-                id: userInfoMenu
+                property var room: null
+                property var roomPreview: null
 
-                Platform.MenuItem {
-                    text: qsTr("Profile settings")
-                    onTriggered: userInfoPanel.openUserProfile()
-                }
+                color: palette.window
+                height: 650
+                minimumHeight: 150
+                minimumWidth: 150
+                title: room.plainRoomName
+                width: 420
 
-                Platform.MenuItem {
-                    text: qsTr("Set status message")
-                    onTriggered: statusDialog.show()
+                Component.onCompleted: {
+                    MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW);
+                    Nheko.setTransientParent(roomWindowW, null);
                 }
-
-            }
-
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                acceptedButtons: Qt.LeftButton
-                onSingleTapped: userInfoPanel.openUserProfile()
-                onLongPressed: userInfoMenu.open()
-                gesturePolicy: TapHandler.ReleaseWithinBounds
-            }
-
-            TapHandler {
-                margin: -Nheko.paddingSmall
-                acceptedButtons: Qt.RightButton
-                onSingleTapped: userInfoMenu.open()
-                gesturePolicy: TapHandler.ReleaseWithinBounds
-            }
-
-            contentItem: RowLayout {
-                id: userInfoGrid
-
-                property var profile: Nheko.currentUser
-
-                spacing: Nheko.paddingMedium
-
-                Avatar {
-                    id: avatar
-
-                    Layout.alignment: Qt.AlignVCenter
-                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
-                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
-                    url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
-                    displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
-                    userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
-                    enabled: false
+                Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW)
+                onActiveChanged: {
+                    room.lastReadIdOnWindowFocus();
                 }
 
-                ColumnLayout {
-                    id: col
-
-                    visible: !collapsed
-                    Layout.alignment: Qt.AlignLeft
-                    Layout.fillWidth: true
-                    width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
-                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
-                    spacing: 0
-
-                    ElidedLabel {
-                        Layout.alignment: Qt.AlignBottom
-                        font.pointSize: fontMetrics.font.pointSize * 1.1
-                        font.weight: Font.DemiBold
-                        fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
-                        elideWidth: col.width
-                    }
-
-                    ElidedLabel {
-                        Layout.alignment: Qt.AlignTop
-                        color: palette.buttonText
-                        font.pointSize: fontMetrics.font.pointSize * 0.9
-                        elideWidth: col.width
-                        fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
-                    }
+                //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+                Shortcut {
+                    sequence: StandardKey.Cancel
 
+                    onActivated: roomWindowW.close()
                 }
+                TimelineView {
+                    id: timeline
 
-                Item {
+                    anchors.fill: parent
+                    privacyScreen: privacyScreen
+                    room: roomWindowW.room
+                    roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null
                 }
+                PrivacyScreen {
+                    id: privacyScreen
 
-                ImageButton {
-                    id: logoutButton
-
-                    visible: !collapsed
-                    Layout.alignment: Qt.AlignVCenter
-                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
-                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
-                    image: ":/icons/icons/ui/power-off.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Logout")
-                    onClicked: Nheko.openLogoutDialog()
+                    anchors.fill: parent
+                    screenTimeout: Settings.privacyScreenTimeout
+                    timelineRoot: timeline
+                    visible: Settings.privacyScreen
+                    windowTarget: roomWindowW
                 }
-
             }
-
         }
+        Component {
+            id: nestedSpaceMenuLevel
 
-        Rectangle {
-            color: Nheko.theme.separator
-            height: 2
-            Layout.fillWidth: true
+            SpaceMenuLevel {
+                childMenu: rootSpaceMenu.childMenu
+                roomid: roomContextMenu.roomid
+            }
         }
+        Platform.Menu {
+            id: roomContextMenu
 
-        Rectangle {
-            id: unverifiedStuffBubble
-
-            color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
-            Layout.fillWidth: true
-            implicitHeight: explanation.height + Nheko.paddingMedium * 2
-            visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
+            property string roomid
+            property var tags
 
-            RowLayout {
-                id: unverifiedStuffBubbleContainer
+            function show(roomid_, tags_) {
+                roomid = roomid_;
+                tags = tags_;
+                open();
+            }
 
-                width: parent.width
-                height: explanation.height + Nheko.paddingMedium * 2
-                spacing: 0
+            InputDialog {
+                id: newTag
 
-                Label {
-                    id: explanation
+                prompt: qsTr("Enter the tag you want to use:")
+                title: qsTr("New tag")
 
-                    Layout.margins: Nheko.paddingMedium
-                    Layout.rightMargin: Nheko.paddingSmall
-                    color: palette.buttonText
-                    Layout.fillWidth: true
-                    text: {
-                        switch (SelfVerificationStatus.status) {
-                        case SelfVerificationStatus.NoMasterKey:
-                            //: Cross-signing setup has not run yet.
-                            return qsTr("Encryption not set up");
-                        case SelfVerificationStatus.UnverifiedMasterKey:
-                            //: The user just signed in with this device and hasn't verified their master key.
-                            return qsTr("Unverified login");
-                        case SelfVerificationStatus.UnverifiedDevices:
-                            //: There are unverified devices signed in to this account.
-                            return qsTr("Please verify your other devices");
-                        default:
-                            return "";
-                        }
-                    }
-                    textFormat: Text.PlainText
-                    wrapMode: Text.Wrap
+                onAccepted: function (text) {
+                    Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
                 }
+            }
+            Platform.MenuItem {
+                text: qsTr("Open separately")
 
-                ImageButton {
-                    id: closeUnverifiedBubble
-
-                    Layout.rightMargin: Nheko.paddingMedium
-                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                    hoverEnabled: true
-                    width: fontMetrics.font.pixelSize
-                    height: fontMetrics.font.pixelSize
-                    image: ":/icons/icons/ui/dismiss.svg"
-                    ToolTip.visible: closeUnverifiedBubble.hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Close")
-                    onClicked: unverifiedStuffBubble.visible = false
+                onTriggered: {
+                    var roomWindow = roomWindowComponent.createObject(null, {
+                            "room": Rooms.getRoomById(roomContextMenu.roomid),
+                            "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid)
+                        });
+                    roomWindow.showNormal();
+                    destroyOnClose(roomWindow);
                 }
-
             }
+            Platform.MenuItem {
+                text: qsTr("Room settings")
 
-            HoverHandler {
-                id: verifyButtonHovered
-
-                enabled: !closeUnverifiedBubble.hovered
-                acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
             }
+            Platform.MenuItem {
+                text: qsTr("Leave room")
 
-            TapHandler {
-                enabled: !closeUnverifiedBubble.hovered
-                acceptedButtons: Qt.LeftButton
-                onSingleTapped: {
-                    if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
-                        SelfVerificationStatus.verifyUnverifiedDevices();
-                    else
-                        SelfVerificationStatus.statusChanged();
-                }
+                onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
             }
+            Platform.MenuItem {
+                text: qsTr("Copy room link")
 
-        }
-
-        Rectangle {
-            color: Nheko.theme.separator
-            height: 1
-            Layout.fillWidth: true
-            visible: unverifiedStuffBubble.visible
-        }
-
-    }
-
-    footer: ColumnLayout {
-        spacing: 0
-
-        Rectangle {
-            color: Nheko.theme.separator
-            height: 1
-            Layout.fillWidth: true
-        }
-
-        Pane {
-            Layout.fillWidth: true
-            Layout.alignment: Qt.AlignBottom
-            Layout.minimumHeight: 40
-
-            horizontalPadding: Nheko.paddingMedium
-            verticalPadding: 0
-
-            background: Rectangle {color: palette.window}
-            contentItem: RowLayout {
-                id: buttonRow
-
-                ImageButton {
-                    Layout.fillWidth: true
-                    hoverEnabled: true
-                    width: 22
-                    height: 22
-                    image: ":/icons/icons/ui/add-square-button.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Start a new chat")
-                    Layout.margins: Nheko.paddingMedium
-                    onClicked: roomJoinCreateMenu.open(parent)
-
-                    Platform.Menu {
-                        id: roomJoinCreateMenu
+                onTriggered: Rooms.copyLink(roomContextMenu.roomid)
+            }
+            Platform.Menu {
+                id: tagsMenu
 
-                        Platform.MenuItem {
-                            text: qsTr("Join a room")
-                            onTriggered: Nheko.openJoinRoomDialog()
-                        }
+                title: qsTr("Tag room as:")
 
-                        Platform.MenuItem {
-                            text: qsTr("Create a new room")
-                            onTriggered: {
-                                var createRoom = createRoomComponent.createObject(timelineRoot);
-                                createRoom.show();
-                                timelineRoot.destroyOnClose(createRoom);
-                            }
-                        }
+                Instantiator {
+                    model: Communities.tagsWithDefault
 
-                        Platform.MenuItem {
-                            text: qsTr("Start a direct chat")
-                            onTriggered: {
-                                var createDirect = createDirectComponent.createObject(timelineRoot);
-                                createDirect.show();
-                                timelineRoot.destroyOnClose(createDirect);
-                            }
-                        }
+                    delegate: Platform.MenuItem {
+                        property string t: modelData
 
-                        Platform.MenuItem {
-                            text: qsTr("Create a new community")
-                            onTriggered: {
-                                var createRoom = createRoomComponent.createObject(timelineRoot, { "space": true });
-                                createRoom.show();
-                                timelineRoot.destroyOnClose(createRoom);
+                        checkable: true
+                        checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
+                        text: {
+                            switch (t) {
+                            case "m.favourite":
+                                return qsTr("Favourite");
+                            case "m.lowpriority":
+                                return qsTr("Low priority");
+                            case "m.server_notice":
+                                return qsTr("Server notice");
+                            default:
+                                return t.substring(2);
                             }
                         }
 
+                        onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
                     }
 
+                    onObjectAdded: (index, object) => tagsMenu.insertItem(index, object)
+                    onObjectRemoved: (index, object) => tagsMenu.removeItem(object)
                 }
+                Platform.MenuItem {
+                    text: qsTr("Create new tag...")
 
-                ImageButton {
-                    visible: !collapsed
-                    Layout.fillWidth: true
-                    hoverEnabled: true
-                    width: 22
-                    height: 22
-                    image: ":/icons/icons/ui/room-directory.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Room directory")
-                    Layout.margins: Nheko.paddingMedium
-                    onClicked: {
-                        var win = roomDirectoryComponent.createObject(timelineRoot);
-                        win.show();
-                        timelineRoot.destroyOnClose(win);
-                    }
-                }
-
-                ImageButton {
-                    visible: !collapsed
-                    Layout.fillWidth: true
-                    hoverEnabled: true
-                    ripple: false
-                    width: 22
-                    height: 22
-                    image: ":/icons/icons/ui/search.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Search rooms (Ctrl+K)")
-                    Layout.margins: Nheko.paddingMedium
-                    onClicked: {
-                        var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml")
-                        if (component.status == Component.Ready) {
-                            var quickSwitch = component.createObject(timelineRoot);
-                            quickSwitch.open();
-                            destroyOnClosed(quickSwitch);
-                        } else {
-                            console.error("Failed to create component: " + component.errorString());
-                        }
-                    }
-                }
-
-                ImageButton {
-                    visible: !collapsed
-                    Layout.fillWidth: true
-                    hoverEnabled: true
-                    ripple: false
-                    width: 22
-                    height: 22
-                    image: ":/icons/icons/ui/settings.svg"
-                    ToolTip.visible: hovered
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("User settings")
-                    Layout.margins: Nheko.paddingMedium
-                    onClicked: mainWindow.push(userSettingsPage);
+                    onTriggered: newTag.show()
                 }
-
             }
+            SpaceMenuLevel {
+                id: rootSpaceMenu
 
+                childMenu: nestedSpaceMenuLevel
+                position: -1
+                roomid: roomContextMenu.roomid
+                title: qsTr("Add or remove from community...")
+            }
         }
-
     }
-
 }
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 01fde18e..cb000040 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -20,19 +20,14 @@ import im.nheko.EmojiModel 1.0
 Pane {
     id: timelineRoot
 
-    background: null
-    padding: 0
-
-    FontMetrics {
-        id: fontMetrics
-    }
-
-    RoomDirectoryModel {
-        id: publicRooms
+    function destroyOnClose(obj) {
+        if (obj.closing != undefined)
+            obj.closing.connect(() => obj.destroy(1000));
+        else if (obj.aboutToHide != undefined)
+            obj.aboutToHide.connect(() => obj.destroy(1000));
     }
-
-    UserDirectoryModel {
-        id: userDirectory
+    function destroyOnClosed(obj) {
+        obj.aboutToHide.connect(() => obj.destroy(1000));
     }
 
     //Timer {
@@ -41,54 +36,49 @@ Pane {
     //    running: true
     //    repeat: true
     //}
-
     function showAliasEditor(settings) {
-        var component = Qt.createComponent("qrc:/qml/dialogs/AliasEditor.qml")
+        var component = Qt.createComponent("qrc:/qml/dialogs/AliasEditor.qml");
         if (component.status == Component.Ready) {
             var dialog = component.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
+                    "roomSettings": settings
+                });
             dialog.show();
             destroyOnClose(dialog);
         } else {
             console.error("Failed to create component: " + component.errorString());
         }
-
     }
-
-    function showPLEditor(settings) {
-        var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml")
+    function showAllowedRoomsEditor(settings) {
+        var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml");
         if (component.status == Component.Ready) {
             var dialog = component.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
+                    "roomSettings": settings
+                });
             dialog.show();
             destroyOnClose(dialog);
         } else {
             console.error("Failed to create component: " + component.errorString());
         }
     }
-
-    function showSpacePLApplyPrompt(settings, editingModel) {
-        var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml")
+    function showPLEditor(settings) {
+        var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml");
         if (component.status == Component.Ready) {
             var dialog = component.createObject(timelineRoot, {
-                "roomSettings": settings,
-                "editingModel": editingModel
-            });
+                    "roomSettings": settings
+                });
             dialog.show();
             destroyOnClose(dialog);
         } else {
             console.error("Failed to create component: " + component.errorString());
         }
     }
-
-    function showAllowedRoomsEditor(settings) {
-        var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml")
+    function showSpacePLApplyPrompt(settings, editingModel) {
+        var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml");
         if (component.status == Component.Ready) {
             var dialog = component.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
+                    "roomSettings": settings,
+                    "editingModel": editingModel
+                });
             dialog.show();
             destroyOnClose(dialog);
         } else {
@@ -96,23 +86,37 @@ Pane {
         }
     }
 
+    background: null
+    padding: 0
+
+    FontMetrics {
+        id: fontMetrics
+
+    }
+    RoomDirectoryModel {
+        id: publicRooms
+
+    }
+    UserDirectoryModel {
+        id: userDirectory
+
+    }
     Component {
         id: readReceiptsDialog
 
         ReadReceipts {
         }
-
     }
-
     Shortcut {
         sequence: StandardKey.Quit
+
         onActivated: Qt.quit()
     }
-
     Shortcut {
         sequence: "Ctrl+K"
+
         onActivated: {
-            var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml")
+            var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml");
             if (component.status == Component.Ready) {
                 var quickSwitch = component.createObject(timelineRoot);
                 quickSwitch.open();
@@ -122,50 +126,49 @@ Pane {
             }
         }
     }
-
     Shortcut {
         // Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit
         sequences: ["Alt+A", "Ctrl+Shift+A"]
+
         onActivated: Rooms.nextRoomWithActivity()
     }
-
     Shortcut {
         sequence: "Ctrl+Down"
+
         onActivated: Rooms.nextRoom()
     }
-
     Shortcut {
         sequence: "Ctrl+Up"
+
         onActivated: Rooms.previousRoom()
     }
-
     Connections {
-        function onOpenLogoutDialog() {
-            var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml")
+        function onOpenJoinRoomDialog() {
+            var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml");
             if (component.status == Component.Ready) {
                 var dialog = component.createObject(timelineRoot);
-                dialog.open();
+                dialog.show();
                 destroyOnClose(dialog);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
         }
-
-        function onOpenJoinRoomDialog() {
-            var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml")
+        function onOpenLogoutDialog() {
+            var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml");
             if (component.status == Component.Ready) {
                 var dialog = component.createObject(timelineRoot);
-                dialog.show();
+                dialog.open();
                 destroyOnClose(dialog);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
         }
-
         function onShowRoomJoinPrompt(summary) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml")
+            var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml");
             if (component.status == Component.Ready) {
-                var dialog = component.createObject(timelineRoot, {"summary": summary});
+                var dialog = component.createObject(timelineRoot, {
+                        "summary": summary
+                    });
                 dialog.show();
                 destroyOnClose(dialog);
             } else {
@@ -175,12 +178,13 @@ Pane {
 
         target: Nheko
     }
-
     Connections {
         function onNewDeviceVerificationRequest(flow) {
-            var component = Qt.createComponent("qrc:/qml/device-verification/DeviceVerification.qml")
+            var component = Qt.createComponent("qrc:/qml/device-verification/DeviceVerification.qml");
             if (component.status == Component.Ready) {
-                var dialog = component.createObject(timelineRoot, {"flow": flow});
+                var dialog = component.createObject(timelineRoot, {
+                        "flow": flow
+                    });
                 dialog.show();
                 destroyOnClose(dialog);
             } else {
@@ -190,101 +194,71 @@ Pane {
 
         target: VerificationManager
     }
-
-    function destroyOnClose(obj) {
-        if (obj.closing != undefined) obj.closing.connect(() => obj.destroy(1000));
-        else if (obj.aboutToHide != undefined) obj.aboutToHide.connect(() => obj.destroy(1000));
-    }
-
-    function destroyOnClosed(obj) {
-        obj.aboutToHide.connect(() => obj.destroy(1000));
-    }
-
     Connections {
-        function onOpenProfile(profile) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml")
+        function onOpenInviteUsersDialog(invitees) {
+            var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml");
             if (component.status == Component.Ready) {
-                var userProfile = component.createObject(timelineRoot, {"profile": profile});
-                userProfile.show();
-                destroyOnClose(userProfile);
+                var dialog = component.createObject(timelineRoot, {
+                        "invitees": invitees
+                    });
+                dialog.show();
+                destroyOnClose(dialog);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
         }
-
-        function onShowImagePackSettings(room, packlist) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml")
-
+        function onOpenLeaveRoomDialog(roomid, reason) {
+            var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml");
             if (component.status == Component.Ready) {
-                var packSet = component.createObject(timelineRoot, {
-                    "room": room,
-                    "packlist": packlist
-                });
-                packSet.show();
-                destroyOnClose(packSet);
+                var dialog = component.createObject(timelineRoot, {
+                        "roomId": roomid,
+                        "reason": reason
+                    });
+                dialog.open();
+                destroyOnClose(dialog);
+            } else {
+                console.error("Failed to create component: " + component.errorString());
+            }
+        }
+        function onOpenProfile(profile) {
+            var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml");
+            if (component.status == Component.Ready) {
+                var userProfile = component.createObject(timelineRoot, {
+                        "profile": profile
+                    });
+                userProfile.show();
+                destroyOnClose(userProfile);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
         }
-
         function onOpenRoomMembersDialog(members, room) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml")
+            var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml");
             if (component.status == Component.Ready) {
                 var membersDialog = component.createObject(timelineRoot, {
-                    "members": members,
-                    "room": room
-                });
+                        "members": members,
+                        "room": room
+                    });
                 membersDialog.show();
                 destroyOnClose(membersDialog);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
-
         }
-
         function onOpenRoomSettingsDialog(settings) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml")
+            var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml");
             if (component.status == Component.Ready) {
                 var roomSettings = component.createObject(timelineRoot, {
-                    "roomSettings": settings
-                });
+                        "roomSettings": settings
+                    });
                 roomSettings.show();
                 destroyOnClose(roomSettings);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
-
-        }
-
-        function onOpenInviteUsersDialog(invitees) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml")
-            if (component.status == Component.Ready) {
-                var dialog = component.createObject(timelineRoot, {
-                    "invitees": invitees
-                });
-                dialog.show();
-                destroyOnClose(dialog);
-            } else {
-                console.error("Failed to create component: " + component.errorString());
-            }
         }
-
-        function onOpenLeaveRoomDialog(roomid, reason) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml")
-            if (component.status == Component.Ready) {
-                var dialog = component.createObject(timelineRoot, {
-                    "roomId": roomid,
-                    "reason": reason
-                });
-                dialog.open();
-                destroyOnClose(dialog);
-            } else {
-                console.error("Failed to create component: " + component.errorString());
-            }
-        }
-
         function onShowImageOverlay(room, eventId, url, originalWidth, proportionalHeight) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/ImageOverlay.qml")
+            var component = Qt.createComponent("qrc:/qml/dialogs/ImageOverlay.qml");
             if (component.status == Component.Ready) {
                 var dialog = component.createObject(timelineRoot, {
                         "room": room,
@@ -292,22 +266,33 @@ Pane {
                         "url": url,
                         "originalWidth": originalWidth ?? 0,
                         "proportionalHeight": proportionalHeight ?? 0
-                    }
-                );
+                    });
                 dialog.showFullScreen();
                 destroyOnClose(dialog);
             } else {
                 console.error("Failed to create component: " + component.errorString());
             }
         }
+        function onShowImagePackSettings(room, packlist) {
+            var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml");
+            if (component.status == Component.Ready) {
+                var packSet = component.createObject(timelineRoot, {
+                        "room": room,
+                        "packlist": packlist
+                    });
+                packSet.show();
+                destroyOnClose(packSet);
+            } else {
+                console.error("Failed to create component: " + component.errorString());
+            }
+        }
 
         target: TimelineManager
     }
-
     Connections {
         function onNewInviteState() {
             if (CallManager.haveCallInvite && Settings.mobileMode) {
-                var component = Qt.createComponent("qrc:/qml/voip/CallInvite.qml")
+                var component = Qt.createComponent("qrc:/qml/voip/CallInvite.qml");
                 if (component.status == Component.Ready) {
                     var dialog = component.createObject(timelineRoot);
                     dialog.open();
@@ -320,131 +305,110 @@ Pane {
 
         target: CallManager
     }
-
     SelfVerificationCheck {
     }
-
     InputDialog {
         id: uiaPassPrompt
 
         echoMode: TextInput.Password
-        title: UIA.title
         prompt: qsTr("Please enter your login password to continue:")
-        onAccepted: (t) => {
+        title: UIA.title
+
+        onAccepted: t => {
             return UIA.continuePassword(t);
         }
     }
-
     InputDialog {
         id: uiaEmailPrompt
 
-        title: UIA.title
         prompt: qsTr("Please enter a valid email address to continue:")
-        onAccepted: (t) => {
+        title: UIA.title
+
+        onAccepted: t => {
             return UIA.continueEmail(t);
         }
     }
-
     PhoneNumberInputDialog {
         id: uiaPhoneNumberPrompt
 
-        title: UIA.title
         prompt: qsTr("Please enter a valid phone number to continue:")
+        title: UIA.title
+
         onAccepted: (p, t) => {
             return UIA.continuePhoneNumber(p, t);
         }
     }
-
     InputDialog {
         id: uiaTokenPrompt
 
-        title: UIA.title
         prompt: qsTr("Please enter the token which has been sent to you:")
-        onAccepted: (t) => {
+        title: UIA.title
+
+        onAccepted: t => {
             return UIA.submit3pidToken(t);
         }
     }
-
     Platform.MessageDialog {
         id: uiaErrorDialog
 
         buttons: Platform.MessageDialog.Ok
     }
-
     Platform.MessageDialog {
         id: uiaConfirmationLinkDialog
 
         buttons: Platform.MessageDialog.Ok
         text: qsTr("Wait for the confirmation link to arrive, then continue.")
+
         onAccepted: UIA.continue3pidReceived()
     }
-
     Connections {
-        function onPassword() {
-            console.log("UIA: password needed");
-            uiaPassPrompt.show();
+        function onConfirm3pidToken() {
+            uiaConfirmationLinkDialog.open();
         }
-
         function onEmail() {
             uiaEmailPrompt.show();
         }
-
+        function onError(msg) {
+            uiaErrorDialog.text = msg;
+            uiaErrorDialog.open();
+        }
+        function onPassword() {
+            console.log("UIA: password needed");
+            uiaPassPrompt.show();
+        }
         function onPhoneNumber() {
             uiaPhoneNumberPrompt.show();
         }
-
         function onPrompt3pidToken() {
             uiaTokenPrompt.show();
         }
 
-        function onConfirm3pidToken() {
-            uiaConfirmationLinkDialog.open();
-        }
-
-        function onError(msg) {
-            uiaErrorDialog.text = msg;
-            uiaErrorDialog.open();
-        }
-
         target: UIA
     }
-
     StackView {
         id: mainWindow
 
-        anchors.fill: parent
-        initialItem: welcomePage
-
-        Transition {
-            id: reducedMotionTransitionExit
-            PropertyAnimation {
-                property: "opacity"
-                from: 1
-                to:0
-                duration: 200
-            }
-        }
-        Transition {
-            id: reducedMotionTransitionEnter
-            SequentialAnimation {
-                PropertyAction { property: "opacity"; value: 0 }
-                PauseAnimation { duration: 200 }
-                PropertyAnimation {
-                    property: "opacity"
-                    from: 0
-                    to:1
-                    duration: 200
-                }
-            }
-        }
+        property Transition popEnterOrg
+        property Transition popExitOrg
 
         // for some reason direct bindings to a hidden StackView don't work, so manually store and restore here.
         property Transition pushEnterOrg
         property Transition pushExitOrg
-        property Transition popEnterOrg
-        property Transition popExitOrg
         property Transition replaceEnterOrg
         property Transition replaceExitOrg
+
+        function updateTrans() {
+            pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg;
+            pushExit = Settings.reducedMotion ? reducedMotionTransitionExit : pushExitOrg;
+            popEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : popEnterOrg;
+            popExit = Settings.reducedMotion ? reducedMotionTransitionExit : popExitOrg;
+            replaceEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : replaceEnterOrg;
+            replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg;
+        }
+
+        anchors.fill: parent
+        initialItem: welcomePage
+
         Component.onCompleted: {
             pushEnterOrg = pushEnter;
             popEnterOrg = popEnter;
@@ -452,78 +416,94 @@ Pane {
             pushExitOrg = pushExit;
             popExitOrg = popExit;
             replaceExitOrg = replaceExit;
-
-            updateTrans()
+            updateTrans();
         }
 
-        function updateTrans() {
-            pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg;
-            pushExit = Settings.reducedMotion ? reducedMotionTransitionExit : pushExitOrg;
-            popEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : popEnterOrg;
-            popExit = Settings.reducedMotion ? reducedMotionTransitionExit : popExitOrg;
-            replaceEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : replaceEnterOrg;
-            replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg;
+        Transition {
+            id: reducedMotionTransitionExit
+
+            PropertyAnimation {
+                duration: 200
+                from: 1
+                property: "opacity"
+                to: 0
+            }
         }
+        Transition {
+            id: reducedMotionTransitionEnter
 
+            SequentialAnimation {
+                PropertyAction {
+                    property: "opacity"
+                    value: 0
+                }
+                PauseAnimation {
+                    duration: 200
+                }
+                PropertyAnimation {
+                    duration: 200
+                    from: 0
+                    property: "opacity"
+                    to: 1
+                }
+            }
+        }
         Connections {
-            target: Settings
             function onReducedMotionChanged() {
                 mainWindow.updateTrans();
             }
+
+            target: Settings
         }
     }
-
     Component {
         id: welcomePage
 
         WelcomePage {
         }
     }
-
     Component {
         id: chatPage
 
         ChatPage {
         }
     }
-
     Component {
         id: loginPage
 
         LoginPage {
         }
     }
-
     Component {
         id: registerPage
 
         RegisterPage {
         }
     }
-
     Component {
         id: userSettingsPage
 
         UserSettingsPage {
         }
-
     }
+    Snackbar {
+        id: snackbar
 
-
-    Snackbar { id: snackbar }
-
+    }
     Connections {
+        function onShowNotification(msg) {
+            snackbar.showNotification(msg);
+            console.log("New snack: " + msg);
+        }
         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);
+            mainWindow.replace(welcomePage, {}, loginPage, {
+                    "error": error
+                }, StackView.PopTransition);
         }
+
         target: MainWindow
     }
-
 }
diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml
index bb7ea5f0..80897ff9 100644
--- a/resources/qml/SelfVerificationCheck.qml
+++ b/resources/qml/SelfVerificationCheck.qml
@@ -10,22 +10,29 @@ import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Item {
-    visible: false
     enabled: false
+    visible: false
 
     Dialog {
         id: showRecoverKeyDialog
 
         property string recoveryKey: ""
 
-        parent: Overlay.overlay
         anchors.centerIn: parent
+        closePolicy: Popup.NoAutoClose
         height: content.height + implicitFooterHeight + implicitHeaderHeight
-        width: content.width
-        padding: 0
         modal: true
+        padding: 0
+        parent: Overlay.overlay
         standardButtons: Dialog.Ok
-        closePolicy: Popup.NoAutoClose
+        width: content.width
+
+        background: Rectangle {
+            border.color: Nheko.theme.separator
+            border.width: 1
+            color: palette.window
+            radius: Nheko.paddingSmall
+        }
 
         ColumnLayout {
             id: content
@@ -33,45 +40,33 @@ Item {
             spacing: 0
 
             Label {
+                Layout.fillWidth: true
                 Layout.margins: Nheko.paddingMedium
                 Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
-                Layout.fillWidth: true
-                text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!")
                 color: palette.text
+                text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!")
                 wrapMode: Text.Wrap
             }
-
             TextEdit {
-                Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
                 Layout.alignment: Qt.AlignHCenter
+                Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
+                color: palette.text
+                font.bold: true
                 horizontalAlignment: TextEdit.AlignHCenter
-                verticalAlignment: TextEdit.AlignVCenter
                 readOnly: true
                 selectByMouse: true
                 text: showRecoverKeyDialog.recoveryKey
-                color: palette.text
-                font.bold: true
+                verticalAlignment: TextEdit.AlignVCenter
                 wrapMode: TextEdit.Wrap
             }
-
         }
-
-        background: Rectangle {
-            color: palette.window
-            border.color: Nheko.theme.separator
-            border.width: 1
-            radius: Nheko.paddingSmall
-        }
-
     }
-
     P.MessageDialog {
         id: successDialog
 
         buttons: P.MessageDialog.Ok
         text: qsTr("Encryption setup successfully")
     }
-
     P.MessageDialog {
         id: failureDialog
 
@@ -80,85 +75,86 @@ Item {
         buttons: P.MessageDialog.Ok
         text: qsTr("Failed to setup encryption: %1").arg(errorMessage)
     }
-
     MainWindowDialog {
         id: bootstrapCrosssigning
 
+        background: Rectangle {
+            border.color: Nheko.theme.separator
+            border.width: 1
+            color: palette.window
+            radius: Nheko.paddingSmall
+        }
+
         onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked)
 
         GridLayout {
             id: grid
 
-            width: bootstrapCrosssigning.useableWidth
+            columnSpacing: 0
             columns: 2
             rowSpacing: 0
-            columnSpacing: 0
+            width: bootstrapCrosssigning.useableWidth
             z: 1
 
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignHCenter
                 Layout.columnSpan: 2
+                Layout.margins: Nheko.paddingMedium
+                color: palette.text
                 font.pointSize: fontMetrics.font.pointSize * 2
                 text: qsTr("Setup Encryption")
-                color: palette.text
                 wrapMode: Text.Wrap
             }
-
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignLeft
                 Layout.columnSpan: 2
+                Layout.margins: Nheko.paddingMedium
                 Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
-                text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!")
                 color: palette.text
+                text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!")
                 wrapMode: Text.Wrap
             }
-
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignLeft
                 Layout.columnSpan: 1
+                Layout.margins: Nheko.paddingMedium
                 Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
-                text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!"
                 color: palette.text
+                text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!"
                 wrapMode: Text.Wrap
             }
-
             Item {
-                Layout.margins: Nheko.paddingMedium
-                Layout.preferredHeight: storeSecretsOnline.height
                 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
                 Layout.fillWidth: true
+                Layout.margins: Nheko.paddingMedium
+                Layout.preferredHeight: storeSecretsOnline.height
 
                 ToggleButton {
                     id: storeSecretsOnline
 
                     checked: true
+
                     onClicked: console.log("Store secrets toggled: " + checked)
                 }
-
             }
-
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignLeft
                 Layout.columnSpan: 1
-                Layout.rowSpan: 2
+                Layout.margins: Nheko.paddingMedium
                 Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
-                visible: storeSecretsOnline.checked
-                text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)"
+                Layout.rowSpan: 2
                 color: palette.text
+                text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)"
+                visible: storeSecretsOnline.checked
                 wrapMode: Text.Wrap
             }
-
             Item {
+                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+                Layout.fillWidth: true
                 Layout.margins: Nheko.paddingMedium
-                Layout.topMargin: Nheko.paddingLarge
                 Layout.preferredHeight: storeSecretsOnline.height
-                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
                 Layout.rowSpan: usePassword.checked ? 1 : 2
-                Layout.fillWidth: true
+                Layout.topMargin: Nheko.paddingLarge
                 visible: storeSecretsOnline.checked
 
                 ToggleButton {
@@ -166,57 +162,43 @@ Item {
 
                     checked: false
                 }
-
             }
-
             MatrixTextField {
                 id: passwordField
 
-                Layout.margins: Nheko.paddingMedium
-                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
                 Layout.alignment: Qt.AlignLeft | Qt.AlignTop
                 Layout.columnSpan: 1
                 Layout.fillWidth: true
-                visible: storeSecretsOnline.checked && usePassword.checked
+                Layout.margins: Nheko.paddingMedium
+                Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
                 echoMode: TextInput.Password
+                visible: storeSecretsOnline.checked && usePassword.checked
             }
-
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignLeft
                 Layout.columnSpan: 1
+                Layout.margins: Nheko.paddingMedium
                 Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
-                text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages."
                 color: palette.text
+                text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages."
                 wrapMode: Text.Wrap
             }
-
             Item {
-                Layout.margins: Nheko.paddingMedium
-                Layout.preferredHeight: storeSecretsOnline.height
                 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
                 Layout.fillWidth: true
+                Layout.margins: Nheko.paddingMedium
+                Layout.preferredHeight: storeSecretsOnline.height
 
                 ToggleButton {
                     id: useOnlineKeyBackup
 
                     checked: true
+
                     onClicked: console.log("Online key backup toggled: " + checked)
                 }
-
             }
-
         }
-
-        background: Rectangle {
-            color: palette.window
-            border.color: Nheko.theme.separator
-            border.width: 1
-            radius: Nheko.paddingSmall
-        }
-
     }
-
     MainWindowDialog {
         id: verifyMasterKey
 
@@ -225,54 +207,61 @@ Item {
         GridLayout {
             id: masterGrid
 
-            width: verifyMasterKey.useableWidth
             columns: 1
+            width: verifyMasterKey.useableWidth
             z: 1
 
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignHCenter
+                Layout.margins: Nheko.paddingMedium
+                color: palette.text
                 //Layout.columnSpan: 2
                 font.pointSize: fontMetrics.font.pointSize * 2
                 text: qsTr("Activate Encryption")
-                color: palette.text
                 wrapMode: Text.Wrap
             }
-
             Label {
-                Layout.margins: Nheko.paddingMedium
                 Layout.alignment: Qt.AlignLeft
+                Layout.margins: Nheko.paddingMedium
                 //Layout.columnSpan: 2
                 Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
-                text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
                 color: palette.text
+                text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
                 wrapMode: Text.Wrap
             }
-
             FlatButton {
                 Layout.alignment: Qt.AlignHCenter
                 text: qsTr("verify")
+
                 onClicked: {
                     SelfVerificationStatus.verifyMasterKey();
                     verifyMasterKey.close();
                 }
             }
-
             FlatButton {
-                visible: SelfVerificationStatus.hasSSSS
                 Layout.alignment: Qt.AlignHCenter
                 text: qsTr("enter passphrase")
+                visible: SelfVerificationStatus.hasSSSS
+
                 onClicked: {
                     SelfVerificationStatus.verifyMasterKeyWithPassphrase();
                     verifyMasterKey.close();
                 }
             }
-
         }
-
     }
-
     Connections {
+        function onSetupCompleted() {
+            successDialog.open();
+        }
+        function onSetupFailed(m) {
+            failureDialog.errorMessage = m;
+            failureDialog.open();
+        }
+        function onShowRecoveryKey(key) {
+            showRecoverKeyDialog.recoveryKey = key;
+            showRecoverKeyDialog.open();
+        }
         function onStatusChanged() {
             console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
             if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) {
@@ -285,21 +274,6 @@ Item {
             }
         }
 
-        function onShowRecoveryKey(key) {
-            showRecoverKeyDialog.recoveryKey = key;
-            showRecoverKeyDialog.open();
-        }
-
-        function onSetupCompleted() {
-            successDialog.open();
-        }
-
-        function onSetupFailed(m) {
-            failureDialog.errorMessage = m;
-            failureDialog.open();
-        }
-
         target: SelfVerificationStatus
     }
-
 }
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 862f9d7a..4a305ac5 100644
--- a/resources/qml/StatusIndicator.qml
+++ b/resources/qml/StatusIndicator.qml
@@ -9,15 +9,9 @@ import im.nheko 1.0
 ImageButton {
     id: indicator
 
-    required property int status
     required property string eventId
+    required property int status
 
-    width: 16
-    height: 16
-    hoverEnabled: true
-    changeColorOnHover: (status == MtxEvent.Read)
-    cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor
-    ToolTip.visible: hovered && status != MtxEvent.Empty
     ToolTip.text: {
         switch (status) {
         case MtxEvent.Failed:
@@ -32,11 +26,11 @@ ImageButton {
             return "";
         }
     }
-    onClicked: {
-        if (status == MtxEvent.Read)
-            room.showReadReceipts(eventId);
-
-    }
+    ToolTip.visible: hovered && status != MtxEvent.Empty
+    changeColorOnHover: (status == MtxEvent.Read)
+    cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor
+    height: 16
+    hoverEnabled: true
     image: {
         switch (status) {
         case MtxEvent.Failed:
@@ -51,4 +45,10 @@ ImageButton {
             return "";
         }
     }
+    width: 16
+
+    onClicked: {
+        if (status == MtxEvent.Read)
+            room.showReadReceipts(eventId);
+    }
 }
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 07cb5ce2..a064bd15 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -13,72 +13,45 @@ import im.nheko 1.0
 AbstractButton {
     id: r
 
-    required property double proportionalHeight
-    required property int type
-    required property string typeString
-    required property int originalWidth
     required property string blurhash
     required property string body
-    required property string formattedBody
+    required property string callType
+    required property int duration
+    required property int encryptionError
     required property string eventId
     required property string filename
     required property string filesize
-    required property string url
-    required property string thumbnailUrl
-    required property bool isOnlyEmoji
-    required property bool isSender
-    required property bool isEncrypted
+    required property string formattedBody
+    required property int index
     required property bool isEditable
     required property bool isEdited
+    required property bool isEncrypted
+    required property bool isOnlyEmoji
+    required property bool isSender
     required property bool isStateEvent
+    required property int notificationlevel
+    required property int originalWidth
+    required property double proportionalHeight
+    required property var reactions
+    required property int relatedEventCacheBuster
     required property string replyTo
+    required property string roomName
+    required property string roomTopic
+    required property int status
     required property string threadId
+    required property string thumbnailUrl
+    required property var timestamp
+    required property int trustlevel
+    required property int type
+    required property string typeString
+    required property string url
     required property string userId
     required property string userName
-    required property string roomTopic
-    required property string roomName
-    required property string callType
-    required property var reactions
-    required property int trustlevel
-    required property int notificationlevel
-    required property int encryptionError
-    required property int duration
-    required property var timestamp
-    required property int status
-    required property int index
-    required property int relatedEventCacheBuster
 
+    height: row.height + (reactionRow.height > 0 ? reactionRow.height - 2 : 0) + unreadRow.height
     hoverEnabled: true
-
     width: parent.width
-    height: row.height+(reactionRow.height > 0 ? reactionRow.height-2 : 0 )+unreadRow.height
-
-    Rectangle {
-        color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent"
-        anchors.fill: parent
-        // this looks better without margins
-        TapHandler {
-            acceptedButtons: Qt.RightButton
-            onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
-            gesturePolicy: TapHandler.ReleaseWithinBounds
-            acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
-        }
-    }
 
-
-    onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
-    onDoubleClicked: room.reply = eventId
-
-    DragHandler {
-        id: draghandler
-        yAxis.enabled: false
-        xAxis.maximum: 100
-        xAxis.minimum: -100
-        onActiveChanged: {
-            if(!active && (x < -70 || x > 70))
-                room.reply = eventId
-        }
-    }
     states: State {
         name: "dragging"
         when: draghandler.active
@@ -86,265 +59,292 @@ AbstractButton {
     transitions: Transition {
         from: "dragging"
         to: ""
+
         PropertyAnimation {
-            target: r
-            properties: "x"
+            duration: 100
             easing.type: Easing.InOutQuad
+            properties: "x"
+            target: r
             to: 0
-            duration: 100
         }
     }
 
     onClicked: {
-        let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX-row.x-msg.x, pressY-row.y-msg.y-contentItem.y);
+        let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX - row.x - msg.x, pressY - row.y - msg.y - contentItem.y);
         if (link) {
-            Nheko.openLink(link)
+            Nheko.openLink(link);
         }
     }
+    onDoubleClicked: room.reply = eventId
+    onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+
+    Rectangle {
+        anchors.fill: parent
+        color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent"
+
+        // this looks better without margins
+        TapHandler {
+            acceptedButtons: Qt.RightButton
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+            gesturePolicy: TapHandler.ReleaseWithinBounds
+
+            onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+        }
+    }
+    DragHandler {
+        id: draghandler
+
+        xAxis.maximum: 100
+        xAxis.minimum: -100
+        yAxis.enabled: false
 
+        onActiveChanged: {
+            if (!active && (x < -70 || x > 70))
+                room.reply = eventId;
+        }
+    }
     AbstractButton {
-        anchors.leftMargin: Settings.smallAvatars? 0 : (Nheko.avatarSize + 8) // align bubble with section header
+        ToolTip.delay: Nheko.tooltipDelay
+        ToolTip.text: qsTr("Part of a thread")
+        ToolTip.visible: hovered
         anchors.left: parent.left
+        anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header
+        height: parent.height
         visible: threadId
         width: 4
-        height: parent.height
+
+        onClicked: room.thread = threadId
 
         Rectangle {
             id: threadLine
 
-            color: TimelineManager.userColor(threadId, palette.base)
             anchors.fill: parent
+            color: TimelineManager.userColor(threadId, palette.base)
         }
-
-        ToolTip.visible: hovered
-        ToolTip.delay: Nheko.tooltipDelay
-        ToolTip.text: qsTr("Part of a thread")
-        onClicked: room.thread = threadId
     }
-
     Rectangle {
         id: row
-        property bool bubbleOnRight : isSender && Settings.bubbles
-        anchors.leftMargin: (isStateEvent || Settings.smallAvatars? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header
-        anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left
-        anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right
-        anchors.horizontalCenter: isStateEvent? parent.horizontalCenter : undefined
-        property int maxWidth: (parent.width-(Settings.smallAvatars || isStateEvent? 0 : Nheko.avatarSize+8))*(Settings.bubbles && !isStateEvent? 0.9 : 1)
-        width: Settings.bubbles? Math.min(maxWidth,Math.max(reply.implicitWidth+8,contentItem.implicitWidth+metadata.width+20)) : maxWidth
-        height: msg.height+msg.anchors.margins*2
 
-        property color userColor: TimelineManager.userColor(userId, palette.base)
         property color bgColor: palette.base
+        property bool bubbleOnRight: isSender && Settings.bubbles
+        property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1)
+        property color userColor: TimelineManager.userColor(userId, palette.base)
+
+        anchors.horizontalCenter: isStateEvent ? parent.horizontalCenter : undefined
+        anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left
+        anchors.leftMargin: (isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header
+        anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right
+        border.color: Nheko.theme.red
+        border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0
         color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000"
+        height: msg.height + msg.anchors.margins * 2
         radius: 4
-        border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0
-        border.color: Nheko.theme.red
+        width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth
 
         GridLayout {
+            id: msg
+
+            columnSpacing: 2
+            columns: Settings.bubbles ? 1 : 2
+            rowSpacing: 0
+            rows: Settings.bubbles ? 3 : 2
+
             anchors {
                 left: parent.left
-                top: parent.top
-                right: parent.right
-                margins: (Settings.bubbles && ! isStateEvent)? 4 : 2
                 leftMargin: 4
+                margins: (Settings.bubbles && !isStateEvent) ? 4 : 2
+                right: parent.right
                 rightMargin: 4
+                top: parent.top
             }
-            id: msg
-            rowSpacing: 0
-            columnSpacing: 2
-            columns: Settings.bubbles? 1 : 2
-            rows: Settings.bubbles? 3 : 2
 
             // fancy reply, if this is a reply
             Reply {
-                Layout.row: 0
-                Layout.column: 0
-                Layout.fillWidth: true
-                Layout.maximumWidth: Settings.bubbles? Number.MAX_VALUE : implicitWidth
-                Layout.bottomMargin: visible? 2 : 0
-                Layout.preferredHeight: height
                 id: reply
 
                 function fromModel(role) {
                     return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null;
                 }
-                visible: replyTo
-                userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base)
+
+                Layout.bottomMargin: visible ? 2 : 0
+                Layout.column: 0
+                Layout.fillWidth: true
+                Layout.maximumWidth: Settings.bubbles ? Number.MAX_VALUE : implicitWidth
+                Layout.preferredHeight: height
+                Layout.row: 0
                 blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? ""
                 body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? ""
-                formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? ""
+                callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
+                duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0
+                encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0
                 eventId: fromModel(Room.EventId) ?? ""
                 filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? ""
                 filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? ""
+                formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? ""
+                isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false
+                isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false
+                originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0
                 proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1
+                relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
+                roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
+                roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
+                thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
                 type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage
                 typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? ""
                 url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? ""
-                originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0
-                isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false
-                isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false
+                userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base)
                 userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
                 userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
-                thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
-                duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0
-                roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
-                roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
-                callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
-                encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0
-                relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
+                visible: replyTo
             }
 
             // actual message content
             MessageDelegate {
-                Layout.row: 1
+                id: contentItem
+
                 Layout.column: 0
                 Layout.fillWidth: true
                 Layout.preferredHeight: height
-                id: contentItem
-
+                Layout.row: 1
                 blurhash: r.blurhash
                 body: r.body
-                formattedBody: r.formattedBody
+                callType: r.callType
+                duration: r.duration
+                encryptionError: r.encryptionError
                 eventId: r.eventId
                 filename: r.filename
                 filesize: r.filesize
+                formattedBody: r.formattedBody
+                isOnlyEmoji: r.isOnlyEmoji
+                isReply: false
+                isStateEvent: r.isStateEvent
+                metadataWidth: metadata.width
+                originalWidth: r.originalWidth
                 proportionalHeight: r.proportionalHeight
+                relatedEventCacheBuster: r.relatedEventCacheBuster
+                roomName: r.roomName
+                roomTopic: r.roomTopic
+                thumbnailUrl: r.thumbnailUrl
                 type: r.type
                 typeString: r.typeString ?? ""
                 url: r.url
-                thumbnailUrl: r.thumbnailUrl
-                duration: r.duration
-                originalWidth: r.originalWidth
-                isOnlyEmoji: r.isOnlyEmoji
-                isStateEvent: r.isStateEvent
                 userId: r.userId
                 userName: r.userName
-                roomTopic: r.roomTopic
-                roomName: r.roomName
-                callType: r.callType
-                encryptionError: r.encryptionError
-                relatedEventCacheBuster: r.relatedEventCacheBuster
-                isReply: false
-                metadataWidth: metadata.width
             }
-
             Row {
                 id: metadata
-                Layout.column: Settings.bubbles? 0 : 1
-                Layout.row: Settings.bubbles? 2 : 0
-                Layout.rowSpan: Settings.bubbles? 1 : 2
-                Layout.bottomMargin: -2
-                Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles)? -height-Layout.bottomMargin : 0
+
+                property int iconSize: Math.floor(fontMetrics.ascent * scaling)
+                property double scaling: Settings.bubbles ? 0.75 : 1
+
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
+                Layout.bottomMargin: -2
+                Layout.column: Settings.bubbles ? 0 : 1
                 Layout.preferredWidth: implicitWidth
-                visible: !isStateEvent
+                Layout.row: Settings.bubbles ? 2 : 0
+                Layout.rowSpan: Settings.bubbles ? 1 : 2
+                Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles) ? -height - Layout.bottomMargin : 0
                 spacing: 2
-
-                property double scaling: Settings.bubbles? 0.75 : 1
-
-                property int iconSize: Math.floor(fontMetrics.ascent*scaling)
+                visible: !isStateEvent
 
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
+                    anchors.verticalCenter: ts.verticalCenter
+                    eventId: r.eventId
                     height: parent.iconSize
-                    width: parent.iconSize
                     status: r.status
-                    eventId: r.eventId
-                    anchors.verticalCenter: ts.verticalCenter
+                    width: parent.iconSize
                 }
-
                 Image {
-                    visible: isEdited || eventId == room.edit
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    height: parent.iconSize
-                    width: parent.iconSize
-                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
-                    sourceSize.height: parent.iconSize * Screen.devicePixelRatio
-                    source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText)
-                    ToolTip.visible: editHovered.hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: qsTr("Edited")
+                    ToolTip.visible: editHovered.hovered
                     anchors.verticalCenter: ts.verticalCenter
+                    height: parent.iconSize
+                    source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText)
+                    sourceSize.height: parent.iconSize * Screen.devicePixelRatio
+                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
+                    visible: isEdited || eventId == room.edit
+                    width: parent.iconSize
 
                     HoverHandler {
                         id: editHovered
-                    }
 
+                    }
                 }
-
                 ImageButton {
-                    visible: threadId
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    height: parent.iconSize
-                    width: parent.iconSize
-                    image: ":/icons/icons/ui/thread.svg"
-                    buttonTextColor: TimelineManager.userColor(threadId, palette.base)
-                    ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: qsTr("Part of a thread")
+                    ToolTip.visible: hovered
                     anchors.verticalCenter: ts.verticalCenter
+                    buttonTextColor: TimelineManager.userColor(threadId, palette.base)
+                    height: parent.iconSize
+                    image: ":/icons/icons/ui/thread.svg"
+                    visible: threadId
+                    width: parent.iconSize
+
                     onClicked: room.thread = threadId
                 }
-
                 EncryptionIndicator {
-                    visible: room.isEncrypted
-                    encrypted: isEncrypted
-                    trust: trustlevel
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
+                    anchors.verticalCenter: ts.verticalCenter
+                    encrypted: isEncrypted
                     height: parent.iconSize
-                    width: parent.iconSize
-                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
                     sourceSize.height: parent.iconSize * Screen.devicePixelRatio
-                    anchors.verticalCenter: ts.verticalCenter
+                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
+                    trust: trustlevel
+                    visible: room.isEncrypted
+                    width: parent.iconSize
                 }
-
                 Label {
                     id: ts
+
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
                     Layout.preferredWidth: implicitWidth
-                    text: timestamp.toLocaleTimeString(Locale.ShortFormat)
-                    color: palette.inactive.text
-                    ToolTip.visible: ma.hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate)
-                    font.pointSize: fontMetrics.font.pointSize*parent.scaling
+                    ToolTip.visible: ma.hovered
+                    color: palette.inactive.text
+                    font.pointSize: fontMetrics.font.pointSize * parent.scaling
+                    text: timestamp.toLocaleTimeString(Locale.ShortFormat)
+
                     HoverHandler {
                         id: ma
-                    }
 
+                    }
                 }
             }
         }
     }
-
     Reactions {
+        id: reactionRow
+
+        eventId: r.eventId
+        layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight
+        reactions: r.reactions
+        width: row.maxWidth
+
         anchors {
+            left: row.bubbleOnRight ? undefined : row.left
+            right: row.bubbleOnRight ? row.right : undefined
             top: row.bottom
             topMargin: -4
-            left: row.bubbleOnRight? undefined : row.left
-            right: row.bubbleOnRight? row.right : undefined
         }
-        width: row.maxWidth
-        layoutDirection: row.bubbleOnRight? Qt.RightToLeft : Qt.LeftToRight
-
-        id: reactionRow
-
-        reactions: r.reactions
-        eventId: r.eventId
     }
-
     Rectangle {
         id: unreadRow
+
+        color: palette.highlight
+        height: visible ? 3 : 0
+        visible: (r.index > 0 && (room.fullyReadEventId == r.eventId))
+
         anchors {
-            top: reactionRow.bottom
-            topMargin: 5
             left: parent.left
             right: parent.right
+            top: reactionRow.bottom
+            topMargin: 5
         }
-        color: palette.highlight
-        
-        visible: (r.index > 0 && (room.fullyReadEventId == r.eventId))
-        height: visible ? 3 : 0
-
     }
 }
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 8fc567f2..24489d0b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -20,86 +20,85 @@ import im.nheko.EmojiModel 1.0
 Item {
     id: timelineView
 
+    required property PrivacyScreen privacyScreen
     property var room: null
     property var roomPreview: null
-    property bool showBackButton: false
     property bool shouldEffectsRun: false
-    required property PrivacyScreen privacyScreen
-    clip: true
-
-    onRoomChanged: if (room != null) room.triggerSpecialEffects()
-
-    StickerPicker {
-        id: emojiPopup
+    property bool showBackButton: false
 
-        emoji: true
-    }
+    clip: true
 
     // focus message input on key press, but not on Ctrl-C and such.
-    Keys.onPressed: (event) => {
+    Keys.onPressed: event => {
         if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) {
             TimelineManager.focusMessageInput();
             room.input.setText(room.input.text + event.text);
         }
     }
+    onRoomChanged: if (room != null)
+        room.triggerSpecialEffects()
+
+    StickerPicker {
+        id: emojiPopup
 
+        emoji: true
+    }
     Shortcut {
         sequence: StandardKey.Close
+
         onActivated: Rooms.resetCurrentRoom()
     }
-
     Label {
-        visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
         anchors.centerIn: parent
-        text: qsTr("No room open")
         font.pointSize: 24
+        text: qsTr("No room open")
+        visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
     }
-
     Spinner {
-        visible: TimelineManager.isInitialSync
         anchors.centerIn: parent
         foreground: palette.mid
-        running: TimelineManager.isInitialSync
         // height is somewhat arbitrary here... don't set width because width scales w/ height
         height: parent.height / 16
-        z: 3
         opacity: hh.hovered ? 0.3 : 1
+        running: TimelineManager.isInitialSync
+        visible: TimelineManager.isInitialSync
+        z: 3
 
-        Behavior on opacity {
-            NumberAnimation { duration: 100; }
+        Behavior on opacity  {
+            NumberAnimation {
+                duration: 100
+            }
         }
 
         HoverHandler {
             id: hh
+
         }
     }
-
     ColumnLayout {
         id: timelineLayout
 
-        visible: room != null && !room.isSpace
-        enabled: visible
         anchors.fill: parent
+        enabled: visible
         spacing: 0
+        visible: room != null && !room.isSpace
 
         TopBar {
             id: topBar
 
             showBackButton: timelineView.showBackButton
         }
-
         Rectangle {
             Layout.fillWidth: true
+            color: Nheko.theme.separator
             height: 1
             z: 3
-            color: Nheko.theme.separator
         }
-
         Rectangle {
             id: msgView
 
-            Layout.fillWidth: true
             Layout.fillHeight: true
+            Layout.fillWidth: true
             color: palette.base
 
             ColumnLayout {
@@ -118,143 +117,121 @@ Item {
 
                         target: timelineView
                     }
-
                     MessageView {
+                        Layout.fillWidth: true
                         implicitHeight: msgView.height - typingIndicator.height
                         searchString: topBar.searchString
-                        Layout.fillWidth: true
                     }
-
                     Loader {
                         source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
+
                         onLoaded: TimelineManager.setVideoCallItem()
                     }
-
                 }
-
                 TypingIndicator {
                     id: typingIndicator
-                }
 
+                }
             }
-
         }
-
         CallInviteBar {
             id: callInviteBar
 
             Layout.fillWidth: true
             z: 3
         }
-
         ActiveCallBar {
             Layout.fillWidth: true
             z: 3
         }
-
         Rectangle {
             Layout.fillWidth: true
-            z: 3
-            height: 1
             color: Nheko.theme.separator
+            height: 1
+            z: 3
         }
-
-
         UploadBox {
         }
-
         MessageInputWarning {
             text: qsTr("You are about to notify the whole room")
             visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
         }
-
         MessageInputWarning {
             text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "")
             visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false
         }
-
         MessageInputWarning {
+            bubbleColor: Nheko.theme.orange
             text: qsTr("/%1 looks like an incomplete command. To send it anyway, add a space to the end of your message.").arg(room ? room.input.currentCommand : "")
             visible: room ? room.input.containsIncompleteCommand : false
-            bubbleColor: Nheko.theme.orange
         }
-
         ReplyPopup {
         }
-
         MessageInput {
         }
-
     }
-
     ColumnLayout {
         id: preview
 
+        property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
+        property string reason: roomPreview ? roomPreview.reason : ""
         property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
         property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
         property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "")
-        property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
-        property string reason: roomPreview ? roomPreview.reason : ""
 
-        visible: room != null && room.isSpace || roomPreview != null
-        enabled: visible
         anchors.fill: parent
         anchors.margins: Nheko.paddingLarge
+        enabled: visible
         spacing: Nheko.paddingLarge
+        visible: room != null && room.isSpace || roomPreview != null
 
         Item {
             Layout.fillHeight: true
         }
-
         Avatar {
-            url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
-            roomid: parent.roomId
+            Layout.alignment: Qt.AlignHCenter
             displayName: parent.roomName
+            enabled: false
             height: 130
+            roomid: parent.roomId
+            url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
             width: 130
-            Layout.alignment: Qt.AlignHCenter
-            enabled: false
         }
-
         RowLayout {
-            spacing: Nheko.paddingMedium
             Layout.alignment: Qt.AlignHCenter
+            spacing: Nheko.paddingMedium
 
             MatrixText {
-                text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName
                 font.pixelSize: 24
+                text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName
             }
-
             ImageButton {
+                ToolTip.text: qsTr("Settings")
+                ToolTip.visible: hovered
+                hoverEnabled: true
                 image: ":/icons/icons/ui/settings.svg"
                 visible: !!room
-                hoverEnabled: true
-                ToolTip.visible: hovered
-                ToolTip.text: qsTr("Settings")
+
                 onClicked: TimelineManager.openRoomSettings(room.roomId)
             }
-
         }
-
         RowLayout {
-            visible: !!room
-            spacing: Nheko.paddingMedium
             Layout.alignment: Qt.AlignHCenter
+            spacing: Nheko.paddingMedium
+            visible: !!room
 
             MatrixText {
                 text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0)
             }
-
             ImageButton {
-                image: ":/icons/icons/ui/people.svg"
-                hoverEnabled: true
-                ToolTip.visible: hovered
                 ToolTip.text: qsTr("View members of %1").arg(room ? room.roomName : "")
+                ToolTip.visible: hovered
+                hoverEnabled: true
+                image: ":/icons/icons/ui/people.svg"
+
                 onClicked: TimelineManager.openRoomMembers(room)
             }
-
         }
-
         ScrollView {
             Layout.alignment: Qt.AlignHCenter
             Layout.fillWidth: true
@@ -262,54 +239,53 @@ Item {
             Layout.rightMargin: Nheko.paddingLarge
 
             TextArea {
-                text: (roomPreview?.isFetched ?? false) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.")
-                wrapMode: TextEdit.WordWrap
-                textFormat: TextEdit.RichText
-                readOnly: true
                 background: null
-                selectByMouse: true
                 horizontalAlignment: TextEdit.AlignHCenter
+                readOnly: true
+                selectByMouse: true
+                text: (roomPreview?.isFetched ?? false) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.")
+                textFormat: TextEdit.RichText
+                wrapMode: TextEdit.WordWrap
+
                 onLinkActivated: Nheko.openLink(link)
 
                 CursorShape {
                     anchors.fill: parent
                     cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
                 }
-
             }
-
         }
-
         FlatButton {
-            visible: roomPreview && !roomPreview.isInvite
             Layout.alignment: Qt.AlignHCenter
             text: qsTr("join the conversation")
+            visible: roomPreview && !roomPreview.isInvite
+
             onClicked: Rooms.joinPreview(roomPreview.roomid)
         }
-
         FlatButton {
-            visible: roomPreview && roomPreview.isInvite
             Layout.alignment: Qt.AlignHCenter
             text: qsTr("accept invite")
+            visible: roomPreview && roomPreview.isInvite
+
             onClicked: Rooms.acceptInvite(roomPreview.roomid)
         }
-
         FlatButton {
-            visible: roomPreview && roomPreview.isInvite
             Layout.alignment: Qt.AlignHCenter
             text: qsTr("decline invite")
+            visible: roomPreview && roomPreview.isInvite
+
             onClicked: Rooms.declineInvite(roomPreview.roomid)
         }
-
         FlatButton {
-            visible: !!room
             Layout.alignment: Qt.AlignHCenter
             text: qsTr("leave")
+            visible: !!room
+
             onClicked: TimelineManager.openLeaveRoomDialog(room.roomId)
         }
-
         ScrollView {
             id: reasonField
+
             property bool showReason: false
 
             Layout.alignment: Qt.AlignHCenter
@@ -319,17 +295,15 @@ Item {
             visible: preview.reason !== "" && showReason
 
             TextArea {
-                text: TimelineManager.escapeEmoji(preview.reason)
-                wrapMode: TextEdit.WordWrap
-                textFormat: TextEdit.RichText
-                readOnly: true
                 background: null
-                selectByMouse: true
                 horizontalAlignment: TextEdit.AlignHCenter
+                readOnly: true
+                selectByMouse: true
+                text: TimelineManager.escapeEmoji(preview.reason)
+                textFormat: TextEdit.RichText
+                wrapMode: TextEdit.WordWrap
             }
-
         }
-
         Button {
             id: showReasonButton
 
@@ -337,76 +311,94 @@ Item {
             //Layout.fillWidth: true
             Layout.leftMargin: Nheko.paddingLarge
             Layout.rightMargin: Nheko.paddingLarge
-
-            visible: preview.reason !== ""
             text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason")
+            visible: preview.reason !== ""
+
             onClicked: {
                 reasonField.showReason = !reasonField.showReason;
             }
         }
-
         Item {
-            visible: room != null
             Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2)
+            visible: room != null
         }
-
         Item {
             Layout.fillHeight: true
         }
-
     }
-
     ImageButton {
         id: backToRoomsButton
 
-        anchors.top: parent.top
+        ToolTip.text: qsTr("Back to room list")
+        ToolTip.visible: hovered
         anchors.left: parent.left
         anchors.margins: Nheko.paddingMedium
-        width: Nheko.avatarSize
-        height: Nheko.avatarSize
-        visible: (room == null || room.isSpace) && showBackButton
+        anchors.top: parent.top
         enabled: visible
+        height: Nheko.avatarSize
         image: ":/icons/icons/ui/angle-arrow-left.svg"
-        ToolTip.visible: hovered
-        ToolTip.text: qsTr("Back to room list")
+        visible: (room == null || room.isSpace) && showBackButton
+        width: Nheko.avatarSize
+
         onClicked: Rooms.resetCurrentRoom()
     }
-
     TimelineEffects {
         id: timelineEffects
 
         anchors.fill: parent
     }
-
     NhekoDropArea {
         anchors.fill: parent
         roomid: room ? room.roomId : ""
     }
-
     Timer {
         id: effectsTimer
-        onTriggered: shouldEffectsRun = false;
+
         interval: timelineEffects.maxLifespan
         repeat: false
         running: false
-    }
 
+        onTriggered: shouldEffectsRun = false
+    }
     Connections {
+        function onConfetti() {
+            if (!Settings.fancyEffects)
+                return;
+            shouldEffectsRun = true;
+            timelineEffects.pulseConfetti();
+            room.markSpecialEffectsDone();
+        }
+        function onConfettiDone() {
+            if (!Settings.fancyEffects)
+                return;
+            effectsTimer.restart();
+        }
         function onOpenReadReceiptsDialog(rr) {
             var dialog = readReceiptsDialog.createObject(timelineRoot, {
-                "readReceipts": rr,
-                "room": room
-            });
+                    "readReceipts": rr,
+                    "room": room
+                });
             dialog.show();
             timelineRoot.destroyOnClose(dialog);
         }
-
+        function onRainfall() {
+            if (!Settings.fancyEffects)
+                return;
+            shouldEffectsRun = true;
+            timelineEffects.pulseRainfall();
+            room.markSpecialEffectsDone();
+        }
+        function onRainfallDone() {
+            if (!Settings.fancyEffects)
+                return;
+            effectsTimer.restart();
+        }
         function onShowRawMessageDialog(rawMessage) {
-            var component = Qt.createComponent("qrc:/qml/dialogs/RawMessageDialog.qml")
+            var component = Qt.createComponent("qrc:/qml/dialogs/RawMessageDialog.qml");
             if (component.status == Component.Ready) {
                 var dialog = component.createObject(timelineRoot, {
-                    "rawMessage": rawMessage
-                });
+                        "rawMessage": rawMessage
+                    });
                 dialog.show();
                 timelineRoot.destroyOnClose(dialog);
             } else {
@@ -414,43 +406,6 @@ Item {
             }
         }
 
-        function onConfetti()
-        {
-            if (!Settings.fancyEffects)
-                return
-
-            shouldEffectsRun = true;
-            timelineEffects.pulseConfetti()
-            room.markSpecialEffectsDone()
-        }
-
-        function onConfettiDone()
-        {
-            if (!Settings.fancyEffects)
-                return
-
-            effectsTimer.restart();
-        }
-
-        function onRainfall()
-        {
-            if (!Settings.fancyEffects)
-                return
-
-            shouldEffectsRun = true;
-            timelineEffects.pulseRainfall()
-            room.markSpecialEffectsDone()
-        }
-
-        function onRainfallDone()
-        {
-            if (!Settings.fancyEffects)
-                return
-
-            effectsTimer.restart();
-        }
-
         target: room
     }
-
 }
diff --git a/resources/qml/ToggleButton.qml b/resources/qml/ToggleButton.qml
index 66902bfd..f3bd5cce 100644
--- a/resources/qml/ToggleButton.qml
+++ b/resources/qml/ToggleButton.qml
@@ -11,17 +11,44 @@ Switch {
     id: toggleButton
 
     implicitWidth: indicatorItem.width
-
     state: checked ? "on" : "off"
+
+    indicator: Item {
+        id: indicatorItem
+
+        implicitHeight: 24
+        implicitWidth: 48
+        y: parent.height / 2 - height / 2
+
+        Rectangle {
+            id: track
+
+            color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6)
+            height: parent.height * 0.6
+            radius: height / 2
+            width: parent.width - height
+            x: radius
+            y: parent.height / 2 - height / 2
+        }
+        Rectangle {
+            id: handle
+
+            border.color: "#767676"
+            color: palette.button
+            height: width
+            radius: width / 2
+            width: parent.height * 0.9
+            y: parent.height / 2 - height / 2
+        }
+    }
     states: [
         State {
             name: "off"
 
             PropertyChanges {
-                target: track
                 border.color: "#767676"
+                target: track
             }
-
             PropertyChanges {
                 target: handle
                 x: 0
@@ -31,10 +58,9 @@ Switch {
             name: "on"
 
             PropertyChanges {
-                target: track
                 border.color: palette.highlight
+                target: track
             }
-
             PropertyChanges {
                 target: handle
                 x: indicatorItem.width - handle.width
@@ -43,55 +69,22 @@ Switch {
     ]
     transitions: [
         Transition {
-            to: "off"
             reversible: true
+            to: "off"
 
             ParallelAnimation {
                 NumberAnimation {
-                    target: handle
-                    property: "x"
                     duration: 200
                     easing.type: Easing.InOutQuad
+                    property: "x"
+                    target: handle
                 }
-
                 ColorAnimation {
-                    target: track
-                    properties: "color,border.color"
                     duration: 200
+                    properties: "color,border.color"
+                    target: track
                 }
             }
         }
     ]
-
-    indicator: Item {
-        id: indicatorItem
-
-        implicitWidth: 48
-        implicitHeight: 24
-        y: parent.height / 2 - height / 2
-
-        Rectangle {
-            id: track
-
-            height: parent.height * 0.6
-            radius: height / 2
-            width: parent.width - height
-            x: radius
-            y: parent.height / 2 - height / 2
-            color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6)
-        }
-
-        Rectangle {
-            id: handle
-
-            y: parent.height / 2 - height / 2
-            width: parent.height * 0.9
-            height: width
-            radius: width / 2
-            color: palette.button
-            border.color: "#767676"
-        }
-
-    }
-
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index a54c5ed7..3f2d8d2a 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -8,212 +8,142 @@ import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.15
 import im.nheko 1.0
-
 import "./delegates"
 
 Pane {
     id: topBar
 
-    property bool showBackButton: false
-    property string roomName: room ? room.roomName : qsTr("No room selected")
-    property string roomId: room ? room.roomId : ""
     property string avatarUrl: room ? room.roomAvatarUrl : ""
-    property string roomTopic: room ? room.roomTopic : ""
-    property bool isEncrypted: room ? room.isEncrypted : false
-    property int trustlevel: room ? room.trustlevel : Crypto.Unverified
-    property bool isDirect: room ? room.isDirect : false
     property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
-
+    property bool isDirect: room ? room.isDirect : false
+    property bool isEncrypted: room ? room.isEncrypted : false
+    property string roomId: room ? room.roomId : ""
+    property string roomName: room ? room.roomName : qsTr("No room selected")
+    property string roomTopic: room ? room.roomTopic : ""
     property bool searchHasFocus: searchField.focus && searchField.enabled
-
     property string searchString: ""
-
-    // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
-    Connections {
-        function onHideMenu() {
-            roomOptionsMenu.close()
-        }
-        target: MainWindow
-    }
-
-    onRoomIdChanged: {
-        searchString = "";
-        searchButton.searchActive = false;
-        searchField.text = ""
-    }
-
-    Shortcut {
-        sequence: StandardKey.Find
-        onActivated: searchButton.searchActive = !searchButton.searchActive
-    }
+    property bool showBackButton: false
+    property int trustlevel: room ? room.trustlevel : Crypto.Unverified
 
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
+    padding: 0
     z: 3
 
-    padding: 0
     background: Rectangle {
         color: palette.window
     }
-
-    TapHandler {
-        onSingleTapped: {
-            if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) {
-                eventPoint.accepted = true
-                return;
-            }
-            if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) {
-                eventPoint.accepted = true
-                return;
-            }
-            if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) {
-                eventPoint.accepted = true
-                return;
-            }
-
-            if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall/2) {
-                if (!Communities.trySwitchToSpace(room.parentSpace.roomid))
-                    room.parentSpace.promptJoin();
-                eventPoint.accepted = true
-                return;
-            }
-
-            if (room) {
-                let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y);
-                let link = roomTopicC.linkAt(p.x, p.y);
-
-                if (link) {
-                    Nheko.openLink(link);
-                } else {
-                    TimelineManager.openRoomSettings(room.roomId);
-                }
-            }
-
-            eventPoint.accepted = true;
-        }
-        gesturePolicy: TapHandler.ReleaseWithinBounds
-    }
-
-    HoverHandler {
-        grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
-    }
-
     contentItem: Item {
         GridLayout {
             id: topLayout
 
             anchors.left: parent.left
-            anchors.right: parent.right
             anchors.margins: Nheko.paddingMedium
+            anchors.right: parent.right
             anchors.verticalCenter: parent.verticalCenter
             columnSpacing: Nheko.paddingSmall
             rowSpacing: Nheko.paddingSmall
 
-
             Avatar {
                 id: communityAvatar
 
-                visible: roomid && room.parentSpace.isLoaded && ("space:"+room.parentSpace.roomid != Communities.currentTagId)
-
                 property string avatarUrl: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomAvatarUrl) || ""
                 property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || ""
                 property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || ""
 
+                Layout.alignment: Qt.AlignRight
                 Layout.column: 1
                 Layout.row: 0
-                Layout.alignment: Qt.AlignRight
-                width: fontMetrics.lineSpacing
-                height: fontMetrics.lineSpacing
-                url: avatarUrl.replace("mxc://", "image://MxcImage/")
-                roomid: communityId
                 displayName: communityName
                 enabled: false
+                height: fontMetrics.lineSpacing
+                roomid: communityId
+                url: avatarUrl.replace("mxc://", "image://MxcImage/")
+                visible: roomid && room.parentSpace.isLoaded && ("space:" + room.parentSpace.roomid != Communities.currentTagId)
+                width: fontMetrics.lineSpacing
             }
-
             Label {
                 id: communityLabel
-                visible: communityAvatar.visible
 
                 Layout.column: 2
-                Layout.row: 0
                 Layout.fillWidth: true
+                Layout.row: 0
                 color: palette.text
-                text: qsTr("In %1").arg(communityAvatar.displayName)
-                maximumLineCount: 1
                 elide: Text.ElideRight
+                maximumLineCount: 1
+                text: qsTr("In %1").arg(communityAvatar.displayName)
                 textFormat: Text.RichText
+                visible: communityAvatar.visible
             }
-
             ImageButton {
                 id: backToRoomsButton
 
-                Layout.column: 0
-                Layout.row: 1
-                Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
+                Layout.column: 0
                 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
+                Layout.row: 1
+                Layout.rowSpan: 2
                 ToolTip.text: qsTr("Back to room list")
+                ToolTip.visible: hovered
+                image: ":/icons/icons/ui/angle-arrow-left.svg"
+                visible: showBackButton
+
                 onClicked: Rooms.resetCurrentRoom()
             }
-
             Avatar {
+                Layout.alignment: Qt.AlignVCenter
                 Layout.column: 1
                 Layout.row: 1
                 Layout.rowSpan: 2
-                Layout.alignment: Qt.AlignVCenter
-                width: Nheko.avatarSize
+                displayName: roomName
+                enabled: false
                 height: Nheko.avatarSize
-                url: avatarUrl.replace("mxc://", "image://MxcImage/")
                 roomid: roomId
+                url: avatarUrl.replace("mxc://", "image://MxcImage/")
                 userid: isDirect ? directChatOtherUserId : ""
-                displayName: roomName
-                enabled: false
+                width: Nheko.avatarSize
             }
-
             Label {
-                Layout.fillWidth: true
                 Layout.column: 2
+                Layout.fillWidth: true
                 Layout.row: 1
                 color: palette.text
-                font.pointSize: fontMetrics.font.pointSize * 1.1
+                elide: Text.ElideRight
                 font.bold: true
-                text: roomName
+                font.pointSize: fontMetrics.font.pointSize * 1.1
                 maximumLineCount: 1
-                elide: Text.ElideRight
+                text: roomName
                 textFormat: Text.RichText
             }
-
             MatrixText {
                 id: roomTopicC
-                Layout.fillWidth: true
+
                 Layout.column: 2
-                Layout.row: 2
+                Layout.fillWidth: true
                 Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
-                selectByMouse: false
-                enabled: false
+                Layout.row: 2
                 clip: true
+                enabled: false
+                selectByMouse: false
                 text: roomTopic
             }
-
             ImageButton {
                 id: pinButton
 
                 property bool pinsShown: !Settings.hiddenPins.includes(roomId)
 
-                visible: !!room && room.pinnedMessages.length > 0
-                Layout.column: 3
-                Layout.row: 1
-                Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
+                Layout.column: 3
                 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
+                Layout.row: 1
+                Layout.rowSpan: 2
                 ToolTip.text: qsTr("Show or hide pinned messages")
+                ToolTip.visible: hovered
+                image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
+                visible: !!room && room.pinnedMessages.length > 0
+
                 onClicked: {
                     var ps = Settings.hiddenPins;
                     if (pinsShown) {
@@ -226,242 +156,280 @@ Pane {
                     }
                     Settings.hiddenPins = ps;
                 }
-
             }
-
             AbstractButton {
                 Layout.column: 4
-                Layout.row: 1
-                Layout.rowSpan: 2
                 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
                 Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+                Layout.row: 1
+                Layout.rowSpan: 2
+                background: null
 
                 contentItem: EncryptionIndicator {
-                    encrypted: isEncrypted
-                    trust: trustlevel
-                    enabled: false
-                    unencryptedIcon: ":/icons/icons/ui/people.svg"
-                    unencryptedColor: palette.buttonText
-                    unencryptedHoverColor: palette.highlight
-                    hovered: parent.hovered
-
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: {
                         if (!isEncrypted)
-                        return qsTr("Show room members.");
-
+                            return qsTr("Show room members.");
                         switch (trustlevel) {
-                            case Crypto.Verified:
+                        case Crypto.Verified:
                             return qsTr("This room contains only verified devices.");
-                            case Crypto.TOFU:
+                        case Crypto.TOFU:
                             return qsTr("This room contains verified devices and devices which have never changed their master key.");
-                            default:
+                        default:
                             return qsTr("This room contains unverified devices!");
                         }
                     }
+                    enabled: false
+                    encrypted: isEncrypted
+                    hovered: parent.hovered
+                    trust: trustlevel
+                    unencryptedColor: palette.buttonText
+                    unencryptedHoverColor: palette.highlight
+                    unencryptedIcon: ":/icons/icons/ui/people.svg"
                 }
 
-                background: null
                 onClicked: TimelineManager.openRoomMembers(room)
             }
-
             ImageButton {
                 id: searchButton
 
                 property bool searchActive: false
 
-                visible: !!room
-                Layout.column: 5
-                Layout.row: 1
-                Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
+                Layout.column: 5
                 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
                 Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-                image: ":/icons/icons/ui/search.svg"
-                ToolTip.visible: hovered
+                Layout.row: 1
+                Layout.rowSpan: 2
                 ToolTip.text: qsTr("Search this room")
-                onClicked: searchActive = !searchActive
+                ToolTip.visible: hovered
+                image: ":/icons/icons/ui/search.svg"
+                visible: !!room
 
+                onClicked: searchActive = !searchActive
                 onSearchActiveChanged: {
                     if (searchActive) {
                         searchField.forceActiveFocus();
-                    }
-                    else {
+                    } else {
                         searchField.clear();
                         topBar.searchString = "";
                     }
                 }
             }
-
             ImageButton {
                 id: roomOptionsButton
 
-                visible: !!room
-                Layout.column: 6
-                Layout.row: 1
-                Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignVCenter
+                Layout.column: 6
                 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
                 Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
-                image: ":/icons/icons/ui/options.svg"
-                ToolTip.visible: hovered
+                Layout.row: 1
+                Layout.rowSpan: 2
                 ToolTip.text: qsTr("Room options")
+                ToolTip.visible: hovered
+                image: ":/icons/icons/ui/options.svg"
+                visible: !!room
+
                 onClicked: roomOptionsMenu.open(roomOptionsButton)
 
                 Platform.Menu {
                     id: roomOptionsMenu
 
                     Platform.MenuItem {
-                        visible: room ? room.permissions.canInvite() : false
                         text: qsTr("Invite users")
+                        visible: room ? room.permissions.canInvite() : false
+
                         onTriggered: TimelineManager.openInviteUsers(roomId)
                     }
-
                     Platform.MenuItem {
                         text: qsTr("Members")
+
                         onTriggered: TimelineManager.openRoomMembers(room)
                     }
-
                     Platform.MenuItem {
                         text: qsTr("Leave room")
+
                         onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
                     }
-
                     Platform.MenuItem {
                         text: qsTr("Settings")
+
                         onTriggered: TimelineManager.openRoomSettings(roomId)
                     }
-
                 }
-
             }
-
             ScrollView {
                 id: pinnedMessages
 
-                Layout.row: 3
                 Layout.column: 2
                 Layout.columnSpan: 4
-
                 Layout.fillWidth: true
                 Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
-
-                visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
-                clip: true
-
+                Layout.row: 3
                 ScrollBar.horizontal.visible: false
+                clip: true
+                visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
 
                 ListView {
-
-                    spacing: Nheko.paddingSmall
                     model: room ? room.pinnedMessages : undefined
+                    spacing: Nheko.paddingSmall
+
                     delegate: RowLayout {
                         required property string modelData
 
-                        width: ListView.view.width
                         height: implicitHeight
+                        width: ListView.view.width
 
                         Reply {
                             id: reply
+
                             property var e: room ? room.getDump(modelData, "pins") : {}
-                            Connections {
-                                function onPinnedMessagesChanged() { reply.e = room.getDump(modelData, "pins") }
-                                target: room
-                            }
+
                             Layout.fillWidth: true
                             Layout.preferredHeight: height
-
-                            userColor: TimelineManager.userColor(e.userId, palette.window)
                             blurhash: e.blurhash ?? ""
                             body: e.body ?? ""
-                            formattedBody: e.formattedBody ?? ""
+                            encryptionError: e.encryptionError ?? 0
                             eventId: e.eventId ?? ""
                             filename: e.filename ?? ""
                             filesize: e.filesize ?? ""
+                            formattedBody: e.formattedBody ?? ""
+                            isOnlyEmoji: e.isOnlyEmoji ?? false
+                            keepFullText: true
+                            originalWidth: e.originalWidth ?? 0
                             proportionalHeight: e.proportionalHeight ?? 1
                             type: e.type ?? MtxEvent.UnknownMessage
                             typeString: e.typeString ?? ""
                             url: e.url ?? ""
-                            originalWidth: e.originalWidth ?? 0
-                            isOnlyEmoji: e.isOnlyEmoji ?? false
+                            userColor: TimelineManager.userColor(e.userId, palette.window)
                             userId: e.userId ?? ""
                             userName: e.userName ?? ""
-                            encryptionError: e.encryptionError ?? 0
-                            keepFullText: true
-                        }
 
+                            Connections {
+                                function onPinnedMessagesChanged() {
+                                    reply.e = room.getDump(modelData, "pins");
+                                }
+
+                                target: room
+                            }
+                        }
                         ImageButton {
                             id: deletePinButton
 
+                            Layout.alignment: Qt.AlignTop | Qt.AlignLeft
                             Layout.preferredHeight: 16
                             Layout.preferredWidth: 16
-                            Layout.alignment: Qt.AlignTop | Qt.AlignLeft
-                            visible: room.permissions.canChange(MtxEvent.PinnedEvents)
-
+                            ToolTip.text: qsTr("Unpin")
+                            ToolTip.visible: hovered
                             hoverEnabled: true
                             image: ":/icons/icons/ui/dismiss.svg"
-                            ToolTip.visible: hovered
-                            ToolTip.text: qsTr("Unpin")
+                            visible: room.permissions.canChange(MtxEvent.PinnedEvents)
 
                             onClicked: room.unpin(modelData)
                         }
                     }
-
-
                 }
             }
-
             ScrollView {
                 id: widgets
 
-                Layout.row: 4
                 Layout.column: 2
                 Layout.columnSpan: 4
-
                 Layout.fillWidth: true
                 Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
-
-                visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId)
-                clip: true
-
+                Layout.row: 4
                 ScrollBar.horizontal.visible: false
+                clip: true
+                visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId)
 
                 ListView {
-
-                    spacing: Nheko.paddingSmall
                     model: room ? room.widgetLinks : undefined
+                    spacing: Nheko.paddingSmall
+
                     delegate: MatrixText {
                         required property var modelData
 
                         color: palette.text
                         text: modelData
                     }
-
-
                 }
             }
-
             MatrixTextField {
                 id: searchField
-                visible: searchButton.searchActive
-                enabled: visible
-                hasClear: true
 
-                Layout.row: 5
                 Layout.column: 2
                 Layout.columnSpan: 4
-
                 Layout.fillWidth: true
-
+                Layout.row: 5
+                enabled: visible
+                hasClear: true
                 placeholderText: qsTr("Enter search query")
+                visible: searchButton.searchActive
+
                 onAccepted: topBar.searchString = text
             }
         }
-
         CursorShape {
-            anchors.fill: parent
             anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0)
+            anchors.fill: parent
             cursorShape: Qt.PointingHandCursor
         }
     }
+
+    onRoomIdChanged: {
+        searchString = "";
+        searchButton.searchActive = false;
+        searchField.text = "";
+    }
+
+    // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
+    Connections {
+        function onHideMenu() {
+            roomOptionsMenu.close();
+        }
+
+        target: MainWindow
+    }
+    Shortcut {
+        sequence: StandardKey.Find
+
+        onActivated: searchButton.searchActive = !searchButton.searchActive
+    }
+    TapHandler {
+        gesturePolicy: TapHandler.ReleaseWithinBounds
+
+        onSingleTapped: {
+            if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) {
+                eventPoint.accepted = true;
+                return;
+            }
+            if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) {
+                eventPoint.accepted = true;
+                return;
+            }
+            if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) {
+                eventPoint.accepted = true;
+                return;
+            }
+            if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall / 2) {
+                if (!Communities.trySwitchToSpace(room.parentSpace.roomid))
+                    room.parentSpace.promptJoin();
+                eventPoint.accepted = true;
+                return;
+            }
+            if (room) {
+                let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y);
+                let link = roomTopicC.linkAt(p.x, p.y);
+                if (link) {
+                    Nheko.openLink(link);
+                } else {
+                    TimelineManager.openRoomSettings(room.roomId);
+                }
+            }
+            eventPoint.accepted = true;
+        }
+    }
+    HoverHandler {
+        grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
+    }
 }
diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml
index 704fe8ef..b6c502d8 100644
--- a/resources/qml/TypingIndicator.qml
+++ b/resources/qml/TypingIndicator.qml
@@ -8,30 +8,28 @@ import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
 Item {
-    implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
     Layout.fillWidth: true
+    implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
 
     Rectangle {
         id: typingRect
 
-        visible: (room && room.typingUsers.length > 0)
-        color: palette.base
         anchors.fill: parent
+        color: palette.base
+        visible: (room && room.typingUsers.length > 0)
         z: 3
 
         Label {
             id: typingDisplay
 
+            anchors.bottom: parent.bottom
             anchors.left: parent.left
             anchors.leftMargin: 10
             anchors.right: parent.right
             anchors.rightMargin: 10
-            anchors.bottom: parent.bottom
             color: palette.text
             text: room ? room.formatTypingUsers(room.typingUsers, palette.base) : ""
             textFormat: Text.RichText
         }
-
     }
-
 }
diff --git a/resources/qml/UploadBox.qml b/resources/qml/UploadBox.qml
index ccec6131..54007163 100644
--- a/resources/qml/UploadBox.qml
+++ b/resources/qml/UploadBox.qml
@@ -4,7 +4,6 @@
 
 import "./components"
 import "./ui"
-
 import QtQuick 2.9
 import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
@@ -12,31 +11,33 @@ import im.nheko 1.0
 
 Page {
     id: uploadPopup
-    visible: room && room.input.uploads.length > 0
-    Layout.preferredHeight: 200
-    clip: true
 
     Layout.fillWidth: true
-
+    Layout.preferredHeight: 200
+    clip: true
     padding: Nheko.paddingMedium
+    visible: room && room.input.uploads.length > 0
 
+    background: Rectangle {
+        color: palette.base
+    }
     contentItem: ListView {
         id: uploadsList
+
         anchors.horizontalCenter: parent.horizontalCenter
         boundsBehavior: Flickable.StopAtBounds
+        model: room ? room.input.uploads : undefined
+        orientation: ListView.Horizontal
+        spacing: Nheko.paddingMedium
+        width: Math.min(contentWidth, parent.availableWidth)
 
         ScrollBar.horizontal: ScrollBar {
             id: scr
-        }
-
-        orientation: ListView.Horizontal
-        width: Math.min(contentWidth, parent.availableWidth)
-        model: room ? room.input.uploads : undefined
-        spacing: Nheko.paddingMedium
 
+        }
         delegate: Pane {
+            height: uploadPopup.availableHeight - buttons.height - (scr.visible ? scr.height : 0)
             padding: Nheko.paddingSmall
-            height: uploadPopup.availableHeight - buttons.height - (scr.visible? scr.height : 0)
             width: uploadPopup.availableHeight - buttons.height
 
             background: Rectangle {
@@ -45,46 +46,48 @@ Page {
             }
             contentItem: ColumnLayout {
                 Image {
+                    property string typeStr: switch (modelData.mediaType) {
+                    case MediaUpload.Video:
+                        return "video-file";
+                    case MediaUpload.Audio:
+                        return "music";
+                    case MediaUpload.Image:
+                        return "image";
+                    default:
+                        return "zip";
+                    }
+
                     Layout.fillHeight: true
                     Layout.fillWidth: true
-
-                    sourceSize.height: parent.availableHeight - namefield.height
-                    sourceSize.width: parent.availableWidth
                     fillMode: Image.PreserveAspectFit
-                    smooth: true
                     mipmap: true
-
-                    property string typeStr: switch(modelData.mediaType) {
-                        case MediaUpload.Video: return "video-file";
-                        case MediaUpload.Audio: return "music";
-                        case MediaUpload.Image: return "image";
-                        default: return "zip";
-                    }
-                    source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + palette.buttonText)
+                    smooth: true
+                    source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/" + typeStr + ".svg?" + palette.buttonText)
+                    sourceSize.height: parent.availableHeight - namefield.height
+                    sourceSize.width: parent.availableWidth
                 }
                 MatrixTextField {
                     id: namefield
+
                     Layout.fillWidth: true
                     text: modelData.filename
+
                     onTextEdited: modelData.filename = text
                 }
             }
         }
     }
-
     footer: DialogButtonBox {
         id: buttons
 
         standardButtons: DialogButtonBox.Cancel
-        Button {
-            text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0))
-            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
-        }
+
         onAccepted: room.input.acceptUploads()
         onRejected: room.input.declineUploads()
-    }
 
-    background: Rectangle {
-        color: palette.base
+        Button {
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+            text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0))
+        }
     }
 }