diff --git a/CMakeLists.txt b/CMakeLists.txt index 96948827..5e4b0f3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,7 +137,7 @@ endif() # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) +find_package(Qt5 5.12 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) find_package(Qt5QuickCompiler) find_package(Qt5DBus) @@ -265,6 +265,7 @@ set(SRC_FILES # Timeline + src/timeline/CommunitiesModel.cpp src/timeline/EventStore.cpp src/timeline/InputBar.cpp src/timeline/Reaction.cpp @@ -272,6 +273,7 @@ set(SRC_FILES src/timeline/TimelineModel.cpp src/timeline/DelegateChooser.cpp src/timeline/Permissions.cpp + src/timeline/RoomlistModel.cpp # UI components src/ui/Avatar.cpp @@ -284,11 +286,13 @@ set(SRC_FILES src/ui/LoadingIndicator.cpp src/ui/NhekoCursorShape.cpp src/ui/NhekoDropArea.cpp + src/ui/NhekoGlobalObject.cpp src/ui/OverlayModal.cpp src/ui/OverlayWidget.cpp src/ui/RaisedButton.cpp src/ui/Ripple.cpp src/ui/RippleOverlay.cpp + src/ui/RoomSettings.cpp src/ui/SnackBar.cpp src/ui/TextField.cpp src/ui/TextLabel.cpp @@ -296,7 +300,6 @@ set(SRC_FILES src/ui/ThemeManager.cpp src/ui/ToggleButton.cpp src/ui/UserProfile.cpp - src/ui/RoomSettings.cpp # Generic notification stuff src/notifications/Manager.cpp @@ -309,8 +312,6 @@ set(SRC_FILES src/ChatPage.cpp src/Clipboard.cpp src/ColorImageProvider.cpp - src/CommunitiesList.cpp - src/CommunitiesListItem.cpp src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp @@ -322,22 +323,14 @@ set(SRC_FILES src/MxcImageProvider.cpp src/Olm.cpp src/RegisterPage.cpp - src/RoomInfoListItem.cpp - src/RoomList.cpp src/SSOHandler.cpp - src/SideBarActions.cpp - src/Splitter.cpp src/TrayIcon.cpp - src/UserInfoWidget.cpp src/UserSettingsPage.cpp src/UsersModel.cpp src/RoomsModel.cpp src/Utils.cpp src/WebRTCSession.cpp src/WelcomePage.cpp - src/popups/PopupItem.cpp - src/popups/SuggestionsPopup.cpp - src/popups/UserMentions.cpp src/main.cpp third_party/blurhash/blurhash.cpp @@ -489,6 +482,7 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/Provider.h # Timeline + src/timeline/CommunitiesModel.h src/timeline/EventStore.h src/timeline/InputBar.h src/timeline/Reaction.h @@ -496,30 +490,32 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/TimelineModel.h src/timeline/DelegateChooser.h src/timeline/Permissions.h + src/timeline/RoomlistModel.h # UI components src/ui/Avatar.h src/ui/Badge.h - src/ui/LoadingIndicator.h - src/ui/InfoMessage.h src/ui/FlatButton.h - src/ui/Label.h src/ui/FloatingButton.h + src/ui/InfoMessage.h + src/ui/Label.h + src/ui/LoadingIndicator.h src/ui/Menu.h src/ui/NhekoCursorShape.h src/ui/NhekoDropArea.h + src/ui/NhekoGlobalObject.h src/ui/OverlayWidget.h - src/ui/SnackBar.h src/ui/RaisedButton.h src/ui/Ripple.h src/ui/RippleOverlay.h + src/ui/RoomSettings.h + src/ui/SnackBar.h src/ui/TextField.h src/ui/TextLabel.h - src/ui/ToggleButton.h src/ui/Theme.h src/ui/ThemeManager.h + src/ui/ToggleButton.h src/ui/UserProfile.h - src/ui/RoomSettings.h src/notifications/Manager.h @@ -531,8 +527,6 @@ qt5_wrap_cpp(MOC_HEADERS src/CallManager.h src/ChatPage.h src/Clipboard.h - src/CommunitiesList.h - src/CommunitiesListItem.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h src/InviteeItem.h @@ -540,21 +534,13 @@ qt5_wrap_cpp(MOC_HEADERS src/MainWindow.h src/MxcImageProvider.h src/RegisterPage.h - src/RoomInfoListItem.h - src/RoomList.h src/SSOHandler.h - src/SideBarActions.h - src/Splitter.h src/TrayIcon.h - src/UserInfoWidget.h src/UserSettingsPage.h src/UsersModel.h src/RoomsModel.h src/WebRTCSession.h src/WelcomePage.h - src/popups/PopupItem.h - src/popups/SuggestionsPopup.h - src/popups/UserMentions.h ) # diff --git a/resources/icons/ui/user-friends-solid.svg b/resources/icons/ui/user-friends-solid.svg new file mode 100644 index 00000000..1add45ec --- /dev/null +++ b/resources/icons/ui/user-friends-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 108bb768..6c12952a 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -14,18 +14,21 @@ Rectangle { property alias url: img.source property string userid property string displayName + property alias textColor: label.color signal clicked(var mouse) width: 48 height: 48 radius: Settings.avatarCircles ? height / 2 : 3 - color: colors.alternateBase + color: Nheko.colors.alternateBase Component.onCompleted: { mouseArea.clicked.connect(clicked); } Label { + id: label + anchors.fill: parent text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText @@ -33,7 +36,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter visible: img.status != Image.Ready - color: colors.text + color: Nheko.colors.text } Image { @@ -55,7 +58,7 @@ Rectangle { Ripple { rippleTarget: mouseArea - color: Qt.rgba(colors.alternateBase.r, colors.alternateBase.g, colors.alternateBase.b, 0.5) + color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5) } } diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml new file mode 100644 index 00000000..7a428019 --- /dev/null +++ b/resources/qml/ChatPage.qml @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import "components" +import im.nheko 1.0 + +Rectangle { + id: chatPage + + color: Nheko.colors.window + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: width < communityListC.maximumWidth + roomListC.maximumWidth + timlineViewC.minimumWidth + pageIndex: Rooms.currentRoom ? 2 : 1 + + AdaptiveLayoutElement { + id: communityListC + + 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 + + CommunitiesList { + id: communitiesList + + collapsed: parent.collapsed + } + + Binding { + target: Settings + property: 'communityListWidth' + value: communityListC.preferredWidth + when: !adaptiveView.singlePageMode + delayed: true + } + + } + + AdaptiveLayoutElement { + id: roomListC + + minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 + preferredWidth: Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : roomlist.avatarSize * 5 + Nheko.paddingSmall * 2 + maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 + collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium + + RoomList { + id: roomlist + + collapsed: parent.collapsed + } + + Binding { + target: Settings + property: 'roomListWidth' + value: roomListC.preferredWidth + when: !adaptiveView.singlePageMode + delayed: true + } + + } + + AdaptiveLayoutElement { + id: timlineViewC + + minimumWidth: 400 + + TimelineView { + id: timeline + + showBackButton: adaptiveView.singlePageMode + room: Rooms.currentRoom + } + + } + + } + + PrivacyScreen { + anchors.fill: parent + visible: Settings.privacyScreen + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: adaptiveView + } + +} diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml new file mode 100644 index 00000000..491913be --- /dev/null +++ b/resources/qml/CommunitiesList.qml @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Page { + //leftPadding: Nheko.paddingSmall + //rightPadding: Nheko.paddingSmall + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) + property bool collapsed: false + + ListView { + id: communitiesList + + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Communities + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + Platform.Menu { + id: communityContextMenu + + property string tagId + + function show(id_, tags_) { + tagId = id_; + open(); + } + + Platform.MenuItem { + text: qsTr("Hide rooms with this tag or from this space by default.") + onTriggered: Communities.toggleTagId(communityContextMenu.tagId) + } + + } + + delegate: Rectangle { + id: communityItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + ToolTip.visible: hovered.hovered && collapsed + ToolTip.text: model.tooltip + states: [ + State { + name: "highlight" + when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id) + + PropertyChanges { + target: communityItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: Communities.currentTagId == model.id + + PropertyChanges { + target: communityItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.RightButton + onSingleTapped: communityContextMenu.show(model.id) + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Communities.setCurrentTagId(model.id) + onLongPressed: communityContextMenu.show(model.id) + } + + HoverHandler { + id: hovered + + margin: -Nheko.paddingSmall + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: { + if (model.avatarUrl.startsWith("mxc://")) + return model.avatarUrl.replace("mxc://", "image://MxcImage/"); + else + return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; + } + displayName: model.displayName + color: communityItem.background + } + + ElidedLabel { + visible: !collapsed + Layout.alignment: Qt.AlignVCenter + color: communityItem.importantText + elideWidth: parent.width - avatar.width - Nheko.paddingMedium + fullText: model.displayName + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + +} diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index d648553f..bbfa7b94 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -70,7 +70,7 @@ Popup { onCompleterNameChanged: { if (completerName) { if (completerName == "user") - completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId()); + completer = TimelineManager.completerFor(completerName, room.roomId()); else completer = TimelineManager.completerFor(completerName); completer.setSearchString(""); @@ -83,8 +83,8 @@ Popup { height: listView.contentHeight + 2 // + 2 for the padding on top and bottom Connections { - onTimelineChanged: completer = null - target: TimelineManager + onRoomChanged: completer = null + target: timelineView } ListView { @@ -100,7 +100,7 @@ Popup { delegate: Rectangle { property variant modelData: model - color: model.index == popup.currentIndex ? colors.highlight : colors.base + color: model.index == popup.currentIndex ? Nheko.colors.highlight : Nheko.colors.base height: chooser.childrenRect.height + 2 * popup.rowMargin implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4 @@ -119,7 +119,7 @@ Popup { Ripple { rippleTarget: mouseArea - color: Qt.rgba(colors.base.r, colors.base.g, colors.base.b, 0.5) + color: Qt.rgba(Nheko.colors.base.r, Nheko.colors.base.g, Nheko.colors.base.b, 0.5) } } @@ -150,12 +150,12 @@ Popup { Label { text: model.displayName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } Label { text: "(" + model.userid + ")" - color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } } @@ -173,13 +173,13 @@ Popup { Label { text: model.unicode - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text font: Settings.emojiFont } Label { text: model.shortName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } } @@ -209,7 +209,8 @@ Popup { Label { text: model.roomName font.pixelSize: popup.avatarHeight * 0.5 - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + textFormat: Text.RichText } } @@ -235,12 +236,14 @@ Popup { Label { text: model.roomName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + textFormat: Text.RichText } Label { text: "(" + model.roomAlias + ")" - color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText + textFormat: Text.RichText } } @@ -274,10 +277,10 @@ Popup { } background: Rectangle { - color: colors.base + color: Nheko.colors.base implicitHeight: popup.contentHeight implicitWidth: popup.contentWidth - border.color: colors.mid + border.color: Nheko.colors.mid } } diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml new file mode 100644 index 00000000..bc90e479 --- /dev/null +++ b/resources/qml/ElidedLabel.qml @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import im.nheko 1.0 + +Label { + id: root + + property alias fullText: metrics.text + property alias elideWidth: metrics.elideWidth + + color: Nheko.colors.text + text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(TimelineManager.htmlEscape(metrics.elidedText)) + maximumLineCount: 1 + elide: Text.ElideRight + textFormat: Text.PlainText + + TextMetrics { + id: metrics + + font.pointSize: root.font.pointSize + elide: Text.ElideRight + } + +} diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 1e5d4d38..52d2eeed 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -20,7 +20,7 @@ Image { case Crypto.Verified: return "image://colorimage/:/icons/icons/ui/lock.png?green"; case Crypto.TOFU: - return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText; + return "image://colorimage/:/icons/icons/ui/lock.png?" + Nheko.colors.buttonText; default: return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d"; } diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 408cab12..eee3879c 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -19,7 +19,7 @@ Popup { x: Math.round(parent.width / 2 - width / 2) y: Math.round(parent.height / 2 - height / 2) modal: true - palette: colors + palette: Nheko.colors parent: Overlay.overlay width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) height: implicitHeight + completerPopup.height + padding * 2 @@ -44,22 +44,22 @@ Popup { text: qsTr("Forward Message") font.bold: true bottomPadding: 10 - color: colors.text + color: Nheko.colors.text } Reply { id: replyPreview - modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : { + modelData: room ? room.getDump(mid, "") : { } - userColor: TimelineManager.userColor(modelData.userId, colors.window) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } MatrixTextField { id: roomTextInput width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 - color: colors.text + color: Nheko.colors.text onTextEdited: { completerPopup.completer.searchString = text; } @@ -95,7 +95,7 @@ Popup { Connections { onCompletionSelected: { - TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id); + room.forwardMessage(messageContextMenu.eventId, id); forwardMessagePopup.close(); } onCountChanged: { @@ -107,11 +107,11 @@ Popup { } background: Rectangle { - color: colors.window + color: Nheko.colors.window } Overlay.modal: Rectangle { - color: Qt.rgba(colors.window.r, colors.window.g, colors.window.b, 0.7) + color: Qt.rgba(Nheko.colors.window.r, Nheko.colors.window.g, Nheko.colors.window.b, 0.7) } } diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 76cc0b42..b5d9c152 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -12,8 +12,8 @@ AbstractButton { property alias cursor: mouseArea.cursorShape property string image: undefined - property color highlightColor: colors.highlight - property color buttonTextColor: colors.buttonText + property color highlightColor: Nheko.colors.highlight + property color buttonTextColor: Nheko.colors.buttonText property bool changeColorOnHover: true focusPolicy: Qt.NoFocus @@ -26,6 +26,7 @@ AbstractButton { // Workaround, can't get icon.source working for now... anchors.fill: parent source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" + fillMode: Image.PreserveAspectFit } CursorShape { diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 7cfa6735..167899a5 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -13,8 +13,8 @@ TextEdit { wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode enabled: selectByMouse - color: colors.text - onLinkActivated: TimelineManager.openLink(link) + color: Nheko.colors.text + onLinkActivated: Nheko.openLink(link) ToolTip.visible: hoveredLink ToolTip.text: hoveredLink diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index 3bcc9675..3c660bac 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -5,18 +5,20 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import im.nheko 1.0 TextField { id: input - palette: colors + palette: Nheko.colors + color: Nheko.colors.text Rectangle { id: blueBar anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - color: colors.highlight + color: Nheko.colors.highlight height: 1 width: parent.width @@ -27,7 +29,7 @@ TextField { anchors.horizontalCenter: parent.horizontalCenter height: parent.height + 1 width: 0 - color: colors.text + color: Nheko.colors.text states: State { name: "focused" @@ -60,7 +62,7 @@ TextField { } background: Rectangle { - color: colors.base + color: Nheko.colors.base } } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index c5dfbfa3..24f9b0e8 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -12,7 +12,7 @@ import im.nheko 1.0 Rectangle { id: inputBar - color: colors.window + color: Nheko.colors.window Layout.fillWidth: true Layout.preferredHeight: row.implicitHeight Layout.minimumHeight: 40 @@ -28,7 +28,7 @@ Rectangle { RowLayout { id: row - visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false anchors.fill: parent ImageButton { @@ -43,7 +43,7 @@ Rectangle { ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") Layout.margins: 8 onClicked: { - if (TimelineManager.timeline) { + if (room) { if (CallManager.haveCallInvite) { return ; } else if (CallManager.isOnCall) { @@ -63,14 +63,14 @@ Rectangle { height: 22 image: ":/icons/icons/ui/paper-clip-outline.png" Layout.margins: 8 - onClicked: TimelineManager.timeline.input.openFileSelection() + onClicked: room.input.openFileSelection() ToolTip.visible: hovered ToolTip.text: qsTr("Send a file") Rectangle { anchors.fill: parent - color: colors.window - visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading + color: Nheko.colors.window + visible: room && room.input.uploading NhekoBusyIndicator { anchors.fill: parent @@ -116,23 +116,23 @@ Rectangle { selectByMouse: true placeholderText: qsTr("Write a message...") - placeholderTextColor: colors.buttonText - color: colors.text + placeholderTextColor: Nheko.colors.buttonText + color: Nheko.colors.text width: textInput.width wrapMode: TextEdit.Wrap padding: 8 focus: true onTextChanged: { - if (TimelineManager.timeline) - TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (room) + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); forceActiveFocus(); } onCursorPositionChanged: { - if (!TimelineManager.timeline) + if (!room) return ; - TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); if (cursorPosition <= completerTriggeredAt) { completerTriggeredAt = -1; popup.close(); @@ -141,13 +141,13 @@ Rectangle { popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)); } - onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + 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.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { - TimelineManager.timeline.input.paste(false); + room.input.paste(false); event.accepted = true; } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon @@ -160,9 +160,9 @@ Rectangle { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { messageInput.clear(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { - messageInput.text = TimelineManager.timeline.input.previousText(); + messageInput.text = room.input.previousText(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { - messageInput.text = TimelineManager.timeline.input.nextText(); + messageInput.text = room.input.nextText(); } else if (event.key == Qt.Key_At) { messageInput.openCompleter(cursorPosition, "user"); popup.open(); @@ -188,7 +188,7 @@ Rectangle { return ; } } - TimelineManager.timeline.input.send(); + room.input.send(); event.accepted = true; } else if (event.key == Qt.Key_Tab) { event.accepted = true; @@ -223,11 +223,11 @@ Rectangle { } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) { if (cursorPosition == 0) { event.accepted = true; - var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0; + var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0; while (true) { - var id = TimelineManager.timeline.indexToId(idx); - if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { - TimelineManager.timeline.edit = id; + var id = room.indexToId(idx); + if (!id || room.getDump(id, "").isEditable) { + room.edit = id; cursorPosition = 0; Qt.callLater(positionCursorAtEnd); break; @@ -239,13 +239,13 @@ Rectangle { positionCursorAtStart(); } } else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) { - if (cursorPosition == messageInput.length && TimelineManager.timeline.edit) { + if (cursorPosition == messageInput.length && room.edit) { event.accepted = true; - var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1; + var idx = room.idToIndex(room.edit) - 1; while (true) { - var id = TimelineManager.timeline.indexToId(idx); - if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { - TimelineManager.timeline.edit = id; + var id = room.indexToId(idx); + if (!id || room.getDump(id, "").isEditable) { + room.edit = id; Qt.callLater(positionCursorAtStart); break; } @@ -260,14 +260,14 @@ Rectangle { background: null Connections { - onActiveTimelineChanged: { + onRoomChanged: { messageInput.clear(); - messageInput.append(TimelineManager.timeline.input.text()); + messageInput.append(room.input.text()); messageInput.completerTriggeredAt = -1; popup.completerName = ""; messageInput.forceActiveFocus(); } - target: TimelineManager + target: timelineView } Connections { @@ -292,14 +292,14 @@ Rectangle { messageInput.text = newText; messageInput.cursorPosition = newText.length; } - target: TimelineManager.timeline ? TimelineManager.timeline.input : null + target: room ? room.input : null } Connections { ignoreUnknownSignals: true onReplyChanged: messageInput.forceActiveFocus() onEditChanged: messageInput.forceActiveFocus() - target: TimelineManager.timeline + target: room } Connections { @@ -312,7 +312,7 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.MiddleButton cursorShape: Qt.IBeamCursor - onClicked: TimelineManager.timeline.input.paste(true) + onClicked: room.input.paste(true) } } @@ -347,7 +347,7 @@ Rectangle { ToolTip.visible: hovered ToolTip.text: qsTr("Send") onClicked: { - TimelineManager.timeline.input.send(); + room.input.send(); } } @@ -355,9 +355,9 @@ Rectangle { Text { anchors.centerIn: parent - visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false + visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false text: qsTr("You don't have permission to send messages in this room") - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 7dbe7e12..c936c638 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -4,6 +4,7 @@ import "./delegates" import "./emoji" +import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 import QtQuick 2.12 import QtQuick.Controls 2.3 @@ -13,7 +14,7 @@ import im.nheko 1.0 ScrollView { clip: false - palette: colors + palette: Nheko.colors padding: 8 ScrollBar.horizontal.visible: false @@ -22,7 +23,7 @@ ScrollView { property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 - model: TimelineManager.timeline + model: room boundsBehavior: Flickable.StopAtBounds pixelAligned: true spacing: 4 @@ -51,8 +52,8 @@ ScrollView { z: 10 height: row.implicitHeight + padding * 2 width: row.implicitWidth + padding * 2 - color: colors.window - border.color: colors.buttonText + color: Nheko.colors.window + border.color: Nheko.colors.buttonText border.width: 1 radius: padding @@ -74,7 +75,7 @@ ScrollView { id: editButton visible: !!row.model && row.model.isEditable - buttonTextColor: colors.buttonText + buttonTextColor: Nheko.colors.buttonText width: 16 hoverEnabled: true image: ":/icons/icons/ui/edit.png" @@ -220,7 +221,7 @@ ScrollView { anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined visible: modelData && modelData.previousMessageDay !== modelData.day text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : "" - color: colors.text + color: Nheko.colors.text height: Math.round(fontMetrics.height * 1.4) width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter @@ -228,7 +229,7 @@ ScrollView { background: Rectangle { radius: parent.height / 2 - color: colors.window + color: Nheko.colors.window } } @@ -240,8 +241,8 @@ ScrollView { Avatar { id: messageUserAvatar - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" displayName: modelData ? modelData.userName : "" userid: modelData ? modelData.userId : "" @@ -267,7 +268,7 @@ ScrollView { id: userName text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : "" - color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window) + color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window) textFormat: Text.RichText ToolTip.visible: displayNameHover.hovered ToolTip.text: modelData ? modelData.userId : "" @@ -288,11 +289,11 @@ ScrollView { } Label { - color: colors.buttonText + color: Nheko.colors.buttonText text: modelData ? TimelineManager.userStatus(modelData.userId) : "" textFormat: Text.PlainText elide: Text.ElideRight - width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize + width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize font.italic: true } @@ -317,7 +318,7 @@ ScrollView { opacity: 0 visible: true anchors.fill: timelinerow - color: colors.highlight + color: Nheko.colors.highlight states: State { name: "revealed" @@ -413,4 +414,141 @@ ScrollView { } + Platform.Menu { + id: messageContextMenu + + property string eventId + property string link + property string text + property int eventType + property bool isEncrypted + property bool isEditable + property bool isSender + + function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { + eventId = eventId_; + eventType = eventType_; + isEncrypted = isEncrypted_; + isEditable = isEditable_; + isSender = isSender_; + if (text_) + text = text_; + else + text = ""; + if (link_) + link = link_; + else + link = ""; + if (showAt_) + open(showAt_); + else + open(); + } + + Platform.MenuItem { + visible: messageContextMenu.text + enabled: visible + text: qsTr("&Copy") + onTriggered: Clipboard.text = messageContextMenu.text + } + + Platform.MenuItem { + visible: messageContextMenu.link + enabled: visible + text: qsTr("Copy &link location") + onTriggered: Clipboard.text = messageContextMenu.link + } + + Platform.MenuItem { + id: reactionOption + + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + text: qsTr("Re&act") + onTriggered: emojiPopup.show(null, function(emoji) { + room.input.reaction(messageContextMenu.eventId, emoji); + }) + } + + Platform.MenuItem { + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + text: qsTr("Repl&y") + onTriggered: room.replyAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + enabled: visible + text: qsTr("&Edit") + onTriggered: room.editAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + text: qsTr("Read receip&ts") + onTriggered: room.readReceiptsAction(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") + onTriggered: { + var forwardMess = forwardCompleterComponent.createObject(timelineRoot); + forwardMess.setMessageEventId(messageContextMenu.eventId); + forwardMess.open(); + } + } + + 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") + onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender + text: qsTr("Remo&ve message") + onTriggered: room.redactEvent(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("&Save as") + 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") + onTriggered: room.openMedia(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventId + enabled: visible + text: qsTr("Copy link to eve&nt") + onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) + } + + } + + Component { + id: forwardCompleterComponent + + ForwardCompleter { + } + + } + } diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml index 917e11dc..82dd26dd 100644 --- a/resources/qml/NhekoBusyIndicator.qml +++ b/resources/qml/NhekoBusyIndicator.qml @@ -5,6 +5,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 +import im.nheko 1.0 BusyIndicator { id: control @@ -38,7 +39,7 @@ BusyIndicator { implicitWidth: radius * 2 implicitHeight: radius * 2 radius: item.height / 6 - color: colors.text + color: Nheko.colors.text opacity: (index + 2) / (repeater.count + 2) transform: [ Translate { diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml index 166c788d..8c4f47ca 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml @@ -19,7 +19,7 @@ Popup { modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside parent: Overlay.overlay - palette: colors + palette: Nheko.colors onOpened: { completerPopup.open(); roomTextInput.forceActiveFocus(); @@ -34,7 +34,7 @@ Popup { anchors.fill: parent font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) padding: textMargin - color: colors.text + color: Nheko.colors.text onTextEdited: { completerPopup.completer.searchString = text; } @@ -72,8 +72,7 @@ Popup { Connections { onCompletionSelected: { - TimelineManager.setHistoryView(id); - TimelineManager.highlightRoom(id); + Rooms.setCurrentRoom(id); quickSwitcher.close(); } onCountChanged: { diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index f53c89ad..def87f75 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -12,9 +12,9 @@ Flow { id: reactionFlow // highlight colors for selfReactedEvent background - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + property real highlightHue: Nheko.colors.highlight.hslHue + property real highlightSat: Nheko.colors.highlight.hslSaturation + property real highlightLight: Nheko.colors.highlight.hslLightness property string eventId property alias reactions: repeater.model @@ -35,7 +35,7 @@ Flow { ToolTip.text: modelData.users onClicked: { console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); - TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); + room.input.reaction(reactionFlow.eventId, modelData.key); } contentItem: Row { @@ -59,7 +59,7 @@ Flow { anchors.baseline: reactionCounter.baseline text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") font.family: Settings.emojiFont - color: reaction.hovered ? colors.highlight : colors.text + color: reaction.hovered ? Nheko.colors.highlight : Nheko.colors.text maximumLineCount: 1 } @@ -68,7 +68,7 @@ Flow { height: Math.floor(reactionCounter.implicitHeight * 1.4) width: 1 - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text } Text { @@ -77,7 +77,7 @@ Flow { anchors.verticalCenter: divider.verticalCenter text: modelData.count font: reaction.font - color: reaction.hovered ? colors.highlight : colors.text + color: reaction.hovered ? Nheko.colors.highlight : Nheko.colors.text } } @@ -86,8 +86,8 @@ Flow { anchors.centerIn: parent implicitWidth: reaction.implicitWidth implicitHeight: reaction.implicitHeight - border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.window + border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text + color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : Nheko.colors.window border.width: 1 radius: reaction.height / 2 } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 37b6f6cc..0de68fe8 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -11,13 +11,11 @@ import im.nheko 1.0 Rectangle { id: replyPopup - property var room: TimelineManager.timeline - Layout.fillWidth: true visible: room && (room.reply || room.edit) // Height of child, plus margins, plus border implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10 - color: colors.window + color: Nheko.colors.window z: 3 Reply { @@ -31,7 +29,7 @@ Rectangle { anchors.bottom: parent.bottom modelData: room ? room.getDump(room.reply, room.id) : { } - userColor: TimelineManager.userColor(modelData.userId, colors.window) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } ImageButton { diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml new file mode 100644 index 00000000..76680b37 --- /dev/null +++ b/resources/qml/RoomList.qml @@ -0,0 +1,604 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Page { + //leftPadding: Nheko.paddingSmall + //rightPadding: Nheko.paddingSmall + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property bool collapsed: false + + ListView { + id: roomlist + + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Rooms + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + Connections { + onActiveTimelineChanged: { + roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain); + console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId())); + } + target: TimelineManager + } + + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + open(); + } + + InputDialog { + id: newTag + + title: qsTr("New tag") + prompt: qsTr("Enter the tag you want to use:") + onAccepted: function(text) { + Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); + } + } + + Platform.MenuItem { + text: qsTr("Leave room") + onTriggered: Rooms.leave(roomContextMenu.roomid) + } + + Platform.MenuSeparator { + text: qsTr("Tag room as:") + } + + Instantiator { + model: Communities.tags + onObjectAdded: roomContextMenu.insertItem(index + 2, object) + onObjectRemoved: roomContextMenu.removeItem(object) + + delegate: Platform.MenuItem { + property string t: modelData + + 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); + } + } + checkable: true + checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + } + + Platform.MenuItem { + text: qsTr("Create new tag...") + onTriggered: newTag.show() + } + + } + + delegate: Rectangle { + id: roomItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + ToolTip.visible: hovered.hovered && collapsed + ToolTip.text: model.roomName + states: [ + State { + name: "highlight" + when: hovered.hovered && !(Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()) + + PropertyChanges { + target: roomItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId() + + PropertyChanges { + target: roomItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.RightButton + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(model.roomId, model.tags); + + } + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Rooms.setCurrentRoom(model.roomId) + onLongPressed: { + if (!TimelineManager.isInvite) + roomContextMenu.show(model.roomId, model.tags); + + } + } + + HoverHandler { + id: hovered + + margin: -Nheko.paddingSmall + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + // In the future we could show an online indicator by setting the userid for the avatar + //userid: Nheko.currentUser.userid + + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.roomName + + Rectangle { + id: collapsedNotificationBubble + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: -Nheko.paddingSmall + visible: collapsed && model.notificationCount > 0 + enabled: false + Layout.alignment: Qt.AlignRight + height: fontMetrics.averageCharacterWidth * 3 + width: height + radius: height / 2 + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground + + Label { + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.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 + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: roomItem.importantText + elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium + fullText: model.roomName + textFormat: Text.RichText + } + + Item { + Layout.fillWidth: true + } + + Label { + id: timestamp + + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + color: roomItem.unimportantText + text: model.time + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + visible: !model.isInvite + height: visible ? 0 : undefined + + ElidedLabel { + color: roomItem.unimportantText + font.weight: Font.Thin + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall + fullText: model.lastMessage + textFormat: Text.RichText + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + id: notificationBubble + + visible: model.notificationCount > 0 + Layout.alignment: Qt.AlignRight + height: fontMetrics.averageCharacterWidth * 3 + width: height + radius: height / 2 + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground + + Label { + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.notificationCount + } + + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: Nheko.paddingMedium + visible: model.isInvite + enabled: visible + height: visible ? 0 : undefined + + ElidedLabel { + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Accept") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.acceptInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + ElidedLabel { + Layout.alignment: Qt.AlignRight + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Decline") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.declineInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: parent.height - Nheko.paddingSmall * 2 + width: 3 + color: Nheko.colors.highlight + visible: model.hasUnreadMessages + } + + } + + } + + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + + header: ColumnLayout { + spacing: 0 + + Rectangle { + id: userInfoPanel + + function openUserProfile() { + Nheko.updateUserProfile(); + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": Nheko.currentUser + }); + userProfile.show(); + } + + color: Nheko.colors.window + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium + Layout.minimumHeight: 40 + + InputDialog { + id: statusDialog + + title: qsTr("Status Message") + prompt: qsTr("Enter your status message:") + onAccepted: function(text) { + Nheko.setStatusMessage(text); + } + } + + Platform.Menu { + id: userInfoMenu + + Platform.MenuItem { + text: qsTr("Profile settings") + onTriggered: userInfoPanel.openUserProfile() + } + + Platform.MenuItem { + text: qsTr("Set status message") + onTriggered: statusDialog.show() + } + + } + + 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 + } + + RowLayout { + id: userInfoGrid + + property var profile: Nheko.currentUser + + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: 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 : "" + } + + 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: Nheko.colors.buttonText + font.weight: Font.Thin + font.pointSize: fontMetrics.font.pointSize * 0.9 + elideWidth: col.width + fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" + } + + } + + Item { + } + + ImageButton { + id: logoutButton + + visible: !collapsed + Layout.alignment: Qt.AlignVCenter + image: ":/icons/icons/ui/power-button-off.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Logout") + onClicked: Nheko.openLogoutDialog() + } + + } + + } + + Rectangle { + color: Nheko.theme.separator + height: 2 + Layout.fillWidth: true + } + + } + + footer: ColumnLayout { + spacing: 0 + + Rectangle { + color: Nheko.theme.separator + height: 1 + Layout.fillWidth: true + } + + Rectangle { + color: Nheko.colors.window + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + Layout.preferredHeight: buttonRow.implicitHeight + Layout.minimumHeight: 40 + + RowLayout { + id: buttonRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Nheko.paddingMedium + + ImageButton { + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/plus-black-symbol.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Start a new chat") + Layout.margins: Nheko.paddingMedium + onClicked: roomJoinCreateMenu.open(parent) + + Platform.Menu { + id: roomJoinCreateMenu + + Platform.MenuItem { + text: qsTr("Join a room") + onTriggered: Nheko.openJoinRoomDialog() + } + + Platform.MenuItem { + text: qsTr("Create a new room") + onTriggered: Nheko.openCreateRoomDialog() + } + + } + + } + + ImageButton { + visible: !collapsed + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/speech-bubbles-comment-option.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Room directory") + Layout.margins: Nheko.paddingMedium + } + + ImageButton { + visible: !collapsed + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/settings.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("User settings") + Layout.margins: Nheko.paddingMedium + onClicked: Nheko.showUserSettingsPage() + } + + } + + } + + } + +} diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 58567916..1f7fe5de 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -18,9 +18,9 @@ ApplicationWindow { y: MainWindow.y + (MainWindow.height / 2) - (height / 2) minimumWidth: 420 minimumHeight: 650 - palette: colors - color: colors.window - modality: Qt.WindowModal + palette: Nheko.colors + color: Nheko.colors.window + modality: Qt.NonModal flags: Qt.Dialog title: qsTr("Room Settings") @@ -126,9 +126,9 @@ ApplicationWindow { readOnly: true background: null selectByMouse: true - color: colors.text + color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter - onLinkActivated: TimelineManager.openLink(link) + onLinkActivated: Nheko.openLink(link) CursorShape { anchors.fill: parent @@ -205,7 +205,7 @@ ApplicationWindow { title: qsTr("End-to-End Encryption") text: qsTr("Encryption is currently experimental and things might break unexpectedly.
Please take note that it can't be disabled afterwards.") - modality: Qt.WindowModal + modality: Qt.NonModal onAccepted: { if (roomSettings.isEncryptionEnabled) return ; diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml new file mode 100644 index 00000000..5316e20d --- /dev/null +++ b/resources/qml/Root.qml @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./delegates" +import "./device-verification" +import "./emoji" +import "./voip" +import Qt.labs.platform 1.1 as Platform +import QtGraphicalEffects 1.0 +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 +import im.nheko 1.0 +import im.nheko.EmojiModel 1.0 + +Page { + id: timelineRoot + + palette: Nheko.colors + + FontMetrics { + id: fontMetrics + } + + EmojiPicker { + id: emojiPopup + + colors: palette + model: TimelineManager.completerFor("allemoji", "") + } + + Component { + id: userProfileComponent + + UserProfile { + } + + } + + Component { + id: roomSettingsComponent + + RoomSettings { + } + + } + + Component { + id: mobileCallInviteDialog + + CallInvite { + } + + } + + Component { + id: quickSwitcherComponent + + QuickSwitcher { + } + + } + + Shortcut { + sequence: "Ctrl+K" + onActivated: { + var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); + TimelineManager.focusTimeline(); + quickSwitch.open(); + } + } + + Shortcut { + sequence: "Ctrl+Down" + onActivated: Rooms.nextRoom() + } + + Shortcut { + sequence: "Ctrl+Up" + onActivated: Rooms.previousRoom() + } + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + target: TimelineManager + onNewDeviceVerificationRequest: { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + onOpenProfile: { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + } + + Connections { + target: CallManager + onNewInviteState: { + if (CallManager.haveCallInvite && Settings.mobileMode) { + var dialog = mobileCallInviteDialog.createObject(msgView); + dialog.open(); + } + } + } + + ChatPage { + anchors.fill: parent + } + +} diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 3d2d8278..739cc007 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -31,7 +31,7 @@ ImageButton { } onClicked: { if (model.state == MtxEvent.Read) - TimelineManager.timeline.readReceiptsAction(model.id); + room.readReceiptsAction(model.id); } image: { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 09a55e60..3fa1ad8e 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -16,7 +16,7 @@ Item { height: row.height Rectangle { - color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent" + color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent" anchors.fill: row } @@ -42,7 +42,7 @@ Item { id: row anchors.rightMargin: 1 - anchors.leftMargin: avatarSize + 16 + anchors.leftMargin: Nheko.avatarSize + 16 anchors.left: parent.left anchors.right: parent.right @@ -57,7 +57,7 @@ Item { Reply { visible: model.replyTo modelData: chat.model.getDump(model.replyTo, model.id) - userColor: TimelineManager.userColor(modelData.userId, colors.base) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.base) } // actual message content @@ -101,7 +101,7 @@ Item { width: 16 sourceSize.width: 16 sourceSize.height: 16 - source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? colors.highlight : colors.buttonText) + source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) ToolTip.visible: editHovered.hovered ToolTip.text: qsTr("Edited") @@ -115,7 +115,7 @@ Item { Layout.alignment: Qt.AlignRight | Qt.AlignTop text: model.timestamp.toLocaleTimeString(Locale.ShortFormat) width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) - color: inactiveColors.text + color: Nheko.inactiveColors.text ToolTip.visible: ma.hovered ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 86287586..90e28166 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -9,382 +9,137 @@ import "./voip" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -Page { - id: timelineRoot +Item { + id: timelineView - property var colors: currentActivePalette - property var systemInactive - property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive - readonly property int avatarSize: 40 - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + property var room: null + property bool showBackButton: false - palette: colors - - FontMetrics { - id: fontMetrics + Label { + visible: !room && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: Nheko.colors.text } - EmojiPicker { - id: emojiPopup - - colors: palette - model: TimelineManager.completerFor("allemoji", "") + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 } - Component { - id: userProfileComponent + ColumnLayout { + id: timelineLayout - UserProfile { - } - - } - - Component { - id: roomSettingsComponent - - RoomSettings { - } - - } - - Component { - id: mobileCallInviteDialog - - CallInvite { - } - - } - - Component { - id: quickSwitcherComponent - - QuickSwitcher { - } - - } - - Component { - id: forwardCompleterComponent - - ForwardCompleter { - } - - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: { - var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); - TimelineManager.focusTimeline(); - quickSwitch.open(); - } - } - - Platform.Menu { - id: messageContextMenu - - property string eventId - property string link - property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender - - function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { - eventId = eventId_; - eventType = eventType_; - isEncrypted = isEncrypted_; - isEditable = isEditable_; - isSender = isSender_; - if (text_) - text = text_; - else - text = ""; - if (link_) - link = link_; - else - link = ""; - if (showAt_) - open(showAt_); - else - open(); - } - - Platform.MenuItem { - visible: messageContextMenu.text - enabled: visible - text: qsTr("&Copy") - onTriggered: Clipboard.text = messageContextMenu.text - } - - Platform.MenuItem { - visible: messageContextMenu.link - enabled: visible - text: qsTr("Copy &link location") - onTriggered: Clipboard.text = messageContextMenu.link - } - - Platform.MenuItem { - id: reactionOption - - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false - text: qsTr("Re&act") - onTriggered: emojiPopup.show(null, function(emoji) { - TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji); - }) - } - - Platform.MenuItem { - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false - text: qsTr("Repl&y") - onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) - enabled: visible - text: qsTr("&Edit") - onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - text: qsTr("Read receip&ts") - onTriggered: TimelineManager.timeline.readReceiptsAction(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") - onTriggered: { - var forwardMess = forwardCompleterComponent.createObject(timelineRoot); - forwardMess.setMessageEventId(messageContextMenu.eventId); - forwardMess.open(); - } - } - - Platform.MenuItem { - text: qsTr("&Mark as read") - } - - Platform.MenuItem { - text: qsTr("View raw message") - onTriggered: TimelineManager.timeline.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") - onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender - text: qsTr("Remo&ve message") - onTriggered: TimelineManager.timeline.redactEvent(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("&Save as") - onTriggered: TimelineManager.timeline.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") - onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventId - enabled: visible - text: qsTr("Copy link to eve&nt") - onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) - } - - } - - Rectangle { + visible: room != null anchors.fill: parent - color: colors.window - - Component { - id: deviceVerificationDialog - - DeviceVerification { - } + spacing: 0 + TopBar { + showBackButton: timelineView.showBackButton } - Connections { - target: TimelineManager - onNewDeviceVerificationRequest: { - var dialog = deviceVerificationDialog.createObject(timelineRoot, { - "flow": flow - }); - dialog.show(); - } - onOpenProfile: { - var userProfile = userProfileComponent.createObject(timelineRoot, { - "profile": profile - }); - userProfile.show(); - } + Rectangle { + Layout.fillWidth: true + height: 1 + z: 3 + color: Nheko.theme.separator } - Connections { - target: TimelineManager.timeline - onOpenRoomSettingsDialog: { - var roomSettings = roomSettingsComponent.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - } - } + Rectangle { + id: msgView + + Layout.fillWidth: true + Layout.fillHeight: true + color: Nheko.colors.base + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + StackLayout { + id: stackLayout + + currentIndex: 0 + + Connections { + function onRoomChanged() { + stackLayout.currentIndex = 0; + } + + target: timelineView + } + + MessageView { + Layout.fillWidth: true + implicitHeight: msgView.height - typingIndicator.height + } + + Loader { + source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() + } - Connections { - target: CallManager - onNewInviteState: { - if (CallManager.haveCallInvite && Settings.mobileMode) { - var dialog = mobileCallInviteDialog.createObject(msgView); - dialog.open(); } + + TypingIndicator { + id: typingIndicator + } + } + } - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: colors.text - } + CallInviteBar { + id: callInviteBar - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 + Layout.fillWidth: true z: 3 } - ColumnLayout { - id: timelineLayout - - visible: TimelineManager.timeline != null - anchors.fill: parent - spacing: 0 - - TopBar { - } - - Rectangle { - Layout.fillWidth: true - height: 1 - z: 3 - color: colors.mid - } - - Rectangle { - id: msgView - - Layout.fillWidth: true - Layout.fillHeight: true - color: colors.base - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - StackLayout { - id: stackLayout - - currentIndex: 0 - - Connections { - function onActiveTimelineChanged() { - stackLayout.currentIndex = 0; - } - - target: TimelineManager - } - - MessageView { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Loader { - source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" - onLoaded: TimelineManager.setVideoCallItem() - } - - } - - TypingIndicator { - } - - } - - } - - CallInviteBar { - id: callInviteBar - - Layout.fillWidth: true - z: 3 - } - - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } - - Rectangle { - Layout.fillWidth: true - z: 3 - height: 1 - color: colors.mid - } - - ReplyPopup { - } - - MessageInput { - } - + ActiveCallBar { + Layout.fillWidth: true + z: 3 } - NhekoDropArea { - anchors.fill: parent - roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" + Rectangle { + Layout.fillWidth: true + z: 3 + height: 1 + color: Nheko.theme.separator + } + + ReplyPopup { + } + + MessageInput { } } - PrivacyScreen { + NhekoDropArea { anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timelineLayout + roomid: room ? room.roomId() : "" } - systemInactive: SystemPalette { - colorGroup: SystemPalette.Disabled + Connections { + target: room + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } } } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 0b943ed1..30ab2e7c 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -11,16 +11,16 @@ import im.nheko 1.0 Rectangle { id: topBar - property var room: TimelineManager.timeline + property bool showBackButton: false Layout.fillWidth: true - implicitHeight: topLayout.height + 16 + implicitHeight: topLayout.height + Nheko.paddingMedium * 2 z: 3 - color: colors.window + color: Nheko.colors.window TapHandler { onSingleTapped: { - TimelineManager.timeline.openRoomSettings(); + room.openRoomSettings(); eventPoint.accepted = true; } gesturePolicy: TapHandler.ReleaseWithinBounds @@ -33,7 +33,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - anchors.margins: 8 + anchors.margins: Nheko.paddingMedium anchors.verticalCenter: parent.verticalCenter ImageButton { @@ -43,13 +43,13 @@ Rectangle { Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter - width: avatarSize - height: avatarSize - visible: TimelineManager.isNarrowView + width: Nheko.avatarSize + height: Nheko.avatarSize + visible: showBackButton image: ":/icons/icons/ui/angle-pointing-to-left.png" ToolTip.visible: hovered ToolTip.text: qsTr("Back to room list") - onClicked: TimelineManager.backToRooms() + onClicked: Rooms.resetCurrentRoom() } Avatar { @@ -57,18 +57,18 @@ Rectangle { Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" displayName: room ? room.roomName : qsTr("No room selected") - onClicked: TimelineManager.timeline.openRoomSettings() + onClicked: room.openRoomSettings() } Label { Layout.fillWidth: true Layout.column: 2 Layout.row: 0 - color: colors.text + color: Nheko.colors.text font.pointSize: fontMetrics.font.pointSize * 1.1 text: room ? room.roomName : qsTr("No room selected") maximumLineCount: 1 @@ -101,24 +101,24 @@ Rectangle { id: roomOptionsMenu Platform.MenuItem { - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false + visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") onTriggered: TimelineManager.openInviteUsersDialog() } Platform.MenuItem { text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog() + onTriggered: TimelineManager.openMemberListDialog(room.roomId()) } Platform.MenuItem { text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog() + onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId()) } Platform.MenuItem { text: qsTr("Settings") - onTriggered: TimelineManager.timeline.openRoomSettings() + onTriggered: room.openRoomSettings() } } diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml index ffe88fb6..974d1840 100644 --- a/resources/qml/TypingIndicator.qml +++ b/resources/qml/TypingIndicator.qml @@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - property var room: TimelineManager.timeline - implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Layout.fillWidth: true @@ -17,7 +15,7 @@ Item { id: typingRect visible: (room && room.typingUsers.length > 0) - color: colors.base + color: Nheko.colors.base anchors.fill: parent z: 3 @@ -29,8 +27,8 @@ Item { anchors.right: parent.right anchors.rightMargin: 10 anchors.bottom: parent.bottom - color: colors.text - text: room ? room.formatTypingUsers(room.typingUsers, colors.base) : "" + color: Nheko.colors.text + text: room ? room.formatTypingUsers(room.typingUsers, Nheko.colors.base) : "" textFormat: Text.RichText } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 21c44793..21f34f15 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -19,10 +19,10 @@ ApplicationWindow { height: 650 width: 420 minimumHeight: 420 - palette: colors - color: colors.window + palette: Nheko.colors + color: Nheko.colors.window title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") - modality: Qt.WindowModal + modality: Qt.NonModal flags: Qt.Dialog Shortcut { @@ -97,7 +97,7 @@ ApplicationWindow { readOnly: !isUsernameEditingAllowed text: profile.displayName font.pixelSize: 20 - color: TimelineManager.userColor(profile.userid, colors.window) + color: TimelineManager.userColor(profile.userid, Nheko.colors.window) font.bold: true Layout.alignment: Qt.AlignHCenter selectByMouse: true @@ -145,7 +145,7 @@ ApplicationWindow { Image { Layout.preferredHeight: 16 Layout.preferredWidth: 16 - source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : colors.buttonText) + source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText) visible: profile.userVerified != Crypto.Unverified Layout.alignment: Qt.AlignHCenter } @@ -218,7 +218,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignLeft elide: Text.ElideRight font.bold: true - color: colors.text + color: Nheko.colors.text text: model.deviceId } @@ -226,7 +226,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.alignment: Qt.AlignRight elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text text: model.deviceName } diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml new file mode 100644 index 00000000..357a7831 --- /dev/null +++ b/resources/qml/components/AdaptiveLayout.qml @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +Container { + //Component.onCompleted: { + // parent.width = Qt.binding(function() { return calculatedWidth; }) + //} + + id: container + + property bool singlePageMode: width < 800 + property int splitterGrabMargin: Nheko.paddingSmall + property int pageIndex: 0 + property Component handle + property Component handleToucharea + + anchors.fill: parent + Component.onCompleted: { + for (var i = 0; i < count - 1; i++) { + let handle_ = handle.createObject(contentChildren[i]); + let split_ = handleToucharea.createObject(contentChildren[i]); + contentChildren[i].width = Qt.binding(function() { + return split_.calculatedWidth; + }); + contentChildren[i].splitterWidth = Qt.binding(function() { + return handle_.width; + }); + } + contentChildren[count - 1].width = Qt.binding(function() { + if (container.singlePageMode) { + return container.width; + } else { + var w = container.width; + for (var i = 0; i < count - 1; i++) { + if (contentChildren[i].width) + w = w - contentChildren[i].width; + + } + return w; + } + }); + contentChildren[count - 1].splitterWidth = 0; + for (var i = 0; i < count; i++) { + contentChildren[i].height = Qt.binding(function() { + return container.height; + }); + contentChildren[i].children[0].height = Qt.binding(function() { + return container.height; + }); + } + } + + handle: Rectangle { + z: 3 + color: Nheko.theme.separator + height: container.height + width: visible ? 1 : 0 + anchors.right: parent.right + } + + handleToucharea: Item { + id: splitter + + property int minimumWidth: parent.minimumWidth + property int maximumWidth: parent.maximumWidth + property int collapsedWidth: parent.collapsedWidth + property bool collapsible: parent.collapsible + property int calculatedWidth: { + if (!visible) + return 0; + else if (container.singlePageMode) + return container.width; + else + return (collapsible && x < minimumWidth) ? collapsedWidth : x; + } + + //visible: !container.singlePageMode + enabled: !container.singlePageMode + height: container.height + width: 1 + x: parent.preferredWidth + z: 3 + + CursorShape { + height: parent.height + width: container.splitterGrabMargin * 2 + x: -container.splitterGrabMargin + cursorShape: Qt.SizeHorCursor + } + + DragHandler { + id: dragHandler + + enabled: !container.singlePageMode + xAxis.enabled: true + yAxis.enabled: false + xAxis.minimum: splitter.minimumWidth - 1 + xAxis.maximum: splitter.maximumWidth + margin: container.splitterGrabMargin + //dragThreshold: 0 + grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.ApprovesTakeOverByHandlersOfSameType + //cursorShape: Qt.SizeHorCursor + onActiveChanged: { + if (!active) + splitter.parent.preferredWidth = splitter.x; + + } + } + + HoverHandler { + //cursorShape: Qt.SizeHorCursor + + enabled: !container.singlePageMode + margin: container.splitterGrabMargin + } + + } + + contentItem: ListView { + id: view + + model: container.contentModel + snapMode: ListView.SnapOneItem + orientation: ListView.Horizontal + highlightRangeMode: ListView.StrictlyEnforceRange + interactive: false + highlightMoveDuration: container.singlePageMode ? 200 : 0 + currentIndex: container.singlePageMode ? container.pageIndex : 0 + } + +} diff --git a/resources/qml/components/AdaptiveLayoutElement.qml b/resources/qml/components/AdaptiveLayoutElement.qml new file mode 100644 index 00000000..a4aec72e --- /dev/null +++ b/resources/qml/components/AdaptiveLayoutElement.qml @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.12 + +Item { + property int minimumWidth: 100 + property int maximumWidth: 400 + property int collapsedWidth: 40 + property bool collapsible: true + property bool collapsed: width < minimumWidth + property int splitterWidth: 1 + property int preferredWidth: 100 + + Component.onCompleted: { + children[0].width = Qt.binding(() => { + return parent.singlePageMode ? parent.width : width - splitterWidth; + }); + children[0].height = Qt.binding(() => { + return parent.height; + }); + } +} diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index e883ddbb..0392c73a 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -20,7 +20,7 @@ Item { Rectangle { id: button - color: colors.light + color: Nheko.colors.light radius: 22 height: 44 width: 44 @@ -34,7 +34,7 @@ Item { } TapHandler { - onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) + onSingleTapped: room.saveMedia(model.data.id) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -55,7 +55,7 @@ Item { text: model.data.filename textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } Text { @@ -65,7 +65,7 @@ Item { text: model.data.filesize textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } } @@ -73,7 +73,7 @@ Item { } Rectangle { - color: colors.alternateBase + color: Nheko.colors.alternateBase z: -1 radius: 10 height: row.height + 24 diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 8fcf3f82..ce8e779c 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -9,17 +9,17 @@ Item { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 5 : 3 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor - height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) + height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight) + width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth) Image { id: blurhash anchors.fill: parent visible: img.status != Image.Ready - source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) + source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit sourceSize.width: parent.width @@ -61,7 +61,7 @@ Item { width: parent.width implicitHeight: imgcaption.implicitHeight anchors.bottom: overlay.bottom - color: colors.window + color: Nheko.colors.window opacity: 0.75 } @@ -74,7 +74,7 @@ Item { verticalAlignment: Text.AlignVCenter // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 text: model.data.filename ? model.data.filename : model.data.body - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index d278a586..9e076a7a 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -58,7 +58,7 @@ Item { NoticeMessage { formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: TimelineManager.userColor(modelData.userId, colors.window) + color: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } } @@ -207,7 +207,7 @@ Item { roleValue: MtxEvent.PowerLevels NoticeMessage { - text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) + text: room.formatPowerLevelEvent(model.data.id) } } @@ -216,7 +216,7 @@ Item { roleValue: MtxEvent.RoomJoinRules NoticeMessage { - text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) + text: room.formatJoinRuleEvent(model.data.id) } } @@ -225,7 +225,7 @@ Item { roleValue: MtxEvent.RoomHistoryVisibility NoticeMessage { - text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) + text: room.formatHistoryVisibilityEvent(model.data.id) } } @@ -234,7 +234,7 @@ Item { roleValue: MtxEvent.RoomGuestAccess NoticeMessage { - text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) + text: room.formatGuestAccessEvent(model.data.id) } } @@ -243,7 +243,7 @@ Item { roleValue: MtxEvent.Member NoticeMessage { - text: TimelineManager.timeline.formatMemberEvent(model.data.id) + text: room.formatMemberEvent(model.data.id) } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index 4f8fbfd2..2af41bb2 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import im.nheko 1.0 + TextMessage { font.italic: true - color: colors.buttonText + color: Nheko.colors.buttonText } diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml index a3fa0d9e..248d91da 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml @@ -4,16 +4,17 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 +import im.nheko 1.0 Label { - color: colors.brightText + color: Nheko.colors.brightText horizontalAlignment: Text.AlignHCenter height: contentHeight * 1.2 width: contentWidth * 1.2 background: Rectangle { radius: parent.height / 2 - color: colors.alternateBase + color: Nheko.colors.alternateBase } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index addbc7e7..c4fc6cc3 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -3,9 +3,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later import ".." +import im.nheko 1.0 MatrixText { text: qsTr("unimplemented event: ") + model.data.typeString width: parent ? parent.width : undefined - color: inactiveColors.text + color: Nheko.inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index b62c90df..83864db9 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -13,7 +13,7 @@ Rectangle { id: bg radius: 10 - color: colors.alternateBase + color: Nheko.colors.alternateBase height: Math.round(content.height + 24) width: parent ? parent.width : undefined @@ -29,11 +29,11 @@ Rectangle { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor visible: model.data.type == MtxEvent.VideoMessage - height: tooHigh ? timelineRoot.height / divisor : tempHeight - width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth + height: tooHigh ? timelineView.height / divisor : tempHeight + width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth Image { anchors.fill: parent @@ -58,7 +58,7 @@ Rectangle { id: positionText text: "--:--:--" - color: colors.text + color: Nheko.colors.text } Slider { @@ -92,14 +92,14 @@ Rectangle { to: media.duration onMoved: media.seek(value) onValueChanged: updatePositionTexts() - palette: colors + palette: Nheko.colors } Text { id: durationText text: "--:--:--" - color: colors.text + color: Nheko.colors.text } } @@ -112,7 +112,7 @@ Rectangle { id: button Layout.alignment: Qt.AlignVCenter - //color: colors.window + //color: Nheko.colors.window //radius: 22 height: 32 width: 32 @@ -121,7 +121,7 @@ Rectangle { onClicked: { switch (button.state) { case "": - TimelineManager.timeline.cacheMedia(model.data.id); + room.cacheMedia(model.data.id); break; case "stopped": media.play(); @@ -174,7 +174,7 @@ Rectangle { } Connections { - target: TimelineManager.timeline + target: room onMediaCached: { if (mxcUrl == model.data.url) { media.source = cacheUrl; @@ -194,7 +194,7 @@ Rectangle { Layout.fillWidth: true text: model.data.body elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } Text { @@ -202,7 +202,7 @@ Rectangle { text: model.data.filesize textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 603d0cf6..b8c33539 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -33,7 +33,7 @@ Item { anchors.top: replyContainer.top anchors.bottom: replyContainer.bottom width: 4 - color: TimelineManager.userColor(reply.modelData.userId, colors.window) + color: TimelineManager.userColor(reply.modelData.userId, Nheko.colors.window) } Column { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index f44165b4..cd46f8ca 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -9,9 +9,26 @@ MatrixText { property string formatted: model.data.formattedBody property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body - text: "" + formatted.replace("
", "
")
+    // table border-collapse doesn't seem to work
+    text: "
+    
+    " + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
     width: parent ? parent.width : undefined
-    height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
+    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
diff --git a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
index ae62c334..a6a7f027 100644
--- a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
+++ b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
@@ -21,7 +21,7 @@ Pane {
             Layout.fillWidth: true
             wrapMode: Text.Wrap
             text: qsTr("Waiting for other side to complete verification.")
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index 41ed8d57..e2c66c5a 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -15,8 +15,8 @@ ApplicationWindow {
     onClosing: TimelineManager.removeVerificationFlow(flow)
     title: stack.currentItem.title
     flags: Qt.Dialog
-    modality: Qt.WindowModal
-    palette: colors
+    modality: Qt.NonModal
+    palette: Nheko.colors
     height: stack.implicitHeight
     width: stack.implicitWidth
     x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml
index a387756d..aafdc043 100644
--- a/resources/qml/device-verification/DigitVerification.qml
+++ b/resources/qml/device-verification/DigitVerification.qml
@@ -19,7 +19,7 @@ Pane {
             Layout.fillWidth: true
             wrapMode: Text.Wrap
             text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
@@ -29,19 +29,19 @@ Pane {
             Label {
                 font.pixelSize: Qt.application.font.pixelSize * 2
                 text: flow.sasList[0]
-                color: colors.text
+                color: Nheko.colors.text
             }
 
             Label {
                 font.pixelSize: Qt.application.font.pixelSize * 2
                 text: flow.sasList[1]
-                color: colors.text
+                color: Nheko.colors.text
             }
 
             Label {
                 font.pixelSize: Qt.application.font.pixelSize * 2
                 text: flow.sasList[2]
-                color: colors.text
+                color: Nheko.colors.text
             }
 
         }
diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml
index be9e3938..b6e8484f 100644
--- a/resources/qml/device-verification/EmojiVerification.qml
+++ b/resources/qml/device-verification/EmojiVerification.qml
@@ -19,7 +19,7 @@ Pane {
             Layout.fillWidth: true
             wrapMode: Text.Wrap
             text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
@@ -374,13 +374,13 @@ Pane {
                             text: col.emoji.emoji
                             font.pixelSize: Qt.application.font.pixelSize * 2
                             font.family: Settings.emojiFont
-                            color: colors.text
+                            color: Nheko.colors.text
                         }
 
                         Label {
                             Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
                             text: col.emoji.description
-                            color: colors.text
+                            color: Nheko.colors.text
                         }
 
                     }
diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
index 5c71b02e..71ef8b9b 100644
--- a/resources/qml/device-verification/Failed.qml
+++ b/resources/qml/device-verification/Failed.qml
@@ -38,7 +38,7 @@ Pane {
                     return "Unknown verification error.";
                 }
             }
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
index e8589cf7..5ae2d25b 100644
--- a/resources/qml/device-verification/NewVerificationRequest.qml
+++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -35,7 +35,7 @@ Pane {
                         return qsTr("Your device (%1) has requested to be verified.").arg(flow.deviceId);
                 }
             }
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml
index f2657b12..70cfafaf 100644
--- a/resources/qml/device-verification/Success.qml
+++ b/resources/qml/device-verification/Success.qml
@@ -5,6 +5,7 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.10
+import im.nheko 1.0
 
 Pane {
     property string title: qsTr("Successful Verification")
@@ -20,7 +21,7 @@ Pane {
             Layout.fillWidth: true
             wrapMode: Text.Wrap
             text: qsTr("Verification successful! Both sides verified their devices!")
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml
index 3bfa153d..c521503b 100644
--- a/resources/qml/device-verification/Waiting.qml
+++ b/resources/qml/device-verification/Waiting.qml
@@ -30,13 +30,13 @@ Pane {
                     return qsTr("Waiting for other side to complete the verification process.");
                 }
             }
-            color: colors.text
+            color: Nheko.colors.text
             verticalAlignment: Text.AlignVCenter
         }
 
         BusyIndicator {
             Layout.alignment: Qt.AlignHCenter
-            palette: colors
+            palette: Nheko.colors
         }
 
         RowLayout {
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
new file mode 100644
index 00000000..134b78a3
--- /dev/null
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.12
+import QtQuick.Controls 2.5
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: inputDialog
+
+    property alias prompt: promptLabel.text
+    property var onAccepted: undefined
+
+    modality: Qt.NonModal
+    flags: Qt.Dialog
+    width: 350
+    height: fontMetrics.lineSpacing * 7
+
+    ColumnLayout {
+        anchors.margins: Nheko.paddingLarge
+        anchors.fill: parent
+
+        Label {
+            id: promptLabel
+
+            color: Nheko.colors.text
+        }
+
+        MatrixTextField {
+            id: statusInput
+
+            Layout.fillWidth: true
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+        onAccepted: {
+            if (inputDialog.onAccepted)
+                inputDialog.onAccepted(statusInput.text);
+
+            inputDialog.close();
+        }
+        onRejected: {
+            inputDialog.close();
+        }
+    }
+
+}
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index cec51d75..5f4d23d3 100644
--- a/resources/qml/emoji/EmojiButton.qml
+++ b/resources/qml/emoji/EmojiButton.qml
@@ -17,7 +17,7 @@ ImageButton {
 
     image: ":/icons/icons/ui/smile.png"
     onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
-        TimelineManager.queueReactionMessage(event_id, emoji);
+        room.input.reaction(event_id, emoji);
         TimelineManager.focusMessageInput();
     })
 }
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 4aad832d..efcdc2cf 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -18,9 +18,9 @@ Menu {
     property alias model: gridView.model
     property var textArea
     property string emojiCategory: "people"
-    property real highlightHue: colors.highlight.hslHue
-    property real highlightSat: colors.highlight.hslSaturation
-    property real highlightLight: colors.highlight.hslLightness
+    property real highlightHue: Nheko.colors.highlight.hslHue
+    property real highlightSat: Nheko.colors.highlight.hslSaturation
+    property real highlightLight: Nheko.colors.highlight.hslLightness
 
     function show(showAt, callback) {
         console.debug("Showing emojiPicker");
@@ -80,7 +80,7 @@ Menu {
                 id: clearSearch
 
                 visible: emojiSearch.text !== ''
-                icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
+                icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
                 focusPolicy: Qt.NoFocus
                 onClicked: emojiSearch.clear()
 
@@ -146,7 +146,7 @@ Menu {
 
                 background: Rectangle {
                     anchors.fill: parent
-                    color: hovered ? colors.highlight : 'transparent'
+                    color: hovered ? Nheko.colors.highlight : 'transparent'
                     radius: 5
                 }
 
@@ -163,7 +163,7 @@ Menu {
             visible: emojiSearch.text === ''
             Layout.fillWidth: true
             Layout.preferredHeight: 1
-            color: emojiPopup.colors.alternateBase
+            color: emojiPopup.Nheko.colors.alternateBase
         }
 
         // Category picker row
@@ -265,14 +265,14 @@ Menu {
                         fillMode: Image.Pad
                         sourceSize.width: 32
                         sourceSize.height: 32
-                        source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText)
+                        source: "image://colorimage/" + model.image + "?" + (hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
                     }
 
                     background: Rectangle {
                         anchors.fill: parent
                         color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent'
                         radius: 5
-                        border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent'
+                        border.color: emojiPopup.model.category === model.category ? Nheko.colors.highlight : 'transparent'
                     }
 
                 }
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 68d3bc4a..3106c382 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -31,11 +31,11 @@ Rectangle {
         anchors.leftMargin: 8
 
         Avatar {
-            width: avatarSize
-            height: avatarSize
+            width: Nheko.avatarSize
+            height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
diff --git a/resources/qml/voip/CallDevices.qml b/resources/qml/voip/CallDevices.qml
index 11644797..824bb2e0 100644
--- a/resources/qml/voip/CallDevices.qml
+++ b/resources/qml/voip/CallDevices.qml
@@ -9,7 +9,7 @@ import im.nheko 1.0
 
 Popup {
     modal: true
-    palette: colors
+    palette: Nheko.colors
     // only set the anchors on Qt 5.12 or higher
     // see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
     Component.onCompleted: {
@@ -31,7 +31,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: 22
                     Layout.preferredHeight: 22
-                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -49,7 +49,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: 22
                     Layout.preferredHeight: 22
-                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -81,8 +81,8 @@ Popup {
     }
 
     background: Rectangle {
-        color: colors.window
-        border.color: colors.windowText
+        color: Nheko.colors.window
+        border.color: Nheko.colors.windowText
     }
 
 }
diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml
index 15d987e7..1b57976d 100644
--- a/resources/qml/voip/CallInvite.qml
+++ b/resources/qml/voip/CallInvite.qml
@@ -12,7 +12,7 @@ Popup {
     closePolicy: Popup.NoAutoClose
     width: parent.width
     height: parent.height
-    palette: colors
+    palette: Nheko.colors
 
     Component {
         id: deviceError
@@ -41,7 +41,7 @@ Popup {
             Layout.topMargin: msgView.height / 25
             text: CallManager.callParty
             font.pointSize: fontMetrics.font.pointSize * 2
-            color: colors.windowText
+            color: Nheko.colors.windowText
         }
 
         Avatar {
@@ -62,14 +62,14 @@ Popup {
                 Layout.alignment: Qt.AlignCenter
                 Layout.preferredWidth: msgView.height / 10
                 Layout.preferredHeight: msgView.height / 10
-                source: "image://colorimage/" + image + "?" + colors.windowText
+                source: "image://colorimage/" + image + "?" + Nheko.colors.windowText
             }
 
             Label {
                 Layout.alignment: Qt.AlignCenter
                 text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
                 font.pointSize: fontMetrics.font.pointSize * 2
-                color: colors.windowText
+                color: Nheko.colors.windowText
             }
 
         }
@@ -88,7 +88,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: deviceCombos.imageSize
                     Layout.preferredHeight: deviceCombos.imageSize
-                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -107,7 +107,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: deviceCombos.imageSize
                     Layout.preferredHeight: deviceCombos.imageSize
-                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -194,8 +194,8 @@ Popup {
     }
 
     background: Rectangle {
-        color: colors.window
-        border.color: colors.windowText
+        color: Nheko.colors.window
+        border.color: Nheko.colors.windowText
     }
 
 }
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index fe3f791f..2d8e3040 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -38,11 +38,11 @@ Rectangle {
         anchors.leftMargin: 8
 
         Avatar {
-            width: avatarSize
-            height: avatarSize
+            width: Nheko.avatarSize
+            height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
@@ -88,7 +88,7 @@ Rectangle {
             Layout.rightMargin: 4
             icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
             text: qsTr("Accept")
-            palette: colors
+            palette: Nheko.colors
             onClicked: {
                 if (CallManager.mics.length == 0) {
                     var dialog = deviceError.createObject(timelineRoot, {
@@ -121,7 +121,7 @@ Rectangle {
             Layout.rightMargin: 16
             icon.source: "qrc:/icons/icons/ui/end-call.png"
             text: qsTr("Decline")
-            palette: colors
+            palette: Nheko.colors
             onClicked: {
                 CallManager.hangUp();
             }
diff --git a/resources/qml/voip/DeviceError.qml b/resources/qml/voip/DeviceError.qml
index 05cfd409..47ded50a 100644
--- a/resources/qml/voip/DeviceError.qml
+++ b/resources/qml/voip/DeviceError.qml
@@ -24,19 +24,19 @@ Popup {
         Image {
             Layout.preferredWidth: 16
             Layout.preferredHeight: 16
-            source: "image://colorimage/" + image + "?" + colors.windowText
+            source: "image://colorimage/" + image + "?" + Nheko.colors.windowText
         }
 
         Label {
             text: errorString
-            color: colors.windowText
+            color: Nheko.colors.windowText
         }
 
     }
 
     background: Rectangle {
-        color: colors.window
-        border.color: colors.windowText
+        color: Nheko.colors.window
+        border.color: Nheko.colors.windowText
     }
 
 }
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index c9aa8ea1..97e39e02 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -17,7 +17,7 @@ Popup {
             anchors.centerIn = parent;
 
     }
-    palette: colors
+    palette: Nheko.colors
 
     Component {
         id: deviceError
@@ -45,8 +45,8 @@ Popup {
             Layout.leftMargin: 8
 
             Label {
-                text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName)
-                color: colors.windowText
+                text: qsTr("Place a call to %1?").arg(room.roomName)
+                color: Nheko.colors.windowText
             }
 
             Item {
@@ -75,11 +75,11 @@ Popup {
 
             Avatar {
                 Layout.rightMargin: cameraCombo.visible ? 16 : 64
-                width: avatarSize
-                height: avatarSize
-                url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
-                displayName: TimelineManager.timeline.roomName
-                onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+                width: Nheko.avatarSize
+                height: Nheko.avatarSize
+                url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
+                displayName: room.roomName
+                onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
             }
 
             Button {
@@ -88,7 +88,7 @@ Popup {
                 onClicked: {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE);
+                        CallManager.sendInvite(room.roomId(), CallType.VOICE);
                         close();
                     }
                 }
@@ -102,7 +102,7 @@ Popup {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
                         Settings.camera = cameraCombo.currentText;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO);
+                        CallManager.sendInvite(room.roomId(), CallType.VIDEO);
                         close();
                     }
                 }
@@ -139,7 +139,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: 22
                     Layout.preferredHeight: 22
-                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -160,7 +160,7 @@ Popup {
                 Image {
                     Layout.preferredWidth: 22
                     Layout.preferredHeight: 22
-                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
+                    source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText
                 }
 
                 ComboBox {
@@ -177,8 +177,8 @@ Popup {
     }
 
     background: Rectangle {
-        color: colors.window
-        border.color: colors.windowText
+        color: Nheko.colors.window
+        border.color: Nheko.colors.windowText
     }
 
 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index af473c04..a10057b2 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -18,7 +18,7 @@ Popup {
 
         frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate);
     }
-    palette: colors
+    palette: Nheko.colors
 
     ColumnLayout {
         Label {
@@ -27,8 +27,8 @@ Popup {
             Layout.leftMargin: 8
             Layout.rightMargin: 8
             Layout.alignment: Qt.AlignLeft
-            text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
-            color: colors.windowText
+            text: qsTr("Share desktop with %1?").arg(room.roomName)
+            color: Nheko.colors.windowText
         }
 
         RowLayout {
@@ -39,7 +39,7 @@ Popup {
             Label {
                 Layout.alignment: Qt.AlignLeft
                 text: qsTr("Window:")
-                color: colors.windowText
+                color: Nheko.colors.windowText
             }
 
             ComboBox {
@@ -59,7 +59,7 @@ Popup {
             Label {
                 Layout.alignment: Qt.AlignLeft
                 text: qsTr("Frame rate:")
-                color: colors.windowText
+                color: Nheko.colors.windowText
             }
 
             ComboBox {
@@ -136,7 +136,7 @@ Popup {
                         Settings.screenSharePiP = pipCheckBox.checked;
                         Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
                         Settings.screenShareHideCursor = hideCursorCheckBox.checked;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex);
+                        CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
                         close();
                     }
                 }
@@ -161,8 +161,8 @@ Popup {
     }
 
     background: Rectangle {
-        color: colors.window
-        border.color: colors.windowText
+        color: Nheko.colors.window
+        border.color: Nheko.colors.windowText
     }
 
 }
diff --git a/resources/res.qrc b/resources/res.qrc
index 304493b6..53c74ae3 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -123,11 +123,16 @@
     
         qtquickcontrols2.conf
 
+        qml/Root.qml
+        qml/ChatPage.qml
+        qml/CommunitiesList.qml
+        qml/RoomList.qml
         qml/TimelineView.qml
         qml/Avatar.qml
         qml/Completer.qml
         qml/EncryptionIndicator.qml
         qml/ImageButton.qml
+        qml/ElidedLabel.qml
         qml/MatrixText.qml
         qml/MatrixTextField.qml
         qml/ToggleButton.qml
@@ -164,6 +169,7 @@
         qml/device-verification/NewVerificationRequest.qml
         qml/device-verification/Failed.qml
         qml/device-verification/Success.qml
+        qml/dialogs/InputDialog.qml
         qml/ui/Ripple.qml
         qml/voip/ActiveCallBar.qml
         qml/voip/CallDevices.qml
@@ -173,6 +179,8 @@
         qml/voip/PlaceCall.qml
         qml/voip/ScreenShare.qml
         qml/voip/VideoCall.qml
+        qml/components/AdaptiveLayout.qml
+        qml/components/AdaptiveLayoutElement.qml
     
     
         media/ring.ogg
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 24b2bc24..0d75ac51 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -253,6 +253,8 @@ Cache::setup()
         outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
 
         txn.commit();
+
+        databaseReady_ = true;
 }
 
 void
@@ -788,6 +790,7 @@ Cache::nextBatchToken()
 void
 Cache::deleteData()
 {
+        this->databaseReady_ = false;
         // TODO: We need to remove the env_ while not accepting new requests.
         lmdb::dbi_close(env_, syncStateDb_);
         lmdb::dbi_close(env_, roomsDb_);
@@ -2042,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
         return fallbackDesc;
 }
 
-std::map
+QHash
 Cache::invites()
 {
-        std::map result;
+        QHash result;
 
         auto txn    = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
         auto cursor = lmdb::cursor::open(txn, invitesDb_);
 
-        std::string_view room_id, unused;
+        std::string_view room_id, room_data;
 
-        while (cursor.get(room_id, unused, MDB_NEXT))
-                result.emplace(QString::fromStdString(std::string(room_id)), true);
+        while (cursor.get(room_id, room_data, MDB_NEXT)) {
+                try {
+                        RoomInfo tmp     = json::parse(room_data);
+                        tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
+                        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+                } catch (const json::exception &e) {
+                        nhlog::db()->warn("failed to parse room info for invite: "
+                                          "room_id ({}), {}: {}",
+                                          room_id,
+                                          std::string(room_data),
+                                          e.what());
+                }
+        }
 
         cursor.close();
-        txn.commit();
+
+        return result;
+}
+
+std::optional
+Cache::invite(std::string_view roomid)
+{
+        std::optional result;
+
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+        std::string_view room_data;
+
+        if (invitesDb_.get(txn, roomid, room_data)) {
+                try {
+                        RoomInfo tmp     = json::parse(room_data);
+                        tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
+                        result           = std::move(tmp);
+                } catch (const json::exception &e) {
+                        nhlog::db()->warn("failed to parse room info for invite: "
+                                          "room_id ({}), {}: {}",
+                                          roomid,
+                                          std::string(room_data),
+                                          e.what());
+                }
+        }
 
         return result;
 }
@@ -2426,7 +2465,7 @@ Cache::joinedRooms()
 std::optional
 Cache::getMember(const std::string &room_id, const std::string &user_id)
 {
-        if (user_id.empty())
+        if (user_id.empty() || !env_.handle())
                 return std::nullopt;
 
         try {
@@ -2440,7 +2479,8 @@ Cache::getMember(const std::string &room_id, const std::string &user_id)
                         return m;
                 }
         } catch (std::exception &e) {
-                nhlog::db()->warn("Failed to read member ({}): {}", user_id, e.what());
+                nhlog::db()->warn(
+                  "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
         }
         return std::nullopt;
 }
@@ -3412,6 +3452,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
 
                         if (!updateToWrite.master_keys.keys.empty() &&
                             update.master_keys.keys != updateToWrite.master_keys.keys) {
+                                nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
+                                                   user,
+                                                   updateToWrite.master_keys.keys.size(),
+                                                   update.master_keys.keys.size());
                                 updateToWrite.master_key_changed = true;
                         }
 
@@ -3466,6 +3510,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
                         }
                 }
         }
+
         for (auto &[user_id, update] : updates) {
                 (void)update;
                 if (user_id == local_user) {
@@ -3473,9 +3518,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
                                 (void)status;
                                 emit verificationStatusChanged(user);
                         }
-                } else {
-                        emit verificationStatusChanged(user_id);
                 }
+                emit verificationStatusChanged(user_id);
         }
 }
 
@@ -3549,10 +3593,23 @@ Cache::query_keys(const std::string &user_id,
                 last_changed = cache_->last_changed;
         req.token = last_changed;
 
+        // use context object so that we can disconnect again
+        QObject *context{new QObject(this)};
+        QObject::connect(this,
+                         &Cache::verificationStatusChanged,
+                         context,
+                         [cb, user_id, context_ = context](std::string updated_user) mutable {
+                                 if (user_id == updated_user) {
+                                         context_->deleteLater();
+                                         auto keys = cache::userKeys(user_id);
+                                         cb(keys.value_or(UserKeyCache{}), {});
+                                 }
+                         });
+
         http::client()->query_keys(
           req,
-          [cb, user_id, last_changed](const mtx::responses::QueryKeys &res,
-                                      mtx::http::RequestErr err) {
+          [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
+                                            mtx::http::RequestErr err) {
                   if (err) {
                           nhlog::net()->warn("failed to query device keys: {},{}",
                                              mtx::errors::to_string(err->matrix_error.errcode),
@@ -3561,10 +3618,7 @@ Cache::query_keys(const std::string &user_id,
                           return;
                   }
 
-                  cache::updateUserKeys(last_changed, res);
-
-                  auto keys = cache::userKeys(user_id);
-                  cb(keys.value_or(UserKeyCache{}), err);
+                  emit userKeysUpdate(last_changed, res);
           });
 }
 
@@ -3999,6 +4053,8 @@ avatarUrl(const QString &room_id, const QString &user_id)
 mtx::presence::PresenceState
 presenceState(const std::string &user_id)
 {
+        if (!instance_)
+                return {};
         return instance_->presenceState(user_id);
 }
 std::string
@@ -4049,7 +4105,7 @@ roomInfo(bool withInvites)
 {
         return instance_->roomInfo(withInvites);
 }
-std::map
+QHash
 invites()
 {
         return instance_->invites();
diff --git a/src/Cache.h b/src/Cache.h
index 427dbafc..74ec9695 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -62,7 +62,7 @@ joinedRooms();
 
 QMap
 roomInfo(bool withInvites = true);
-std::map
+QHash
 invites();
 
 //! Calculate & return the name of the room.
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index c449f013..f7d6f0e2 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -50,6 +50,19 @@ struct DescInfo
         QDateTime datetime;
 };
 
+inline bool
+operator==(const DescInfo &a, const DescInfo &b)
+{
+        return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) ==
+               std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
+}
+inline bool
+operator!=(const DescInfo &a, const DescInfo &b)
+{
+        return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) !=
+               std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
+}
+
 //! UI info associated with a room.
 struct RoomInfo
 {
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 356c6e42..f2911622 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -70,7 +70,8 @@ public:
 
         QMap roomInfo(bool withInvites = true);
         std::optional getRoomAliases(const std::string &roomid);
-        std::map invites();
+        QHash invites();
+        std::optional invite(std::string_view roomid);
 
         //! Calculate & return the name of the room.
         QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
@@ -100,6 +101,7 @@ public:
 
         void saveState(const mtx::responses::Sync &res);
         bool isInitialized();
+        bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
 
         std::string nextBatchToken();
 
@@ -620,6 +622,8 @@ private:
         QString cacheDirectory_;
 
         VerificationStorage verification_storage;
+
+        bool databaseReady_ = false;
 };
 
 namespace cache {
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 1a1c1044..0f16f205 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -23,10 +23,6 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "Olm.h"
-#include "RoomList.h"
-#include "SideBarActions.h"
-#include "Splitter.h"
-#include "UserInfoWidget.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 #include "ui/OverlayModal.h"
@@ -36,7 +32,6 @@
 #include "notifications/Manager.h"
 
 #include "dialogs/ReadReceipts.h"
-#include "popups/UserMentions.h"
 #include "timeline/TimelineViewManager.h"
 
 #include "blurhash.hpp"
@@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         topLayout_->setSpacing(0);
         topLayout_->setMargin(0);
 
-        communitiesList_ = new CommunitiesList(this);
-        topLayout_->addWidget(communitiesList_);
-
-        splitter = new Splitter(this);
-        splitter->setHandleWidth(0);
-
-        topLayout_->addWidget(splitter);
-
-        // SideBar
-        sideBar_ = new QFrame(this);
-        sideBar_->setObjectName("sideBar");
-        sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal);
-        sideBarLayout_ = new QVBoxLayout(sideBar_);
-        sideBarLayout_->setSpacing(0);
-        sideBarLayout_->setMargin(0);
-
-        sideBarTopWidget_ = new QWidget(sideBar_);
-        sidebarActions_   = new SideBarActions(this);
-        connect(
-          sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
-        connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom);
-        connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom);
-
-        user_info_widget_ = new UserInfoWidget(sideBar_);
-        connect(user_info_widget_, &UserInfoWidget::openGlobalUserProfile, this, [this]() {
-                UserProfile *userProfile = new UserProfile("", utils::localUser(), view_manager_);
-                emit view_manager_->openProfile(userProfile);
-        });
-
-        user_mentions_popup_ = new popups::UserMentions();
-        room_list_           = new RoomList(userSettings, sideBar_);
-        connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom);
-
-        sideBarLayout_->addWidget(user_info_widget_);
-        sideBarLayout_->addWidget(room_list_);
-        sideBarLayout_->addWidget(sidebarActions_);
-
-        sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_);
-        sideBarTopWidgetLayout_->setSpacing(0);
-        sideBarTopWidgetLayout_->setMargin(0);
-
-        // Content
-        content_ = new QFrame(this);
-        content_->setObjectName("mainContent");
-        contentLayout_ = new QVBoxLayout(content_);
-        contentLayout_->setSpacing(0);
-        contentLayout_->setMargin(0);
-
         view_manager_ = new TimelineViewManager(callManager_, this);
 
-        contentLayout_->addWidget(view_manager_->getWidget());
-
-        // Splitter
-        splitter->addWidget(sideBar_);
-        splitter->addWidget(content_);
-        splitter->restoreSizes(parent->width());
+        topLayout_->addWidget(view_manager_->getWidget());
 
         connect(this,
                 &ChatPage::downloadedSecrets,
@@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 trySync();
         });
 
-        connect(
-          new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
-                  if (isVisible())
-                          room_list_->nextRoom();
-          });
-        connect(
-          new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() {
-                  if (isVisible())
-                          room_list_->previousRoom();
-          });
-
         connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
         connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
                 if (http::client()->access_token().empty()) {
@@ -185,10 +116,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
 
         connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
 
-        connect(
-          view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList);
         connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
-                const auto room_id = current_room_.toStdString();
+                const auto room_id = currentRoom().toStdString();
 
                 for (int ii = 0; ii < users.size(); ++ii) {
                         QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
@@ -211,36 +140,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 }
         });
 
-        connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) {
-                this->current_room_ = room_id;
-        });
-        connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
-        connect(
-          room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
-
-        connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
-                joinRoom(room_id);
-                room_list_->removeRoom(room_id, currentRoom() == room_id);
-        });
-
-        connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) {
-                leaveRoom(room_id);
-                room_list_->removeRoom(room_id, currentRoom() == room_id);
-        });
-
-        connect(view_manager_,
-                &TimelineViewManager::updateRoomsLastMessage,
-                room_list_,
-                &RoomList::updateRoomDescription);
-
-        connect(room_list_,
-                SIGNAL(totalUnreadMessageCountUpdated(int)),
-                this,
-                SIGNAL(unreadMessages(int)));
-
-        connect(
-          this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
-
         connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
         connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
         connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
@@ -255,60 +154,31 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                         }
                 });
 
-        connect(communitiesList_,
-                &CommunitiesList::communityChanged,
-                this,
-                [this](const QString &groupId) {
-                        current_community_ = groupId;
-
-                        if (groupId == "world") {
-                                auto hidden = communitiesList_->hiddenTagsAndCommunities();
-                                std::set roomsToHide = communitiesList_->roomList(groupId);
-                                for (const auto &hiddenTag : hidden) {
-                                        auto temp = communitiesList_->roomList(hiddenTag);
-                                        roomsToHide.insert(temp.begin(), temp.end());
-                                }
-
-                                room_list_->removeFilter(roomsToHide);
-                        } else {
-                                auto hidden = communitiesList_->hiddenTagsAndCommunities();
-                                hidden.erase(current_community_);
-
-                                auto roomsToShow = communitiesList_->roomList(groupId);
-                                for (const auto &hiddenTag : hidden) {
-                                        for (const auto &r : communitiesList_->roomList(hiddenTag))
-                                                roomsToShow.erase(r);
-                                }
-
-                                room_list_->applyFilter(roomsToShow);
-                        }
-                });
-
         connect(¬ificationsManager,
                 &NotificationsManager::notificationClicked,
                 this,
                 [this](const QString &roomid, const QString &eventid) {
                         Q_UNUSED(eventid)
-                        room_list_->highlightSelectedRoom(roomid);
+                        view_manager_->rooms()->setCurrentRoom(roomid);
                         activateWindow();
                 });
         connect(¬ificationsManager,
                 &NotificationsManager::sendNotificationReply,
                 this,
                 [this](const QString &roomid, const QString &eventid, const QString &body) {
+                        view_manager_->rooms()->setCurrentRoom(roomid);
                         view_manager_->queueReply(roomid, eventid, body);
-                        room_list_->highlightSelectedRoom(roomid);
                         activateWindow();
                 });
 
-        setGroupViewState(userSettings_->groupView());
+        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+                // ensure the qml context is shutdown before we destroy all other singletons
+                // Otherwise Qml will try to access the room list or settings, after they have been
+                // destroyed
+                topLayout_->removeWidget(view_manager_->getWidget());
+                delete view_manager_->getWidget();
+        });
 
-        connect(userSettings_.data(),
-                &UserSettings::groupViewStateChanged,
-                this,
-                &ChatPage::setGroupViewState);
-
-        connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize);
         connect(
           this,
           &ChatPage::initializeViews,
@@ -318,31 +188,14 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         connect(this,
                 &ChatPage::initializeEmptyViews,
                 view_manager_,
-                &TimelineViewManager::initWithMessages);
-        connect(this,
-                &ChatPage::initializeMentions,
-                user_mentions_popup_,
-                &popups::UserMentions::initializeMentions);
+                &TimelineViewManager::initializeRoomlist);
         connect(
           this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
         connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
-                try {
-                        room_list_->cleanupInvites(cache::invites());
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->error("failed to retrieve invites: {}", e.what());
-                }
-
                 view_manager_->sync(rooms);
-                removeLeftRooms(rooms.leave);
 
                 bool hasNotifications = false;
                 for (const auto &room : rooms.join) {
-                        auto room_id = QString::fromStdString(room.first);
-                        updateRoomNotificationCount(
-                          room_id,
-                          room.second.unread_notifications.notification_count,
-                          room.second.unread_notifications.highlight_count);
-
                         if (room.second.unread_notifications.notification_count > 0)
                                 hasNotifications = true;
                 }
@@ -365,16 +218,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                                   emit notificationsRetrieved(std::move(res));
                           });
         });
-        connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
-        connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
-
-        // Callbacks to update the user info (top left corner of the page).
-        connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
-        connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) {
-                auto userid = utils::localUser();
-                user_info_widget_->setUserId(userid);
-                user_info_widget_->setDisplayName(name);
-        });
 
         connect(
           this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
@@ -427,8 +270,6 @@ ChatPage::dropToLoginPage(const QString &msg)
 void
 ChatPage::resetUI()
 {
-        room_list_->clear();
-        user_info_widget_->reset();
         view_manager_->clearAll();
 
         emit unreadMessages(0);
@@ -481,9 +322,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                         view_manager_,
                         &TimelineViewManager::updateReadReceipts);
 
-                connect(
-                  cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus);
-
                 connect(cache::client(),
                         &Cache::removeNotification,
                         ¬ificationsManager,
@@ -559,10 +397,8 @@ ChatPage::loadStateFromCache()
         try {
                 olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
 
-                emit initializeEmptyViews(cache::client()->roomIds());
-                emit initializeRoomList(cache::roomInfo());
+                emit initializeEmptyViews();
                 emit initializeMentions(cache::getTimelineMentions());
-                emit syncTags(cache::roomInfo().toStdMap());
 
                 cache::calculateRoomReadStatus();
 
@@ -600,38 +436,6 @@ ChatPage::removeRoom(const QString &room_id)
                 nhlog::db()->critical("failure while removing room: {}", e.what());
                 // TODO: Notify the user.
         }
-
-        room_list_->removeRoom(room_id, room_id == current_room_);
-}
-
-void
-ChatPage::removeLeftRooms(const std::map &rooms)
-{
-        for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) {
-                const auto room_id = QString::fromStdString(it->first);
-                room_list_->removeRoom(room_id, room_id == current_room_);
-        }
-}
-
-void
-ChatPage::setGroupViewState(bool isEnabled)
-{
-        if (!isEnabled) {
-                communitiesList_->communityChanged("world");
-                communitiesList_->hide();
-
-                return;
-        }
-
-        communitiesList_->show();
-}
-
-void
-ChatPage::updateRoomNotificationCount(const QString &room_id,
-                                      uint16_t notification_count,
-                                      uint16_t highlight_count)
-{
-        room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count);
 }
 
 void
@@ -679,18 +483,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
         }
 }
 
-void
-ChatPage::showNotificationsDialog(const QPoint &widgetPos)
-{
-        auto notifDialog = user_mentions_popup_;
-
-        notifDialog->setGeometry(
-          widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2);
-
-        notifDialog->raise();
-        notifDialog->showPopup();
-}
-
 void
 ChatPage::tryInitialSync()
 {
@@ -789,11 +581,9 @@ ChatPage::startInitialSync()
                           olm::handle_to_device_messages(res.to_device.events);
 
                           emit initializeViews(std::move(res.rooms));
-                          emit initializeRoomList(cache::roomInfo());
                           emit initializeMentions(cache::getTimelineMentions());
 
                           cache::calculateRoomReadStatus();
-                          emit syncTags(cache::roomInfo().toStdMap());
                   } catch (const lmdb::error &e) {
                           nhlog::db()->error("failed to save state after initial sync: {}",
                                              e.what());
@@ -830,12 +620,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string
 
                 auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
 
-                emit syncRoomlist(updates);
-
                 emit syncUI(res.rooms);
 
-                emit syncTags(cache::getRoomInfo(cache::client()->roomsWithTagUpdates(res)));
-
                 // if we process a lot of syncs (1 every 200ms), this means we clean the
                 // db every 100s
                 static int syncCounter = 0;
@@ -939,7 +725,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
                           emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
                   }
 
-                  room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
+                  view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
           });
 }
 
@@ -987,19 +773,18 @@ ChatPage::leaveRoom(const QString &room_id)
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->setHistoryView(room_id);
-        room_list_->highlightSelectedRoom(room_id);
+        view_manager_->rooms()->setCurrentRoom(room_id);
 }
 
 void
 ChatPage::inviteUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm invite"),
                                   tr("Do you really want to invite %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1021,12 +806,12 @@ ChatPage::inviteUser(QString userid, QString reason)
 void
 ChatPage::kickUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm kick"),
                                   tr("Do you really want to kick %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1048,12 +833,12 @@ ChatPage::kickUser(QString userid, QString reason)
 void
 ChatPage::banUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm ban"),
                                   tr("Do you really want to ban %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1075,12 +860,12 @@ ChatPage::banUser(QString userid, QString reason)
 void
 ChatPage::unbanUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm unban"),
                                   tr("Do you really want to unban %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1182,57 +967,6 @@ ChatPage::getProfileInfo()
 
                   emit setUserAvatar(QString::fromStdString(res.avatar_url));
           });
-
-        http::client()->joined_groups(
-          [this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->critical("failed to retrieve joined groups: {} {}",
-                                                 static_cast(err->status_code),
-                                                 err->matrix_error.error);
-                          emit updateGroupsInfo({});
-                          return;
-                  }
-
-                  emit updateGroupsInfo(res);
-          });
-}
-
-bool
-ChatPage::isRoomActive(const QString &room_id)
-{
-        return isActiveWindow() && content_->isVisible() && currentRoom() == room_id;
-}
-
-void
-ChatPage::hideSideBars()
-{
-        // Don't hide side bar, if we are currently only showing the side bar!
-        if (view_manager_->getWidget()->isVisible()) {
-                communitiesList_->hide();
-                sideBar_->hide();
-        }
-        view_manager_->enableBackButton();
-}
-
-void
-ChatPage::showSideBars()
-{
-        if (userSettings_->groupView())
-                communitiesList_->show();
-
-        sideBar_->show();
-        view_manager_->disableBackButton();
-        content_->show();
-}
-
-uint64_t
-ChatPage::timelineWidth()
-{
-        int sidebarWidth = sideBar_->minimumSize().width();
-        sidebarWidth += communitiesList_->minimumSize().width();
-        nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth);
-
-        return size().width() - sidebarWidth;
 }
 
 void
@@ -1318,7 +1052,8 @@ ChatPage::startChat(QString userid)
                         if (std::find(room_members.begin(),
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
-                                room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
+                                view_manager_->rooms()->setCurrentRoom(
+                                  QString::fromStdString(room_id));
                                 return;
                         }
                 }
@@ -1408,7 +1143,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
 
         if (sigil1 == "u") {
                 if (action.isEmpty()) {
-                        view_manager_->activeTimeline()->openUserProfile(mxid1);
+                        if (auto t = view_manager_->rooms()->currentRoom())
+                                t->openUserProfile(mxid1);
                 } else if (action == "chat") {
                         this->startChat(mxid1);
                 }
@@ -1418,7 +1154,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
 
                 for (auto roomid : joined_rooms) {
                         if (roomid == targetRoomId) {
-                                room_list_->highlightSelectedRoom(mxid1);
+                                view_manager_->rooms()->setCurrentRoom(mxid1);
                                 if (!mxid2.isEmpty())
                                         view_manager_->showEvent(mxid1, mxid2);
                                 return;
@@ -1436,7 +1172,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
                         auto aliases = cache::client()->getRoomAliases(roomid);
                         if (aliases) {
                                 if (aliases->alias == targetRoomAlias) {
-                                        room_list_->highlightSelectedRoom(
+                                        view_manager_->rooms()->setCurrentRoom(
                                           QString::fromStdString(roomid));
                                         if (!mxid2.isEmpty())
                                                 view_manager_->showEvent(
@@ -1458,8 +1194,17 @@ ChatPage::handleMatrixUri(const QUrl &uri)
         handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
 }
 
-void
-ChatPage::highlightRoom(const QString &room_id)
+bool
+ChatPage::isRoomActive(const QString &room_id)
 {
-        room_list_->highlightSelectedRoom(room_id);
+        return isActiveWindow() && currentRoom() == room_id;
+}
+
+QString
+ChatPage::currentRoom() const
+{
+        if (view_manager_->rooms()->currentRoom())
+                return view_manager_->rooms()->currentRoom()->roomId();
+        else
+                return "";
 }
diff --git a/src/ChatPage.h b/src/ChatPage.h
index a7489455..751e7074 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -27,15 +27,10 @@
 
 #include "CacheCryptoStructs.h"
 #include "CacheStructs.h"
-#include "CommunitiesList.h"
 #include "notifications/Manager.h"
 
 class OverlayModal;
-class RoomList;
-class SideBarActions;
-class Splitter;
 class TimelineViewManager;
-class UserInfoWidget;
 class UserSettings;
 class NotificationsManager;
 class TimelineModel;
@@ -53,11 +48,6 @@ struct Notifications;
 struct Sync;
 struct Timeline;
 struct Rooms;
-struct LeftRoom;
-}
-
-namespace popups {
-class UserMentions;
 }
 
 using SecretsToDecrypt = std::map;
@@ -71,7 +61,6 @@ public:
 
         // Initialize all the components of the UI.
         void bootstrap(QString userid, QString homeserver, QString token);
-        QString currentRoom() const { return current_room_; }
 
         static ChatPage *instance() { return instance_; }
 
@@ -80,14 +69,6 @@ public:
         TimelineViewManager *timelineManager() { return view_manager_; }
         void deleteConfigs();
 
-        CommunitiesList *communitiesList() { return communitiesList_; }
-
-        //! Calculate the width of the message timeline.
-        uint64_t timelineWidth();
-        //! Hide the room & group list (if it was visible).
-        void hideSideBars();
-        //! Show the room/group list (if it was visible).
-        void showSideBars();
         void initiateLogout();
 
         QString status() const;
@@ -95,6 +76,9 @@ public:
 
         mtx::presence::PresenceState currentPresence() const;
 
+        // TODO(Nico): Get rid of this!
+        QString currentRoom() const;
+
 public slots:
         void handleMatrixUri(const QByteArray &uri);
         void handleMatrixUri(const QUrl &uri);
@@ -102,7 +86,6 @@ public slots:
         void startChat(QString userid);
         void leaveRoom(const QString &room_id);
         void createRoom(const mtx::requests::CreateRoom &req);
-        void highlightRoom(const QString &room_id);
         void joinRoom(const QString &room);
         void joinRoomVia(const std::string &room_id,
                          const std::vector &via,
@@ -145,13 +128,10 @@ signals:
         void leftRoom(const QString &room_id);
         void newRoom(const QString &room_id);
 
-        void initializeRoomList(QMap);
         void initializeViews(const mtx::responses::Rooms &rooms);
-        void initializeEmptyViews(const std::vector &roomIds);
+        void initializeEmptyViews();
         void initializeMentions(const QMap ¬ifs);
         void syncUI(const mtx::responses::Rooms &rooms);
-        void syncRoomlist(const std::map &updates);
-        void syncTags(const std::map &updates);
         void dropToLoginPageCb(const QString &msg);
 
         void notifyMessage(const QString &roomid,
@@ -161,7 +141,6 @@ signals:
                            const QString &message,
                            const QImage &icon);
 
-        void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
         void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
         void themeChanged();
         void decryptSidebarChanged();
@@ -213,56 +192,25 @@ private:
         using Membership  = mtx::events::StateEvent;
         using Memberships = std::map;
 
-        using LeftRooms = std::map;
-        void removeLeftRooms(const LeftRooms &rooms);
-
         void loadStateFromCache();
         void resetUI();
-        //! Decides whether or not to hide the group's sidebar.
-        void setGroupViewState(bool isEnabled);
 
         template
         Memberships getMemberships(const std::vector &events) const;
 
-        //! Update the room with the new notification count.
-        void updateRoomNotificationCount(const QString &room_id,
-                                         uint16_t notification_count,
-                                         uint16_t highlight_count);
         //! Send desktop notification for the received messages.
         void sendNotifications(const mtx::responses::Notifications &);
 
-        void showNotificationsDialog(const QPoint &point);
-
         template
         void connectCallMessage();
 
         QHBoxLayout *topLayout_;
-        Splitter *splitter;
-
-        QWidget *sideBar_;
-        QVBoxLayout *sideBarLayout_;
-        QWidget *sideBarTopWidget_;
-        QVBoxLayout *sideBarTopWidgetLayout_;
-
-        QFrame *content_;
-        QVBoxLayout *contentLayout_;
-
-        CommunitiesList *communitiesList_;
-        RoomList *room_list_;
 
         TimelineViewManager *view_manager_;
-        SideBarActions *sidebarActions_;
 
         QTimer connectivityTimer_;
         std::atomic_bool isConnected_;
 
-        QString current_room_;
-        QString current_community_;
-
-        UserInfoWidget *user_info_widget_;
-
-        popups::UserMentions *user_mentions_popup_;
-
         // Global user settings.
         QSharedPointer userSettings_;
 
diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp
deleted file mode 100644
index 7cc5d10e..00000000
--- a/src/CommunitiesList.cpp
+++ /dev/null
@@ -1,345 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "CommunitiesList.h"
-#include "Cache.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "MxcImageProvider.h"
-#include "Splitter.h"
-#include "UserSettingsPage.h"
-
-#include 
-#include 
-
-#include 
-
-CommunitiesList::CommunitiesList(QWidget *parent)
-  : QWidget(parent)
-{
-        QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
-        sizePolicy.setHorizontalStretch(0);
-        sizePolicy.setVerticalStretch(1);
-        setSizePolicy(sizePolicy);
-
-        topLayout_ = new QVBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
-
-        const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{});
-        setFixedWidth(sideBarSizes.groups);
-
-        scrollArea_ = new QScrollArea(this);
-        scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
-        scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter);
-
-        contentsLayout_ = new QVBoxLayout();
-        contentsLayout_->setSpacing(0);
-        contentsLayout_->setMargin(0);
-
-        addGlobalItem();
-        contentsLayout_->addStretch(1);
-
-        scrollArea_->setLayout(contentsLayout_);
-        topLayout_->addWidget(scrollArea_);
-
-        connect(
-          this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
-}
-
-void
-CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response)
-{
-        // remove all non-tag communities
-        auto it = communities_.begin();
-        while (it != communities_.end()) {
-                if (it->second->is_tag()) {
-                        ++it;
-                } else {
-                        it = communities_.erase(it);
-                }
-        }
-
-        addGlobalItem();
-
-        for (const auto &group : response.groups)
-                addCommunity(group);
-
-        communities_["world"]->setPressedState(true);
-        selectedCommunity_ = "world";
-        emit communityChanged("world");
-        sortEntries();
-}
-
-void
-CommunitiesList::syncTags(const std::map &info)
-{
-        for (const auto &room : info)
-                setTagsForRoom(room.first, room.second.tags);
-        emit communityChanged(selectedCommunity_);
-        sortEntries();
-}
-
-void
-CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector &tags)
-{
-        // create missing tag if any
-        for (const auto &tag : tags) {
-                // filter out tags we should ignore according to the spec
-                // https://matrix.org/docs/spec/client_server/r0.4.0.html#id154
-                // nheko currently does not make use of internal tags
-                // so we ignore any tag containig a `.` (which would indicate a tag
-                // in the form `tld.domain.*`) except for `m.*` and `u.*`.
-                if (tag.find(".") != ::std::string::npos && tag.compare(0, 2, "m.") &&
-                    tag.compare(0, 2, "u."))
-                        continue;
-                QString name = QString("tag:") + QString::fromStdString(tag);
-                if (!communityExists(name)) {
-                        addCommunity(std::string("tag:") + tag);
-                }
-        }
-        // update membership of the room for all tags
-        auto it = communities_.begin();
-        while (it != communities_.end()) {
-                // Skip if the community is not a tag
-                if (!it->second->is_tag()) {
-                        ++it;
-                        continue;
-                }
-                // insert or remove the room from the tag as appropriate
-                std::string current_tag =
-                  it->first.right(static_cast(it->first.size() - strlen("tag:")))
-                    .toStdString();
-                if (std::find(tags.begin(), tags.end(), current_tag) != tags.end()) {
-                        // the room has this tag
-                        it->second->addRoom(room_id);
-                } else {
-                        // the room does not have this tag
-                        it->second->delRoom(room_id);
-                }
-                // Check if the tag is now empty, if yes delete it
-                if (it->second->rooms().empty()) {
-                        it = communities_.erase(it);
-                } else {
-                        ++it;
-                }
-        }
-}
-
-void
-CommunitiesList::addCommunity(const std::string &group_id)
-{
-        auto hiddenTags = UserSettings::instance()->hiddenTags();
-
-        const auto id = QString::fromStdString(group_id);
-
-        CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_);
-
-        if (hiddenTags.contains(id))
-                list_item->setDisabled(true);
-
-        communities_.emplace(id, QSharedPointer(list_item));
-        contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
-
-        connect(list_item,
-                &CommunitiesListItem::clicked,
-                this,
-                &CommunitiesList::highlightSelectedCommunity);
-        connect(list_item, &CommunitiesListItem::isDisabledChanged, this, [this]() {
-                for (const auto &community : communities_) {
-                        if (community.second->isPressed()) {
-                                emit highlightSelectedCommunity(community.first);
-                                break;
-                        }
-                }
-
-                auto hiddenTags = hiddenTagsAndCommunities();
-                // Qt < 5.14 compat
-                QStringList hiddenTags_;
-                for (auto &&t : hiddenTags)
-                        hiddenTags_.push_back(t);
-                UserSettings::instance()->setHiddenTags(hiddenTags_);
-        });
-
-        if (group_id.empty() || group_id.front() != '+')
-                return;
-
-        nhlog::ui()->debug("Add community: {}", group_id);
-
-        connect(this,
-                &CommunitiesList::groupProfileRetrieved,
-                this,
-                [this](const QString &id, const mtx::responses::GroupProfile &profile) {
-                        if (communities_.find(id) == communities_.end())
-                                return;
-
-                        communities_.at(id)->setName(QString::fromStdString(profile.name));
-
-                        if (!profile.avatar_url.empty())
-                                fetchCommunityAvatar(id,
-                                                     QString::fromStdString(profile.avatar_url));
-                });
-        connect(this,
-                &CommunitiesList::groupRoomsRetrieved,
-                this,
-                [this](const QString &id, const std::set &rooms) {
-                        nhlog::ui()->info(
-                          "Fetched rooms for {}: {}", id.toStdString(), rooms.size());
-                        if (communities_.find(id) == communities_.end())
-                                return;
-
-                        communities_.at(id)->setRooms(rooms);
-                });
-
-        http::client()->group_profile(
-          group_id, [id, this](const mtx::responses::GroupProfile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          return;
-                  }
-
-                  emit groupProfileRetrieved(id, res);
-          });
-
-        http::client()->group_rooms(
-          group_id, [id, this](const nlohmann::json &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          return;
-                  }
-
-                  std::set room_ids;
-                  for (const auto &room : res.at("chunk"))
-                          room_ids.emplace(QString::fromStdString(room.at("room_id")));
-
-                  emit groupRoomsRetrieved(id, room_ids);
-          });
-}
-
-void
-CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img)
-{
-        if (!communityExists(community_id)) {
-                nhlog::ui()->warn("Avatar update on nonexistent community {}",
-                                  community_id.toStdString());
-                return;
-        }
-
-        communities_.at(community_id)->setAvatar(img.toImage());
-}
-
-void
-CommunitiesList::highlightSelectedCommunity(const QString &community_id)
-{
-        if (!communityExists(community_id)) {
-                nhlog::ui()->debug("CommunitiesList: clicked unknown community");
-                return;
-        }
-
-        selectedCommunity_ = community_id;
-        emit communityChanged(community_id);
-
-        for (const auto &community : communities_) {
-                if (community.first != community_id) {
-                        community.second->setPressedState(false);
-                } else {
-                        community.second->setPressedState(true);
-                        scrollArea_->ensureWidgetVisible(community.second.data());
-                }
-        }
-}
-
-void
-CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
-{
-        MxcImageProvider::download(
-          QString(avatarUrl).remove(QStringLiteral("mxc://")),
-          QSize(96, 96),
-          [this, id](QString, QSize, QImage img, QString) {
-                  if (img.isNull()) {
-                          nhlog::net()->warn("failed to download avatar: {})", id.toStdString());
-                          return;
-                  }
-
-                  emit avatarRetrieved(id, QPixmap::fromImage(img));
-          });
-}
-
-std::set
-CommunitiesList::roomList(const QString &id) const
-{
-        if (communityExists(id))
-                return communities_.at(id)->rooms();
-
-        return {};
-}
-
-std::vector
-CommunitiesList::currentTags() const
-{
-        std::vector tags;
-        for (auto &entry : communities_) {
-                CommunitiesListItem *item = entry.second.data();
-                if (item->is_tag())
-                        tags.push_back(entry.first.mid(4).toStdString());
-        }
-        return tags;
-}
-
-std::set
-CommunitiesList::hiddenTagsAndCommunities() const
-{
-        std::set hiddenTags;
-        for (auto &entry : communities_) {
-                if (entry.second->isDisabled())
-                        hiddenTags.insert(entry.first);
-        }
-
-        return hiddenTags;
-}
-
-void
-CommunitiesList::sortEntries()
-{
-        std::vector header;
-        std::vector communities;
-        std::vector tags;
-        std::vector footer;
-        // remove all the contents and sort them in the 4 vectors
-        for (auto &entry : communities_) {
-                CommunitiesListItem *item = entry.second.data();
-                contentsLayout_->removeWidget(item);
-                // world is handled separately
-                if (entry.first == "world")
-                        continue;
-                // sort the rest
-                if (item->is_tag())
-                        if (entry.first == "tag:m.favourite")
-                                header.push_back(item);
-                        else if (entry.first == "tag:m.lowpriority")
-                                footer.push_back(item);
-                        else
-                                tags.push_back(item);
-                else
-                        communities.push_back(item);
-        }
-
-        // now there remains only the stretch in the layout, remove it
-        QLayoutItem *stretch = contentsLayout_->itemAt(0);
-        contentsLayout_->removeItem(stretch);
-
-        contentsLayout_->addWidget(communities_["world"].data());
-
-        auto insert_widgets = [this](auto &vec) {
-                for (auto item : vec)
-                        contentsLayout_->addWidget(item);
-        };
-        insert_widgets(header);
-        insert_widgets(communities);
-        insert_widgets(tags);
-        insert_widgets(footer);
-
-        contentsLayout_->addItem(stretch);
-}
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
deleted file mode 100644
index 2586f6f5..00000000
--- a/src/CommunitiesList.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-#include "CacheStructs.h"
-#include "CommunitiesListItem.h"
-#include "ui/Theme.h"
-
-namespace mtx::responses {
-struct GroupProfile;
-struct JoinedGroups;
-}
-
-class CommunitiesList : public QWidget
-{
-        Q_OBJECT
-
-public:
-        CommunitiesList(QWidget *parent = nullptr);
-
-        void clear() { communities_.clear(); }
-
-        void addCommunity(const std::string &id);
-        void removeCommunity(const QString &id) { communities_.erase(id); };
-        std::set roomList(const QString &id) const;
-
-        void syncTags(const std::map &info);
-        void setTagsForRoom(const QString &id, const std::vector &tags);
-        std::vector currentTags() const;
-        std::set hiddenTagsAndCommunities() const;
-
-signals:
-        void communityChanged(const QString &id);
-        void avatarRetrieved(const QString &id, const QPixmap &img);
-        void groupProfileRetrieved(const QString &group_id, const mtx::responses::GroupProfile &);
-        void groupRoomsRetrieved(const QString &group_id, const std::set &res);
-
-public slots:
-        void updateCommunityAvatar(const QString &id, const QPixmap &img);
-        void highlightSelectedCommunity(const QString &id);
-        void setCommunities(const mtx::responses::JoinedGroups &groups);
-
-private:
-        void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
-        void addGlobalItem() { addCommunity("world"); }
-        void sortEntries();
-
-        //! Check whether or not a community id is currently managed.
-        bool communityExists(const QString &id) const
-        {
-                return communities_.find(id) != communities_.end();
-        }
-
-        QString selectedCommunity_;
-        QVBoxLayout *topLayout_;
-        QVBoxLayout *contentsLayout_;
-        QScrollArea *scrollArea_;
-
-        std::map> communities_;
-};
diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp
deleted file mode 100644
index a2f2777d..00000000
--- a/src/CommunitiesListItem.cpp
+++ /dev/null
@@ -1,201 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "CommunitiesListItem.h"
-
-#include 
-#include 
-
-#include "Utils.h"
-#include "ui/Painter.h"
-#include "ui/Ripple.h"
-#include "ui/RippleOverlay.h"
-
-CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent)
-  : QWidget(parent)
-  , groupId_(group_id)
-{
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        QPainterPath path;
-        path.addRect(0, 0, parent->width(), height());
-        rippleOverlay_ = new RippleOverlay(this);
-        rippleOverlay_->setClipPath(path);
-        rippleOverlay_->setClipping(true);
-
-        menu_ = new QMenu(this);
-        hideRoomsWithTagAction_ =
-          new QAction(tr("Hide rooms with this tag or from this community"), this);
-        hideRoomsWithTagAction_->setCheckable(true);
-        menu_->addAction(hideRoomsWithTagAction_);
-        connect(menu_, &QMenu::aboutToShow, this, [this]() {
-                hideRoomsWithTagAction_->setChecked(isDisabled_);
-        });
-
-        connect(hideRoomsWithTagAction_, &QAction::triggered, this, [this](bool checked) {
-                this->setDisabled(checked);
-        });
-
-        updateTooltip();
-}
-
-void
-CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        menu_->popup(event->globalPos());
-}
-
-void
-CommunitiesListItem::setName(QString name)
-{
-        name_ = name;
-        updateTooltip();
-}
-
-void
-CommunitiesListItem::setPressedState(bool state)
-{
-        if (isPressed_ != state) {
-                isPressed_ = state;
-                update();
-        }
-}
-
-void
-CommunitiesListItem::setDisabled(bool state)
-{
-        if (isDisabled_ != state) {
-                isDisabled_ = state;
-                update();
-                emit isDisabledChanged();
-        }
-}
-
-void
-CommunitiesListItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() == Qt::RightButton) {
-                QWidget::mousePressEvent(event);
-                return;
-        }
-
-        emit clicked(groupId_);
-
-        setPressedState(true);
-
-        QPoint pos           = event->pos();
-        qreal radiusEndValue = static_cast(width()) / 3;
-
-        auto ripple = new Ripple(pos);
-        ripple->setRadiusEndValue(radiusEndValue);
-        ripple->setOpacityStartValue(0.15);
-        ripple->setColor("white");
-        ripple->radiusAnimation()->setDuration(200);
-        ripple->opacityAnimation()->setDuration(400);
-        rippleOverlay_->addRipple(ripple);
-}
-
-void
-CommunitiesListItem::paintEvent(QPaintEvent *)
-{
-        Painter p(this);
-        PainterHighQualityEnabler hq(p);
-
-        if (isPressed_)
-                p.fillRect(rect(), highlightedBackgroundColor_);
-        else if (isDisabled_)
-                p.fillRect(rect(), disabledBackgroundColor_);
-        else if (underMouse())
-                p.fillRect(rect(), hoverBackgroundColor_);
-        else
-                p.fillRect(rect(), backgroundColor_);
-
-        if (avatar_.isNull()) {
-                QPixmap source;
-                if (groupId_ == "world")
-                        source = QPixmap(":/icons/icons/ui/world.png");
-                else if (groupId_ == "tag:m.favourite")
-                        source = QPixmap(":/icons/icons/ui/star.png");
-                else if (groupId_ == "tag:m.lowpriority")
-                        source = QPixmap(":/icons/icons/ui/lowprio.png");
-                else if (groupId_.startsWith("tag:"))
-                        source = QPixmap(":/icons/icons/ui/tag.png");
-
-                if (source.isNull()) {
-                        QFont font;
-                        font.setPointSizeF(font.pointSizeF() * 1.3);
-                        p.setFont(font);
-
-                        p.drawLetterAvatar(utils::firstChar(resolveName()),
-                                           avatarFgColor_,
-                                           avatarBgColor_,
-                                           width(),
-                                           height(),
-                                           IconSize);
-                } else {
-                        QPainter painter(&source);
-                        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
-                        painter.fillRect(source.rect(), avatarFgColor_);
-                        painter.end();
-
-                        const int imageSz = 32;
-                        p.drawPixmap(
-                          QRect(
-                            (width() - imageSz) / 2, (height() - imageSz) / 2, imageSz, imageSz),
-                          source);
-                }
-        } else {
-                p.save();
-
-                p.drawAvatar(avatar_, width(), height(), IconSize);
-                p.restore();
-        }
-}
-
-void
-CommunitiesListItem::setAvatar(const QImage &img)
-{
-        avatar_ = utils::scaleImageToPixmap(img, IconSize);
-        update();
-}
-
-QString
-CommunitiesListItem::resolveName() const
-{
-        if (!name_.isEmpty())
-                return name_;
-        if (groupId_.startsWith("tag:"))
-                return groupId_.right(static_cast(groupId_.size() - strlen("tag:")));
-        if (!groupId_.startsWith("+"))
-                return QString("Group"); // Group with no name or id.
-
-        // Extract the localpart of the group.
-        auto firstPart = groupId_.split(':').at(0);
-        return firstPart.right(firstPart.size() - 1);
-}
-
-void
-CommunitiesListItem::updateTooltip()
-{
-        if (groupId_ == "world")
-                setToolTip(tr("All rooms"));
-        else if (is_tag()) {
-                QStringRef tag =
-                  groupId_.rightRef(static_cast(groupId_.size() - strlen("tag:")));
-                if (tag == "m.favourite")
-                        setToolTip(tr("Favourite rooms"));
-                else if (tag == "m.lowpriority")
-                        setToolTip(tr("Low priority rooms"));
-                else if (tag == "m.server_notice")
-                        setToolTip(tr("Server Notices", "Tag translation for m.server_notice"));
-                else if (tag.startsWith("u."))
-                        setToolTip(tag.right(tag.size() - 2) + tr(" (tag)"));
-                else
-                        setToolTip(tag + tr(" (tag)"));
-        } else {
-                QString name = resolveName();
-                setToolTip(name + tr(" (community)"));
-        }
-}
diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
deleted file mode 100644
index 006511c8..00000000
--- a/src/CommunitiesListItem.h
+++ /dev/null
@@ -1,108 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-
-#include "Config.h"
-#include "ui/Theme.h"
-
-class RippleOverlay;
-class QMouseEvent;
-class QMenu;
-
-class CommunitiesListItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE
-                     setHighlightedBackgroundColor)
-        Q_PROPERTY(QColor disabledBackgroundColor READ disabledBackgroundColor WRITE
-                     setDisabledBackgroundColor)
-        Q_PROPERTY(
-          QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor)
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-        Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor)
-        Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor)
-
-public:
-        CommunitiesListItem(QString group_id, QWidget *parent = nullptr);
-
-        void setName(QString name);
-        bool isPressed() const { return isPressed_; }
-        bool isDisabled() const { return isDisabled_; }
-        void setAvatar(const QImage &img);
-
-        void setRooms(std::set room_ids) { room_ids_ = std::move(room_ids); }
-        void addRoom(const QString &id) { room_ids_.insert(id); }
-        void delRoom(const QString &id) { room_ids_.erase(id); }
-        std::set rooms() const { return room_ids_; }
-
-        bool is_tag() const { return groupId_.startsWith("tag:"); }
-
-        QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
-        QColor disabledBackgroundColor() const { return disabledBackgroundColor_; }
-        QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor avatarFgColor() const { return avatarFgColor_; }
-        QColor avatarBgColor() const { return avatarBgColor_; }
-
-        void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
-        void setDisabledBackgroundColor(QColor &color) { disabledBackgroundColor_ = color; }
-        void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
-        void setBackgroundColor(QColor &color) { backgroundColor_ = color; }
-
-        void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; }
-        void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; }
-
-        QSize sizeHint() const override
-        {
-                return QSize(IconSize + IconSize / 3, IconSize + IconSize / 3);
-        }
-
-signals:
-        void clicked(const QString &group_id);
-        void isDisabledChanged();
-
-public slots:
-        void setPressedState(bool state);
-        void setDisabled(bool state);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        const int IconSize = 36;
-
-        QString resolveName() const;
-        void updateTooltip();
-
-        std::set room_ids_;
-
-        QString name_;
-        QString groupId_;
-        QPixmap avatar_;
-
-        QColor highlightedBackgroundColor_;
-        QColor disabledBackgroundColor_;
-        QColor hoverBackgroundColor_;
-        QColor backgroundColor_;
-
-        QColor avatarFgColor_;
-        QColor avatarBgColor_;
-
-        bool isPressed_  = false;
-        bool isDisabled_ = false;
-
-        RippleOverlay *rippleOverlay_;
-        QMenu *menu_;
-        QAction *hideRoomsWithTagAction_;
-};
diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index 412708a2..e68944c7 100644
--- a/src/CompletionProxyModel.cpp
+++ b/src/CompletionProxyModel.cpp
@@ -19,7 +19,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
   , max_completions_(max_completions)
 {
         setSourceModel(model);
-        QRegularExpression splitPoints("\\s+|-");
+        QChar splitPoints(' ');
 
         // insert all the full texts
         for (int i = 0; i < sourceModel()->rowCount(); i++) {
@@ -48,7 +48,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
                                  .toString()
                                  .toLower();
 
-                for (const auto &e : string1.split(splitPoints)) {
+                for (const auto &e : string1.splitRef(splitPoints)) {
                         if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
                                 trie_.insert(e.toUcs4(), i);
                 }
@@ -59,7 +59,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
                                  .toLower();
 
                 if (!string2.isEmpty()) {
-                        for (const auto &e : string2.split(splitPoints)) {
+                        for (const auto &e : string2.splitRef(splitPoints)) {
                                 if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
                                         trie_.insert(e.toUcs4(), i);
                         }
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 92f43e03..ed337ca4 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -22,7 +22,6 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "RegisterPage.h"
-#include "Splitter.h"
 #include "TrayIcon.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
@@ -109,10 +108,6 @@ MainWindow::MainWindow(QWidget *parent)
           userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
         connect(
           userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
-        connect(userSettingsPage_,
-                &UserSettingsPage::decryptSidebarChanged,
-                chat_page_,
-                &ChatPage::decryptSidebarChanged);
         connect(trayIcon_,
                 SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
                 this,
@@ -176,20 +171,6 @@ MainWindow::setWindowTitle(int notificationCount)
         QMainWindow::setWindowTitle(name);
 }
 
-void
-MainWindow::showEvent(QShowEvent *event)
-{
-        adjustSideBars();
-        QMainWindow::showEvent(event);
-}
-
-void
-MainWindow::resizeEvent(QResizeEvent *event)
-{
-        adjustSideBars();
-        QMainWindow::resizeEvent(event);
-}
-
 bool
 MainWindow::event(QEvent *event)
 {
@@ -203,22 +184,6 @@ MainWindow::event(QEvent *event)
         return QMainWindow::event(event);
 }
 
-void
-MainWindow::adjustSideBars()
-{
-        const auto sz = splitter::calculateSidebarSizes(QFont{});
-
-        const uint64_t timelineWidth     = chat_page_->timelineWidth();
-        const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups;
-
-        nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth);
-        if (timelineWidth < minAvailableWidth) {
-                chat_page_->hideSideBars();
-        } else {
-                chat_page_->showSideBars();
-        }
-}
-
 void
 MainWindow::restoreWindowSize()
 {
@@ -295,6 +260,7 @@ MainWindow::showChatPage()
                 &Cache::secretChanged,
                 userSettingsPage_,
                 &UserSettingsPage::updateSecretStatus);
+        emit reload();
 }
 
 void
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4122e4c1..3571f079 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -77,13 +77,9 @@ public:
 
 protected:
         void closeEvent(QCloseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void showEvent(QShowEvent *event) override;
         bool event(QEvent *event) override;
 
 private slots:
-        //! Show or hide the sidebars based on window's size.
-        void adjustSideBars();
         //! Handle interaction with the tray icon.
         void iconActivated(QSystemTrayIcon::ActivationReason reason);
 
@@ -109,6 +105,7 @@ private slots:
 
 signals:
         void focusChanged(const bool focused);
+        void reload();
 
 private:
         bool loadJdenticonPlugin();
diff --git a/src/Olm.cpp b/src/Olm.cpp
index d08c1b3e..ff4c883b 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg)
 
         for (const auto &cipher : msg.ciphertext) {
                 // We skip messages not meant for the current device.
-                if (cipher.first != my_key)
+                if (cipher.first != my_key) {
+                        nhlog::crypto()->debug(
+                          "Skipping message for {} since we are {}.", cipher.first, my_key);
                         continue;
+                }
 
                 const auto type = cipher.second.type;
                 nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
@@ -661,8 +664,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
         for (const auto &id : session_ids) {
                 auto session = cache::getOlmSession(sender_key, id);
 
-                if (!session)
+                if (!session) {
+                        nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id);
                         continue;
+                }
 
                 mtx::crypto::BinaryBuf text;
 
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
deleted file mode 100644
index ea5de674..00000000
--- a/src/RoomInfoListItem.cpp
+++ /dev/null
@@ -1,522 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "AvatarProvider.h"
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "RoomInfoListItem.h"
-#include "Splitter.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/Ripple.h"
-#include "ui/RippleOverlay.h"
-
-constexpr int MaxUnreadCountDisplayed = 99;
-
-struct WidgetMetrics
-{
-        int maxHeight;
-        int iconSize;
-        int padding;
-        int unit;
-
-        int unreadLineWidth;
-        int unreadLineOffset;
-
-        int inviteBtnX;
-        int inviteBtnY;
-};
-
-WidgetMetrics
-getMetrics(const QFont &font)
-{
-        WidgetMetrics m;
-
-        const int height = QFontMetrics(font).lineSpacing();
-
-        m.unit             = height;
-        m.maxHeight        = std::ceil((double)height * 3.8);
-        m.iconSize         = std::ceil((double)height * 2.8);
-        m.padding          = std::ceil((double)height / 2.0);
-        m.unreadLineWidth  = m.padding - m.padding / 3;
-        m.unreadLineOffset = m.padding - m.padding / 4;
-
-        m.inviteBtnX = m.iconSize + 2 * m.padding;
-        m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0;
-
-        return m;
-}
-
-void
-RoomInfoListItem::init(QWidget *parent)
-{
-        setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        auto wm = getMetrics(QFont{});
-        setFixedHeight(wm.maxHeight);
-
-        QPainterPath path;
-        path.addRect(0, 0, parent->width(), height());
-
-        ripple_overlay_ = new RippleOverlay(this);
-        ripple_overlay_->setClipPath(path);
-        ripple_overlay_->setClipping(true);
-
-        avatar_ = new Avatar(nullptr, wm.iconSize);
-        avatar_->setLetter(utils::firstChar(roomName_));
-        avatar_->resize(wm.iconSize, wm.iconSize);
-
-        unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8);
-        unreadCountFont_.setBold(true);
-
-        bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3;
-
-        menu_      = new QMenu(this);
-        leaveRoom_ = new QAction(tr("Leave room"), this);
-        connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); });
-
-        connect(menu_, &QMenu::aboutToShow, this, [this]() {
-                menu_->clear();
-                menu_->addAction(leaveRoom_);
-
-                menu_->addSection(QIcon(":/icons/icons/ui/tag.png"), tr("Tag room as:"));
-
-                auto roomInfo = cache::singleRoomInfo(roomId_.toStdString());
-
-                auto tags = ChatPage::instance()->communitiesList()->currentTags();
-
-                // add default tag, remove server notice tag
-                if (std::find(tags.begin(), tags.end(), "m.favourite") == tags.end())
-                        tags.push_back("m.favourite");
-                if (std::find(tags.begin(), tags.end(), "m.lowpriority") == tags.end())
-                        tags.push_back("m.lowpriority");
-                if (auto it = std::find(tags.begin(), tags.end(), "m.server_notice");
-                    it != tags.end())
-                        tags.erase(it);
-
-                for (const auto &tag : tags) {
-                        QString tagName;
-                        if (tag == "m.favourite")
-                                tagName = tr("Favourite", "Standard matrix tag for favourites");
-                        else if (tag == "m.lowpriority")
-                                tagName =
-                                  tr("Low Priority", "Standard matrix tag for low priority rooms");
-                        else if (tag == "m.server_notice")
-                                tagName =
-                                  tr("Server Notice", "Standard matrix tag for server notices");
-                        else if ((tag.size() > 2 && tag.substr(0, 2) == "u.") ||
-                                 tag.find(".") !=
-                                   std::string::npos) // tag manager creates tags without u., which
-                                                      // is wrong, but we still want to display them
-                                tagName = QString::fromStdString(tag.substr(2));
-
-                        if (tagName.isEmpty())
-                                continue;
-
-                        auto tagAction = menu_->addAction(tagName);
-                        tagAction->setCheckable(true);
-                        tagAction->setWhatsThis(tr("Adds or removes the specified tag.",
-                                                   "WhatsThis hint for tag menu actions"));
-
-                        for (const auto &riTag : roomInfo.tags) {
-                                if (riTag == tag) {
-                                        tagAction->setChecked(true);
-                                        break;
-                                }
-                        }
-
-                        connect(tagAction, &QAction::triggered, this, [this, tag](bool checked) {
-                                if (checked)
-                                        http::client()->put_tag(
-                                          roomId_.toStdString(),
-                                          tag,
-                                          {},
-                                          [tag](mtx::http::RequestErr err) {
-                                                  if (err) {
-                                                          nhlog::ui()->error(
-                                                            "Failed to add tag: {}, {}",
-                                                            tag,
-                                                            err->matrix_error.error);
-                                                  }
-                                          });
-                                else
-                                        http::client()->delete_tag(
-                                          roomId_.toStdString(),
-                                          tag,
-                                          [tag](mtx::http::RequestErr err) {
-                                                  if (err) {
-                                                          nhlog::ui()->error(
-                                                            "Failed to delete tag: {}, {}",
-                                                            tag,
-                                                            err->matrix_error.error);
-                                                  }
-                                          });
-                        });
-                }
-
-                auto newTagAction = menu_->addAction(tr("New tag...", "Add a new tag to the room"));
-                connect(newTagAction, &QAction::triggered, this, [this]() {
-                        QString tagName =
-                          QInputDialog::getText(this,
-                                                tr("New Tag", "Tag name prompt title"),
-                                                tr("Tag:", "Tag name prompt"));
-                        if (tagName.isEmpty())
-                                return;
-
-                        std::string tag = "u." + tagName.toStdString();
-
-                        http::client()->put_tag(
-                          roomId_.toStdString(), tag, {}, [tag](mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::ui()->error("Failed to add tag: {}, {}",
-                                                             tag,
-                                                             err->matrix_error.error);
-                                  }
-                          });
-                });
-        });
-}
-
-RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent)
-  : QWidget(parent)
-  , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined}
-  , roomId_(std::move(room_id))
-  , roomName_{QString::fromStdString(std::move(info.name))}
-  , isPressed_(false)
-  , unreadMsgCount_(0)
-  , unreadHighlightedMsgCount_(0)
-{
-        init(parent);
-}
-
-void
-RoomInfoListItem::resizeEvent(QResizeEvent *)
-{
-        // Update ripple's clipping path.
-        QPainterPath path;
-        path.addRect(0, 0, width(), height());
-
-        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() > sidebarSizes.small)
-                setToolTip("");
-        else
-                setToolTip(roomName_);
-
-        ripple_overlay_->setClipPath(path);
-        ripple_overlay_->setClipping(true);
-}
-
-void
-RoomInfoListItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter p(this);
-        p.setRenderHint(QPainter::TextAntialiasing);
-        p.setRenderHint(QPainter::SmoothPixmapTransform);
-        p.setRenderHint(QPainter::Antialiasing);
-
-        QFontMetrics metrics(QFont{});
-
-        QPen titlePen(titleColor_);
-        QPen subtitlePen(subtitleColor_);
-
-        auto wm = getMetrics(QFont{});
-
-        QPixmap pixmap(avatar_->size() * p.device()->devicePixelRatioF());
-        pixmap.setDevicePixelRatio(p.device()->devicePixelRatioF());
-        if (isPressed_) {
-                p.fillRect(rect(), highlightedBackgroundColor_);
-                titlePen.setColor(highlightedTitleColor_);
-                subtitlePen.setColor(highlightedSubtitleColor_);
-                pixmap.fill(highlightedBackgroundColor_);
-        } else if (underMouse()) {
-                p.fillRect(rect(), hoverBackgroundColor_);
-                titlePen.setColor(hoverTitleColor_);
-                subtitlePen.setColor(hoverSubtitleColor_);
-                pixmap.fill(hoverBackgroundColor_);
-        } else {
-                p.fillRect(rect(), backgroundColor_);
-                titlePen.setColor(titleColor_);
-                subtitlePen.setColor(subtitleColor_);
-                pixmap.fill(backgroundColor_);
-        }
-
-        avatar_->render(&pixmap, QPoint(), QRegion(), RenderFlags(DrawChildren));
-        p.drawPixmap(QPoint(wm.padding, wm.padding), pixmap);
-
-        // Description line with the default font.
-        int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2;
-
-        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() > sidebarSizes.small) {
-                QFont headingFont;
-                headingFont.setWeight(QFont::Medium);
-                p.setFont(headingFont);
-                p.setPen(titlePen);
-
-                QFont tsFont;
-                tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-                const int msgStampWidth =
-                  QFontMetrics(tsFont).width(lastMsgInfo_.descriptiveTime) + 4;
-#else
-                const int msgStampWidth =
-                  QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.descriptiveTime) + 4;
-#endif
-                // We use the full width of the widget if there is no unread msg bubble.
-                const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0;
-
-                // Name line.
-                QFontMetrics fontNameMetrics(headingFont);
-                int top_y = 2 * wm.padding + fontNameMetrics.ascent() / 2;
-
-                const auto name = metrics.elidedText(
-                  roomName(),
-                  Qt::ElideRight,
-                  (width() - wm.iconSize - 2 * wm.padding - msgStampWidth) * 0.8);
-                p.drawText(QPoint(2 * wm.padding + wm.iconSize, top_y), name);
-
-                if (roomType_ == RoomType::Joined) {
-                        p.setFont(QFont{});
-                        p.setPen(subtitlePen);
-
-                        int descriptionLimit = std::max(
-                          0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
-                        auto description =
-                          metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
-                        p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
-
-                        // We show the last message timestamp.
-                        p.save();
-                        if (isPressed_) {
-                                p.setPen(QPen(highlightedTimestampColor_));
-                        } else if (underMouse()) {
-                                p.setPen(QPen(hoverTimestampColor_));
-                        } else {
-                                p.setPen(QPen(timestampColor_));
-                        }
-
-                        p.setFont(tsFont);
-                        p.drawText(QPoint(width() - wm.padding - msgStampWidth, top_y),
-                                   lastMsgInfo_.descriptiveTime);
-                        p.restore();
-                } else {
-                        int btnWidth = (width() - wm.iconSize - 6 * wm.padding) / 2;
-
-                        acceptBtnRegion_  = QRectF(wm.inviteBtnX, wm.inviteBtnY, btnWidth, 20);
-                        declineBtnRegion_ = QRectF(
-                          wm.inviteBtnX + btnWidth + 2 * wm.padding, wm.inviteBtnY, btnWidth, 20);
-
-                        QPainterPath acceptPath;
-                        acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10);
-
-                        p.setPen(Qt::NoPen);
-                        p.fillPath(acceptPath, btnColor_);
-                        p.drawPath(acceptPath);
-
-                        QPainterPath declinePath;
-                        declinePath.addRoundedRect(declineBtnRegion_, 10, 10);
-
-                        p.setPen(Qt::NoPen);
-                        p.fillPath(declinePath, btnColor_);
-                        p.drawPath(declinePath);
-
-                        p.setPen(QPen(btnTextColor_));
-                        p.setFont(QFont{});
-                        p.drawText(acceptBtnRegion_,
-                                   Qt::AlignCenter,
-                                   metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth));
-                        p.drawText(declineBtnRegion_,
-                                   Qt::AlignCenter,
-                                   metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth));
-                }
-        }
-
-        p.setPen(Qt::NoPen);
-
-        if (unreadMsgCount_ > 0) {
-                QBrush brush;
-                brush.setStyle(Qt::SolidPattern);
-                if (unreadHighlightedMsgCount_ > 0) {
-                        brush.setColor(mentionedColor());
-                } else {
-                        brush.setColor(bubbleBgColor());
-                }
-
-                if (isPressed_)
-                        brush.setColor(bubbleFgColor());
-
-                p.setBrush(brush);
-                p.setPen(Qt::NoPen);
-                p.setFont(unreadCountFont_);
-
-                // Extra space on the x-axis to accomodate the extra character space
-                // inside the bubble.
-                const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed
-                                      ? QFontMetrics(p.font()).averageCharWidth()
-                                      : 0;
-
-                QRectF r(width() - bubbleDiameter_ - wm.padding - x_width,
-                         bottom_y - bubbleDiameter_ / 2 - 5,
-                         bubbleDiameter_ + x_width,
-                         bubbleDiameter_);
-
-                if (width() == sidebarSizes.small)
-                        r = QRectF(width() - bubbleDiameter_ - 5,
-                                   height() - bubbleDiameter_ - 5,
-                                   bubbleDiameter_ + x_width,
-                                   bubbleDiameter_);
-
-                p.setPen(Qt::NoPen);
-                p.drawEllipse(r);
-
-                p.setPen(QPen(bubbleFgColor()));
-
-                if (isPressed_)
-                        p.setPen(QPen(bubbleBgColor()));
-
-                auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed
-                                  ? QString("99+")
-                                  : QString::number(unreadMsgCount_);
-
-                p.setBrush(Qt::NoBrush);
-                p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt);
-        }
-
-        if (!isPressed_ && hasUnreadMessages_) {
-                QPen pen;
-                pen.setWidth(wm.unreadLineWidth);
-                pen.setColor(highlightedBackgroundColor_);
-
-                p.setPen(pen);
-                p.drawLine(0, wm.unreadLineOffset, 0, height() - wm.unreadLineOffset);
-        }
-}
-
-void
-RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount)
-{
-        unreadMsgCount_            = count;
-        unreadHighlightedMsgCount_ = highlightedCount;
-        update();
-}
-
-enum NotificationImportance : short
-{
-        ImportanceDisabled = -1,
-        AllEventsRead      = 0,
-        NewMessage         = 1,
-        NewMentions        = 2,
-        Invite             = 3
-};
-
-short int
-RoomInfoListItem::calculateImportance() const
-{
-        // Returns the degree of importance of the unread messages in the room.
-        // If sorting by importance is disabled in settings, this only ever
-        // returns ImportanceDisabled or Invite
-        if (isInvite()) {
-                return Invite;
-        } else if (!ChatPage::instance()->userSettings()->sortByImportance()) {
-                return ImportanceDisabled;
-        } else if (unreadHighlightedMsgCount_) {
-                return NewMentions;
-        } else if (unreadMsgCount_) {
-                return NewMessage;
-        } else {
-                return AllEventsRead;
-        }
-}
-
-void
-RoomInfoListItem::setPressedState(bool state)
-{
-        if (isPressed_ != state) {
-                isPressed_ = state;
-                update();
-        }
-}
-
-void
-RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        Q_UNUSED(event);
-
-        if (roomType_ == RoomType::Invited)
-                return;
-
-        menu_->popup(event->globalPos());
-}
-
-void
-RoomInfoListItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() == Qt::RightButton) {
-                QWidget::mousePressEvent(event);
-                return;
-        } else if (event->buttons() == Qt::LeftButton) {
-                if (roomType_ == RoomType::Invited) {
-                        const auto point = event->pos();
-
-                        if (acceptBtnRegion_.contains(point))
-                                emit acceptInvite(roomId_);
-
-                        if (declineBtnRegion_.contains(point))
-                                emit declineInvite(roomId_);
-
-                        return;
-                }
-
-                emit clicked(roomId_);
-
-                setPressedState(true);
-
-                // Ripple on mouse position by default.
-                QPoint pos           = event->pos();
-                qreal radiusEndValue = static_cast(width()) / 3;
-
-                Ripple *ripple = new Ripple(pos);
-
-                ripple->setRadiusEndValue(radiusEndValue);
-                ripple->setOpacityStartValue(0.15);
-                ripple->setColor(QColor("white"));
-                ripple->radiusAnimation()->setDuration(200);
-                ripple->opacityAnimation()->setDuration(400);
-
-                ripple_overlay_->addRipple(ripple);
-        }
-}
-
-void
-RoomInfoListItem::setAvatar(const QString &avatar_url)
-{
-        if (avatar_url.isEmpty())
-                avatar_->setLetter(utils::firstChar(roomName_));
-        else
-                avatar_->setImage(avatar_url);
-}
-
-void
-RoomInfoListItem::setDescriptionMessage(const DescInfo &info)
-{
-        lastMsgInfo_ = info;
-        update();
-}
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
deleted file mode 100644
index a5e0009e..00000000
--- a/src/RoomInfoListItem.h
+++ /dev/null
@@ -1,210 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "CacheStructs.h"
-#include "UserSettingsPage.h"
-#include "ui/Avatar.h"
-
-class QMenu;
-class RippleOverlay;
-
-class RoomInfoListItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE
-                     setHighlightedBackgroundColor)
-        Q_PROPERTY(
-          QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor)
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-        Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor)
-        Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor)
-
-        Q_PROPERTY(QColor titleColor READ titleColor WRITE setTitleColor)
-        Q_PROPERTY(QColor subtitleColor READ subtitleColor WRITE setSubtitleColor)
-
-        Q_PROPERTY(QColor timestampColor READ timestampColor WRITE setTimestampColor)
-        Q_PROPERTY(QColor highlightedTimestampColor READ highlightedTimestampColor WRITE
-                     setHighlightedTimestampColor)
-        Q_PROPERTY(QColor hoverTimestampColor READ hoverTimestampColor WRITE setHoverTimestampColor)
-
-        Q_PROPERTY(
-          QColor highlightedTitleColor READ highlightedTitleColor WRITE setHighlightedTitleColor)
-        Q_PROPERTY(QColor highlightedSubtitleColor READ highlightedSubtitleColor WRITE
-                     setHighlightedSubtitleColor)
-
-        Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor)
-        Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor)
-
-        Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor)
-        Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor)
-        Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor)
-
-public:
-        RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr);
-
-        void updateUnreadMessageCount(int count, int highlightedCount);
-        void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); };
-
-        short int calculateImportance() const;
-
-        QString roomId() { return roomId_; }
-        bool isPressed() const { return isPressed_; }
-        int unreadMessageCount() const { return unreadMsgCount_; }
-
-        void setAvatar(const QString &avatar_url);
-        void setDescriptionMessage(const DescInfo &info);
-        DescInfo lastMessageInfo() const { return lastMsgInfo_; }
-
-        QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
-        QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
-        QColor hoverTitleColor() const { return hoverTitleColor_; }
-        QColor hoverSubtitleColor() const { return hoverSubtitleColor_; }
-        QColor hoverTimestampColor() const { return hoverTimestampColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor highlightedTitleColor() const { return highlightedTitleColor_; }
-        QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; }
-        QColor highlightedTimestampColor() const { return highlightedTimestampColor_; }
-
-        QColor titleColor() const { return titleColor_; }
-        QColor subtitleColor() const { return subtitleColor_; }
-        QColor timestampColor() const { return timestampColor_; }
-        QColor btnColor() const { return btnColor_; }
-        QColor btnTextColor() const { return btnTextColor_; }
-
-        QColor bubbleFgColor() const { return bubbleFgColor_; }
-        QColor bubbleBgColor() const { return bubbleBgColor_; }
-        QColor mentionedColor() const { return mentionedFontColor_; }
-
-        void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
-        void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
-        void setHoverSubtitleColor(QColor &color) { hoverSubtitleColor_ = color; }
-        void setHoverTitleColor(QColor &color) { hoverTitleColor_ = color; }
-        void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; }
-        void setBackgroundColor(QColor &color) { backgroundColor_ = color; }
-        void setTimestampColor(QColor &color) { timestampColor_ = color; }
-
-        void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; }
-        void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; }
-        void setHighlightedTimestampColor(QColor &color) { highlightedTimestampColor_ = color; }
-
-        void setTitleColor(QColor &color) { titleColor_ = color; }
-        void setSubtitleColor(QColor &color) { subtitleColor_ = color; }
-
-        void setBtnColor(QColor &color) { btnColor_ = color; }
-        void setBtnTextColor(QColor &color) { btnTextColor_ = color; }
-
-        void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; }
-        void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; }
-        void setMentionedColor(QColor &color) { mentionedFontColor_ = color; }
-
-        void setRoomName(const QString &name) { roomName_ = name; }
-        void setRoomType(bool isInvite)
-        {
-                if (isInvite)
-                        roomType_ = RoomType::Invited;
-                else
-                        roomType_ = RoomType::Joined;
-        }
-
-        bool isInvite() const { return roomType_ == RoomType::Invited; }
-        void setReadState(bool hasUnreadMessages)
-        {
-                if (hasUnreadMessages_ != hasUnreadMessages) {
-                        hasUnreadMessages_ = hasUnreadMessages;
-                        update();
-                }
-        }
-
-signals:
-        void clicked(const QString &room_id);
-        void leaveRoom(const QString &room_id);
-        void acceptInvite(const QString &room_id);
-        void declineInvite(const QString &room_id);
-
-public slots:
-        void setPressedState(bool state);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        void init(QWidget *parent);
-        QString roomName() { return roomName_; }
-
-        RippleOverlay *ripple_overlay_;
-        Avatar *avatar_;
-
-        enum class RoomType
-        {
-                Joined,
-                Invited,
-        };
-
-        RoomType roomType_ = RoomType::Joined;
-
-        // State information for the invited rooms.
-        mtx::responses::InvitedRoom invitedRoom_;
-
-        QString roomId_;
-        QString roomName_;
-
-        DescInfo lastMsgInfo_;
-
-        QMenu *menu_;
-        QAction *leaveRoom_;
-
-        bool isPressed_         = false;
-        bool hasUnreadMessages_ = true;
-
-        int unreadMsgCount_            = 0;
-        int unreadHighlightedMsgCount_ = 0;
-
-        QColor highlightedBackgroundColor_;
-        QColor hoverBackgroundColor_;
-        QColor backgroundColor_;
-
-        QColor highlightedTitleColor_;
-        QColor highlightedSubtitleColor_;
-
-        QColor titleColor_;
-        QColor subtitleColor_;
-
-        QColor hoverTitleColor_;
-        QColor hoverSubtitleColor_;
-
-        QColor btnColor_;
-        QColor btnTextColor_;
-
-        QRectF acceptBtnRegion_;
-        QRectF declineBtnRegion_;
-
-        // Fonts
-        QColor mentionedFontColor_;
-        QFont unreadCountFont_;
-        int bubbleDiameter_;
-
-        QColor timestampColor_;
-        QColor highlightedTimestampColor_;
-        QColor hoverTimestampColor_;
-
-        QColor bubbleBgColor_;
-        QColor bubbleFgColor_;
-
-        friend struct room_sort;
-};
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
deleted file mode 100644
index 8a807e71..00000000
--- a/src/RoomList.cpp
+++ /dev/null
@@ -1,540 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Logging.h"
-#include "MainWindow.h"
-#include "RoomInfoListItem.h"
-#include "RoomList.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/OverlayModal.h"
-
-RoomList::RoomList(QSharedPointer userSettings, QWidget *parent)
-  : QWidget(parent)
-{
-        topLayout_ = new QVBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
-
-        scrollArea_ = new QScrollArea(this);
-        scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
-        scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter);
-        scrollArea_->setAttribute(Qt::WA_AcceptTouchEvents);
-
-        QScroller::grabGesture(scrollArea_, QScroller::TouchGesture);
-        QScroller::grabGesture(scrollArea_, QScroller::LeftMouseButtonGesture);
-
-// The scrollbar on macOS will hide itself when not active so it won't interfere
-// with the content.
-#if not defined(Q_OS_MAC)
-        scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-#endif
-
-        scrollAreaContents_ = new QWidget(this);
-        scrollAreaContents_->setObjectName("roomlist_area");
-
-        contentsLayout_ = new QVBoxLayout(scrollAreaContents_);
-        contentsLayout_->setAlignment(Qt::AlignTop);
-        contentsLayout_->setSpacing(0);
-        contentsLayout_->setMargin(0);
-
-        scrollArea_->setWidget(scrollAreaContents_);
-        topLayout_->addWidget(scrollArea_);
-
-        connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
-        connect(userSettings.data(),
-                &UserSettings::roomSortingChanged,
-                this,
-                &RoomList::sortRoomsByLastMessage);
-}
-
-void
-RoomList::addRoom(const QString &room_id, const RoomInfo &info)
-{
-        auto room_item = new RoomInfoListItem(room_id, info, scrollArea_);
-        room_item->setRoomName(QString::fromStdString(std::move(info.name)));
-
-        connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom);
-        connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) {
-                MainWindow::instance()->openLeaveRoomDialog(room_id);
-        });
-
-        QSharedPointer roomWidget(room_item, &QObject::deleteLater);
-        rooms_.emplace(room_id, roomWidget);
-        rooms_sort_cache_.push_back(roomWidget);
-
-        if (!info.avatar_url.empty())
-                updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-
-        int pos = contentsLayout_->count() - 1;
-        contentsLayout_->insertWidget(pos, room_item);
-}
-
-void
-RoomList::updateAvatar(const QString &room_id, const QString &url)
-{
-        emit updateRoomAvatarCb(room_id, url);
-}
-
-void
-RoomList::removeRoom(const QString &room_id, bool reset)
-{
-        auto roomIt = rooms_.find(room_id);
-        if (roomIt == rooms_.end()) {
-                return;
-        }
-
-        for (auto roomSortIt = rooms_sort_cache_.begin(); roomSortIt != rooms_sort_cache_.end();
-             ++roomSortIt) {
-                if (roomIt->second == *roomSortIt) {
-                        rooms_sort_cache_.erase(roomSortIt);
-                        break;
-                }
-        }
-        rooms_.erase(room_id);
-
-        if (rooms_.empty() || !reset)
-                return;
-
-        auto room = firstRoom();
-
-        if (room.second.isNull())
-                return;
-
-        room.second->setPressedState(true);
-        emit roomChanged(room.first);
-}
-
-void
-RoomList::updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount)
-{
-        if (!roomExists(roomid)) {
-                nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}",
-                                  roomid.toStdString());
-                return;
-        }
-
-        rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount);
-
-        calculateUnreadMessageCount();
-
-        sortRoomsByLastMessage();
-}
-
-void
-RoomList::calculateUnreadMessageCount()
-{
-        int total_unread_msgs = 0;
-
-        for (const auto &room : rooms_) {
-                if (!room.second.isNull())
-                        total_unread_msgs += room.second->unreadMessageCount();
-        }
-
-        emit totalUnreadMessageCountUpdated(total_unread_msgs);
-}
-
-void
-RoomList::initialize(const QMap &info)
-{
-        nhlog::ui()->info("initialize room list");
-
-        rooms_.clear();
-
-        // prevent flickering and save time sorting over and over again
-        setUpdatesEnabled(false);
-        for (auto it = info.begin(); it != info.end(); it++) {
-                if (it.value().is_invite)
-                        addInvitedRoom(it.key(), it.value());
-                else
-                        addRoom(it.key(), it.value());
-        }
-
-        for (auto it = info.begin(); it != info.end(); it++)
-                updateRoomDescription(it.key(), it.value().msgInfo);
-
-        setUpdatesEnabled(true);
-
-        if (rooms_.empty())
-                return;
-
-        sortRoomsByLastMessage();
-
-        auto room = firstRoom();
-        if (room.second.isNull())
-                return;
-
-        room.second->setPressedState(true);
-        emit roomChanged(room.first);
-}
-
-void
-RoomList::cleanupInvites(const std::map &invites)
-{
-        if (invites.size() == 0)
-                return;
-
-        utils::erase_if(rooms_, [invites](auto &room) {
-                auto room_id = room.first;
-                auto item    = room.second;
-
-                if (!item)
-                        return false;
-
-                return item->isInvite() && (invites.find(room_id) == invites.end());
-        });
-}
-
-void
-RoomList::sync(const std::map &info)
-
-{
-        for (const auto &room : info)
-                updateRoom(room.first, room.second);
-
-        if (!info.empty())
-                sortRoomsByLastMessage();
-}
-
-void
-RoomList::highlightSelectedRoom(const QString &room_id)
-{
-        emit roomChanged(room_id);
-
-        if (!roomExists(room_id)) {
-                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                return;
-        }
-
-        for (auto const &room : rooms_) {
-                if (room.second.isNull())
-                        continue;
-
-                if (room.first != room_id) {
-                        room.second->setPressedState(false);
-                } else {
-                        room.second->setPressedState(true);
-                        scrollArea_->ensureWidgetVisible(room.second.data());
-                }
-        }
-
-        selectedRoom_ = room_id;
-}
-
-void
-RoomList::nextRoom()
-{
-        for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) {
-                auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget());
-
-                if (!room)
-                        continue;
-
-                if (room->roomId() == selectedRoom_) {
-                        auto nextRoom = qobject_cast(
-                          contentsLayout_->itemAt(ii + 1)->widget());
-
-                        // Not a room message.
-                        if (!nextRoom || nextRoom->isInvite())
-                                return;
-
-                        emit roomChanged(nextRoom->roomId());
-                        if (!roomExists(nextRoom->roomId())) {
-                                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                                return;
-                        }
-
-                        room->setPressedState(false);
-                        nextRoom->setPressedState(true);
-
-                        scrollArea_->ensureWidgetVisible(nextRoom);
-                        selectedRoom_ = nextRoom->roomId();
-                        return;
-                }
-        }
-}
-
-void
-RoomList::previousRoom()
-{
-        for (int ii = 1; ii < contentsLayout_->count(); ++ii) {
-                auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget());
-
-                if (!room)
-                        continue;
-
-                if (room->roomId() == selectedRoom_) {
-                        auto nextRoom = qobject_cast(
-                          contentsLayout_->itemAt(ii - 1)->widget());
-
-                        // Not a room message.
-                        if (!nextRoom || nextRoom->isInvite())
-                                return;
-
-                        emit roomChanged(nextRoom->roomId());
-                        if (!roomExists(nextRoom->roomId())) {
-                                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                                return;
-                        }
-
-                        room->setPressedState(false);
-                        nextRoom->setPressedState(true);
-
-                        scrollArea_->ensureWidgetVisible(nextRoom);
-                        selectedRoom_ = nextRoom->roomId();
-                        return;
-                }
-        }
-}
-
-void
-RoomList::updateRoomAvatar(const QString &roomid, const QString &img)
-{
-        if (!roomExists(roomid)) {
-                nhlog::ui()->warn("avatar update on non-existent room_id: {}",
-                                  roomid.toStdString());
-                return;
-        }
-
-        rooms_[roomid]->setAvatar(img);
-
-        // Used to inform other widgets for the new image data.
-        emit roomAvatarChanged(roomid, img);
-}
-
-void
-RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
-{
-        if (!roomExists(roomid)) {
-                nhlog::ui()->warn("description update on non-existent room_id: {}, {}",
-                                  roomid.toStdString(),
-                                  info.body.toStdString());
-                return;
-        }
-
-        rooms_[roomid]->setDescriptionMessage(info);
-
-        if (underMouse()) {
-                // When the user hover out of the roomlist a sort will be triggered.
-                isSortPending_ = true;
-                return;
-        }
-
-        isSortPending_ = false;
-
-        emit sortRoomsByLastMessage();
-}
-
-struct room_sort
-{
-        bool operator()(const QSharedPointer &a,
-                        const QSharedPointer &b) const
-        {
-                // Sort by "importance" (i.e. invites before mentions before
-                // notifs before new events before old events), then secondly
-                // by recency.
-
-                // Checking importance first
-                const auto a_importance = a->calculateImportance();
-                const auto b_importance = b->calculateImportance();
-                if (a_importance != b_importance) {
-                        return a_importance > b_importance;
-                }
-
-                // Now sort by recency
-                // Zero if empty, otherwise the time that the event occured
-                const uint64_t a_recency =
-                  a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp;
-                const uint64_t b_recency =
-                  b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp;
-                return a_recency > b_recency;
-        }
-};
-
-void
-RoomList::sortRoomsByLastMessage()
-{
-        isSortPending_ = false;
-
-        std::stable_sort(begin(rooms_sort_cache_), end(rooms_sort_cache_), room_sort{});
-
-        int newIndex = 0;
-        for (const auto &roomWidget : rooms_sort_cache_) {
-                const auto currentIndex = contentsLayout_->indexOf(roomWidget.data());
-
-                if (currentIndex != newIndex) {
-                        contentsLayout_->removeWidget(roomWidget.data());
-                        contentsLayout_->insertWidget(newIndex, roomWidget.data());
-                }
-                newIndex++;
-        }
-}
-
-void
-RoomList::leaveEvent(QEvent *event)
-{
-        if (isSortPending_)
-                QTimer::singleShot(700, this, &RoomList::sortRoomsByLastMessage);
-
-        QWidget::leaveEvent(event);
-}
-
-void
-RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
-{
-        joinRoomModal_->hide();
-
-        if (isJoining)
-                emit joinRoom(roomAlias);
-}
-
-void
-RoomList::removeFilter(const std::set &roomsToHide)
-{
-        setUpdatesEnabled(false);
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto widget =
-                  qobject_cast(contentsLayout_->itemAt(i)->widget());
-                if (widget) {
-                        if (roomsToHide.find(widget->roomId()) == roomsToHide.end())
-                                widget->show();
-                        else
-                                widget->hide();
-                }
-        }
-        setUpdatesEnabled(true);
-}
-
-void
-RoomList::applyFilter(const std::set &filter)
-{
-        // Disabling paint updates will resolve issues with screen flickering on big room lists.
-        setUpdatesEnabled(false);
-
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                // If filter contains the room for the current RoomInfoListItem,
-                // show the list item, otherwise hide it
-                auto listitem =
-                  qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (!listitem)
-                        continue;
-
-                if (filter.find(listitem->roomId()) != filter.end())
-                        listitem->show();
-                else
-                        listitem->hide();
-        }
-
-        setUpdatesEnabled(true);
-
-        // If the already selected room is part of the group, make sure it's visible.
-        if (!selectedRoom_.isEmpty() && (filter.find(selectedRoom_) != filter.end()))
-                return;
-
-        selectFirstVisibleRoom();
-}
-
-void
-RoomList::selectFirstVisibleRoom()
-{
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto item = qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (item && item->isVisible()) {
-                        highlightSelectedRoom(item->roomId());
-                        break;
-                }
-        }
-}
-
-void
-RoomList::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-RoomList::updateRoom(const QString &room_id, const RoomInfo &info)
-{
-        if (!roomExists(room_id)) {
-                if (info.is_invite)
-                        addInvitedRoom(room_id, info);
-                else
-                        addRoom(room_id, info);
-
-                return;
-        }
-
-        auto room = rooms_[room_id];
-        updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-        room->setRoomName(QString::fromStdString(info.name));
-        room->setRoomType(info.is_invite);
-        room->update();
-}
-
-void
-RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info)
-{
-        auto room_item = new RoomInfoListItem(room_id, info, scrollArea_);
-
-        connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite);
-        connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite);
-
-        QSharedPointer roomWidget(room_item);
-        rooms_.emplace(room_id, roomWidget);
-        rooms_sort_cache_.push_back(roomWidget);
-
-        updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-
-        int pos = contentsLayout_->count() - 1;
-        contentsLayout_->insertWidget(pos, room_item);
-}
-
-std::pair>
-RoomList::firstRoom() const
-{
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto item = qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (item) {
-                        auto topRoom = rooms_.find(item->roomId());
-                        if (topRoom != rooms_.end()) {
-                                return std::pair>(
-                                  item->roomId(), topRoom->second);
-                        }
-                }
-        }
-
-        return {};
-}
-
-void
-RoomList::updateReadStatus(const std::map &status)
-{
-        for (const auto &room : status) {
-                if (roomExists(room.first)) {
-                        auto item = rooms_.at(room.first);
-
-                        if (item)
-                                item->setReadState(room.second);
-                }
-        }
-}
diff --git a/src/RoomList.h b/src/RoomList.h
deleted file mode 100644
index 74152c55..00000000
--- a/src/RoomList.h
+++ /dev/null
@@ -1,101 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "CacheStructs.h"
-#include "UserSettingsPage.h"
-
-class LeaveRoomDialog;
-class OverlayModal;
-class RoomInfoListItem;
-class Sync;
-struct DescInfo;
-struct RoomInfo;
-
-class RoomList : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit RoomList(QSharedPointer userSettings, QWidget *parent = nullptr);
-
-        void initialize(const QMap &info);
-        void sync(const std::map &info);
-
-        void clear()
-        {
-                rooms_.clear();
-                rooms_sort_cache_.clear();
-        };
-        void updateAvatar(const QString &room_id, const QString &url);
-
-        void addRoom(const QString &room_id, const RoomInfo &info);
-        void addInvitedRoom(const QString &room_id, const RoomInfo &info);
-        void removeRoom(const QString &room_id, bool reset);
-        //! Hide rooms that are not present in the given filter.
-        void applyFilter(const std::set &rooms);
-        //! Show all the available rooms.
-        void removeFilter(const std::set &roomsToHide);
-        void updateRoom(const QString &room_id, const RoomInfo &info);
-        void cleanupInvites(const std::map &invites);
-
-signals:
-        void roomChanged(const QString &room_id);
-        void totalUnreadMessageCountUpdated(int count);
-        void acceptInvite(const QString &room_id);
-        void declineInvite(const QString &room_id);
-        void roomAvatarChanged(const QString &room_id, const QString &img);
-        void joinRoom(const QString &room_id);
-        void updateRoomAvatarCb(const QString &room_id, const QString &img);
-
-public slots:
-        void updateRoomAvatar(const QString &roomid, const QString &img);
-        void highlightSelectedRoom(const QString &room_id);
-        void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount);
-        void updateRoomDescription(const QString &roomid, const DescInfo &info);
-        void closeJoinRoomDialog(bool isJoining, QString roomAlias);
-        void updateReadStatus(const std::map &status);
-        void nextRoom();
-        void previousRoom();
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void leaveEvent(QEvent *event) override;
-
-private slots:
-        void sortRoomsByLastMessage();
-
-private:
-        //! Return the first non-null room.
-        std::pair> firstRoom() const;
-        void calculateUnreadMessageCount();
-        bool roomExists(const QString &room_id) { return rooms_.find(room_id) != rooms_.end(); }
-        //! Select the first visible room in the room list.
-        void selectFirstVisibleRoom();
-
-        QVBoxLayout *topLayout_;
-        QVBoxLayout *contentsLayout_;
-        QScrollArea *scrollArea_;
-        QWidget *scrollAreaContents_;
-
-        QPushButton *joinRoomButton_;
-
-        OverlayModal *joinRoomModal_;
-
-        std::map> rooms_;
-        std::vector> rooms_sort_cache_;
-        QString selectedRoom_;
-
-        bool isSortPending_ = false;
-};
diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp
deleted file mode 100644
index 0b7756f0..00000000
--- a/src/SideBarActions.cpp
+++ /dev/null
@@ -1,120 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "Config.h"
-#include "MainWindow.h"
-#include "SideBarActions.h"
-#include "Splitter.h"
-#include "ui/FlatButton.h"
-#include "ui/Menu.h"
-
-SideBarActions::SideBarActions(QWidget *parent)
-  : QWidget{parent}
-{
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        const int fontHeight    = QFontMetrics(f).height();
-        const int contentHeight = fontHeight * 2.5;
-
-        setFixedHeight(contentHeight);
-
-        layout_ = new QHBoxLayout(this);
-        layout_->setMargin(0);
-
-        QIcon settingsIcon;
-        settingsIcon.addFile(":/icons/icons/ui/settings.png");
-
-        QIcon createRoomIcon;
-        createRoomIcon.addFile(":/icons/icons/ui/add-square-button.png");
-
-        QIcon joinRoomIcon;
-        joinRoomIcon.addFile(":/icons/icons/ui/speech-bubbles-comment-option.png");
-
-        settingsBtn_ = new FlatButton(this);
-        settingsBtn_->setToolTip(tr("User settings"));
-        settingsBtn_->setIcon(settingsIcon);
-        settingsBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2);
-        settingsBtn_->setIconSize(
-          QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize));
-
-        addMenu_          = new Menu(this);
-        createRoomAction_ = new QAction(tr("Create new room"), this);
-        joinRoomAction_   = new QAction(tr("Join a room"), this);
-
-        connect(joinRoomAction_, &QAction::triggered, this, [this]() {
-                MainWindow::instance()->openJoinRoomDialog(
-                  [this](const QString &room_id) { emit joinRoom(room_id); });
-        });
-
-        connect(createRoomAction_, &QAction::triggered, this, [this]() {
-                MainWindow::instance()->openCreateRoomDialog(
-                  [this](const mtx::requests::CreateRoom &req) { emit createRoom(req); });
-        });
-
-        addMenu_->addAction(createRoomAction_);
-        addMenu_->addAction(joinRoomAction_);
-
-        createRoomBtn_ = new FlatButton(this);
-        createRoomBtn_->setToolTip(tr("Start a new chat"));
-        createRoomBtn_->setIcon(createRoomIcon);
-        createRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2);
-        createRoomBtn_->setIconSize(
-          QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize));
-
-        connect(createRoomBtn_, &QPushButton::clicked, this, [this]() {
-                auto pos     = mapToGlobal(createRoomBtn_->pos());
-                auto padding = conf::sidebarActions::iconSize / 2;
-
-                addMenu_->popup(
-                  QPoint(pos.x() + padding, pos.y() - padding - addMenu_->sizeHint().height()));
-        });
-
-        roomDirectory_ = new FlatButton(this);
-        roomDirectory_->setToolTip(tr("Room directory"));
-        roomDirectory_->setEnabled(false);
-        roomDirectory_->setIcon(joinRoomIcon);
-        roomDirectory_->setCornerRadius(conf::sidebarActions::iconSize / 2);
-        roomDirectory_->setIconSize(
-          QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize));
-
-        layout_->addWidget(createRoomBtn_);
-        layout_->addWidget(roomDirectory_);
-        layout_->addWidget(settingsBtn_);
-
-        connect(settingsBtn_, &QPushButton::clicked, this, &SideBarActions::showSettings);
-}
-
-void
-SideBarActions::resizeEvent(QResizeEvent *event)
-{
-        Q_UNUSED(event);
-
-        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() <= sidebarSizes.small) {
-                roomDirectory_->hide();
-                createRoomBtn_->hide();
-        } else {
-                roomDirectory_->show();
-                createRoomBtn_->show();
-        }
-}
-
-void
-SideBarActions::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/SideBarActions.h b/src/SideBarActions.h
deleted file mode 100644
index 566aa76b..00000000
--- a/src/SideBarActions.h
+++ /dev/null
@@ -1,54 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-namespace mtx {
-namespace requests {
-struct CreateRoom;
-}
-}
-
-class Menu;
-class FlatButton;
-class QResizeEvent;
-
-class SideBarActions : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
-
-public:
-        SideBarActions(QWidget *parent = nullptr);
-
-        QColor borderColor() const { return borderColor_; }
-        void setBorderColor(QColor &color) { borderColor_ = color; }
-
-signals:
-        void showSettings();
-        void joinRoom(const QString &room);
-        void createRoom(const mtx::requests::CreateRoom &request);
-
-protected:
-        void resizeEvent(QResizeEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-
-private:
-        QHBoxLayout *layout_;
-
-        Menu *addMenu_;
-        QAction *createRoomAction_;
-        QAction *joinRoomAction_;
-
-        FlatButton *settingsBtn_;
-        FlatButton *createRoomBtn_;
-        FlatButton *roomDirectory_;
-
-        QColor borderColor_;
-};
diff --git a/src/Splitter.cpp b/src/Splitter.cpp
deleted file mode 100644
index 15e3f5c5..00000000
--- a/src/Splitter.cpp
+++ /dev/null
@@ -1,166 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-
-#include "Logging.h"
-#include "Splitter.h"
-
-constexpr auto MaxWidth = (1 << 24) - 1;
-
-Splitter::Splitter(QWidget *parent)
-  : QSplitter(parent)
-  , sz_{splitter::calculateSidebarSizes(QFont{})}
-{
-        connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved);
-        setChildrenCollapsible(false);
-}
-
-void
-Splitter::restoreSizes(int fallback)
-{
-        QSettings settings;
-        int savedWidth = settings.value("sidebar/width").toInt();
-
-        auto left = widget(0);
-        if (savedWidth <= 0) {
-                hideSidebar();
-                return;
-        } else if (savedWidth <= sz_.small) {
-                if (left) {
-                        left->setMinimumWidth(sz_.small);
-                        left->setMaximumWidth(sz_.small);
-                        return;
-                }
-        } else if (savedWidth < sz_.normal) {
-                savedWidth = sz_.normal;
-        }
-
-        left->setMinimumWidth(sz_.normal);
-        left->setMaximumWidth(2 * sz_.normal);
-        setSizes({savedWidth, fallback - savedWidth});
-
-        setStretchFactor(0, 0);
-        setStretchFactor(1, 1);
-}
-
-Splitter::~Splitter()
-{
-        auto left = widget(0);
-
-        if (left) {
-                QSettings settings;
-                settings.setValue("sidebar/width", left->width());
-        }
-}
-
-void
-Splitter::onSplitterMoved(int pos, int index)
-{
-        Q_UNUSED(pos);
-        Q_UNUSED(index);
-
-        auto s = sizes();
-
-        if (s.count() < 2) {
-                nhlog::ui()->warn("Splitter needs at least two children");
-                return;
-        }
-
-        if (s[0] == sz_.normal) {
-                rightMoveCount_ += 1;
-
-                if (rightMoveCount_ > moveEventLimit_) {
-                        auto left           = widget(0);
-                        auto cursorPosition = left->mapFromGlobal(QCursor::pos());
-
-                        // if we are coming from the right, the cursor should
-                        // end up on the first widget.
-                        if (left->rect().contains(cursorPosition)) {
-                                left->setMinimumWidth(sz_.small);
-                                left->setMaximumWidth(sz_.small);
-
-                                rightMoveCount_ = 0;
-                        }
-                }
-        } else if (s[0] == sz_.small) {
-                leftMoveCount_ += 1;
-
-                if (leftMoveCount_ > moveEventLimit_) {
-                        auto left           = widget(0);
-                        auto right          = widget(1);
-                        auto cursorPosition = right->mapFromGlobal(QCursor::pos());
-
-                        // We move the start a little further so the transition isn't so abrupt.
-                        auto extended = right->rect();
-                        extended.translate(100, 0);
-
-                        // if we are coming from the left, the cursor should
-                        // end up on the second widget.
-                        if (extended.contains(cursorPosition) &&
-                            right->size().width() >= sz_.collapsePoint + sz_.normal) {
-                                left->setMinimumWidth(sz_.normal);
-                                left->setMaximumWidth(2 * sz_.normal);
-
-                                leftMoveCount_ = 0;
-                        }
-                }
-        }
-}
-
-void
-Splitter::hideSidebar()
-{
-        auto left = widget(0);
-        if (left)
-                left->hide();
-}
-
-void
-Splitter::showChatView()
-{
-        auto left  = widget(0);
-        auto right = widget(1);
-
-        if (right->isHidden()) {
-                left->hide();
-                right->show();
-
-                // Restore previous size.
-                if (left->minimumWidth() == sz_.small) {
-                        left->setMinimumWidth(sz_.small);
-                        left->setMaximumWidth(sz_.small);
-                } else {
-                        left->setMinimumWidth(sz_.normal);
-                        left->setMaximumWidth(2 * sz_.normal);
-                }
-        }
-}
-
-void
-Splitter::showFullRoomList()
-{
-        auto left  = widget(0);
-        auto right = widget(1);
-
-        right->hide();
-
-        left->show();
-        left->setMaximumWidth(MaxWidth);
-}
-
-splitter::SideBarSizes
-splitter::calculateSidebarSizes(const QFont &f)
-{
-        const auto height = static_cast(QFontMetrics{f}.lineSpacing());
-
-        SideBarSizes sz;
-        sz.small         = std::ceil(3.8 * height);
-        sz.normal        = std::ceil(16 * height);
-        sz.groups        = std::ceil(3 * height);
-        sz.collapsePoint = 2 * sz.normal;
-
-        return sz;
-}
diff --git a/src/Splitter.h b/src/Splitter.h
deleted file mode 100644
index 94622f89..00000000
--- a/src/Splitter.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-namespace splitter {
-struct SideBarSizes
-{
-        int small;
-        int normal;
-        int groups;
-        int collapsePoint;
-};
-
-SideBarSizes
-calculateSidebarSizes(const QFont &f);
-}
-
-class Splitter : public QSplitter
-{
-        Q_OBJECT
-public:
-        explicit Splitter(QWidget *parent = nullptr);
-        ~Splitter() override;
-
-        void restoreSizes(int fallback);
-
-public slots:
-        void hideSidebar();
-        void showFullRoomList();
-        void showChatView();
-
-signals:
-        void hiddenSidebar();
-
-private:
-        void onSplitterMoved(int pos, int index);
-
-        int moveEventLimit_ = 50;
-
-        int leftMoveCount_  = 0;
-        int rightMoveCount_ = 0;
-
-        splitter::SideBarSizes sz_;
-};
diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp
deleted file mode 100644
index 3d526b8b..00000000
--- a/src/UserInfoWidget.cpp
+++ /dev/null
@@ -1,219 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "ChatPage.h"
-#include "Config.h"
-#include "MainWindow.h"
-#include "Splitter.h"
-#include "UserInfoWidget.h"
-#include "UserSettingsPage.h"
-#include "ui/Avatar.h"
-#include "ui/FlatButton.h"
-#include "ui/OverlayModal.h"
-
-UserInfoWidget::UserInfoWidget(QWidget *parent)
-  : QWidget(parent)
-  , display_name_("User")
-  , user_id_("@user:homeserver.org")
-{
-        QFont f;
-        f.setPointSizeF(f.pointSizeF());
-
-        const int fontHeight    = QFontMetrics(f).height();
-        const int widgetMargin  = fontHeight / 3;
-        const int contentHeight = fontHeight * 3;
-
-        logoutButtonSize_ = std::min(fontHeight, 20);
-
-        setFixedHeight(contentHeight + widgetMargin);
-
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(widgetMargin);
-
-        avatarLayout_ = new QHBoxLayout();
-        textLayout_   = new QVBoxLayout();
-        textLayout_->setSpacing(widgetMargin / 2);
-        textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin);
-
-        userAvatar_ = new Avatar(this, fontHeight * 2.5);
-        userAvatar_->setObjectName("userAvatar");
-        userAvatar_->setLetter(QChar('?'));
-
-        QFont nameFont;
-        nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-        nameFont.setWeight(QFont::Medium);
-
-        displayNameLabel_ = new QLabel(this);
-        displayNameLabel_->setFont(nameFont);
-        displayNameLabel_->setObjectName("displayNameLabel");
-        displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop);
-
-        userIdLabel_ = new QLabel(this);
-        userIdLabel_->setFont(f);
-        userIdLabel_->setObjectName("userIdLabel");
-        userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
-
-        avatarLayout_->addWidget(userAvatar_);
-        textLayout_->addWidget(displayNameLabel_, 0, Qt::AlignBottom);
-        textLayout_->addWidget(userIdLabel_, 0, Qt::AlignTop);
-
-        topLayout_->addLayout(avatarLayout_);
-        topLayout_->addLayout(textLayout_);
-        topLayout_->addStretch(1);
-
-        buttonLayout_ = new QHBoxLayout();
-        buttonLayout_->setSpacing(0);
-        buttonLayout_->setMargin(0);
-
-        logoutButton_ = new FlatButton(this);
-        logoutButton_->setToolTip(tr("Logout"));
-        logoutButton_->setCornerRadius(logoutButtonSize_ / 2);
-
-        QIcon icon;
-        icon.addFile(":/icons/icons/ui/power-button-off.png");
-
-        logoutButton_->setIcon(icon);
-        logoutButton_->setIconSize(QSize(logoutButtonSize_, logoutButtonSize_));
-
-        buttonLayout_->addWidget(logoutButton_);
-
-        topLayout_->addLayout(buttonLayout_);
-
-        // Show the confirmation dialog.
-        connect(logoutButton_, &QPushButton::clicked, this, []() {
-                MainWindow::instance()->openLogoutDialog();
-        });
-
-        menu = new QMenu(this);
-
-        auto setStatusAction = menu->addAction(tr("Set custom status message"));
-        connect(setStatusAction, &QAction::triggered, this, [this]() {
-                bool ok      = false;
-                QString text = QInputDialog::getText(this,
-                                                     tr("Custom status message"),
-                                                     tr("Status:"),
-                                                     QLineEdit::Normal,
-                                                     ChatPage::instance()->status(),
-                                                     &ok);
-                if (ok)
-                        ChatPage::instance()->setStatus(text);
-        });
-
-        auto userProfileAction = menu->addAction(tr("User Profile Settings"));
-        connect(
-          userProfileAction, &QAction::triggered, this, [this]() { emit openGlobalUserProfile(); });
-
-#if 0 // disable presence menu until issues in synapse are resolved
-        auto setAutoPresence = menu->addAction(tr("Set presence automatically"));
-        connect(setAutoPresence, &QAction::triggered, this, []() {
-                ChatPage::instance()->userSettings()->setPresence(
-                  UserSettings::Presence::AutomaticPresence);
-                ChatPage::instance()->setStatus(ChatPage::instance()->status());
-        });
-        auto setOnline = menu->addAction(tr("Online"));
-        connect(setOnline, &QAction::triggered, this, []() {
-                ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Online);
-                ChatPage::instance()->setStatus(ChatPage::instance()->status());
-        });
-        auto setUnavailable = menu->addAction(tr("Unavailable"));
-        connect(setUnavailable, &QAction::triggered, this, []() {
-                ChatPage::instance()->userSettings()->setPresence(
-                  UserSettings::Presence::Unavailable);
-                ChatPage::instance()->setStatus(ChatPage::instance()->status());
-        });
-        auto setOffline = menu->addAction(tr("Offline"));
-        connect(setOffline, &QAction::triggered, this, []() {
-                ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Offline);
-                ChatPage::instance()->setStatus(ChatPage::instance()->status());
-        });
-#endif
-}
-
-void
-UserInfoWidget::contextMenuEvent(QContextMenuEvent *event)
-{
-        menu->popup(event->globalPos());
-}
-
-void
-UserInfoWidget::resizeEvent(QResizeEvent *event)
-{
-        Q_UNUSED(event);
-
-        const auto sz = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() <= sz.small) {
-                topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0);
-
-                userAvatar_->hide();
-                displayNameLabel_->hide();
-                userIdLabel_->hide();
-        } else {
-                topLayout_->setMargin(5);
-                userAvatar_->show();
-                displayNameLabel_->show();
-                userIdLabel_->show();
-        }
-
-        QWidget::resizeEvent(event);
-}
-
-void
-UserInfoWidget::reset()
-{
-        displayNameLabel_->setText("");
-        userIdLabel_->setText("");
-        userAvatar_->setLetter(QChar('?'));
-}
-
-void
-UserInfoWidget::setDisplayName(const QString &name)
-{
-        if (name.isEmpty())
-                display_name_ = user_id_.split(':')[0].split('@')[1];
-        else
-                display_name_ = name;
-
-        displayNameLabel_->setText(display_name_);
-        userAvatar_->setLetter(QChar(display_name_[0]));
-        update();
-}
-
-void
-UserInfoWidget::setUserId(const QString &userid)
-{
-        user_id_ = userid;
-        userIdLabel_->setText(userid);
-        update();
-}
-
-void
-UserInfoWidget::setAvatar(const QString &url)
-{
-        userAvatar_->setImage(url);
-        update();
-}
-
-void
-UserInfoWidget::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h
deleted file mode 100644
index 5aec1cda..00000000
--- a/src/UserInfoWidget.h
+++ /dev/null
@@ -1,68 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-class Avatar;
-class FlatButton;
-class OverlayModal;
-
-class QLabel;
-class QHBoxLayout;
-class QVBoxLayout;
-class QMenu;
-
-class UserInfoWidget : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
-
-public:
-        UserInfoWidget(QWidget *parent = nullptr);
-
-        void setDisplayName(const QString &name);
-        void setUserId(const QString &userid);
-        void setAvatar(const QString &url);
-
-        void reset();
-
-        QColor borderColor() const { return borderColor_; }
-        void setBorderColor(QColor &color) { borderColor_ = color; }
-
-protected:
-        void resizeEvent(QResizeEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *) override;
-
-signals:
-        void openGlobalUserProfile();
-
-private:
-        Avatar *userAvatar_;
-
-        QHBoxLayout *topLayout_;
-        QHBoxLayout *avatarLayout_;
-        QVBoxLayout *textLayout_;
-        QHBoxLayout *buttonLayout_;
-
-        FlatButton *logoutButton_;
-
-        QLabel *displayNameLabel_;
-        QLabel *userIdLabel_;
-
-        QString display_name_;
-        QString user_id_;
-
-        QImage avatar_image_;
-
-        int logoutButtonSize_;
-
-        QColor borderColor_;
-
-        QMenu *menu = nullptr;
-};
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 0edc1288..9b906555 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -64,10 +64,14 @@ void
 UserSettings::load(std::optional profile)
 {
         QSettings settings;
-        tray_                    = settings.value("user/window/tray", false).toBool();
+        tray_        = settings.value("user/window/tray", false).toBool();
+        startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
+
+        roomListWidth_      = settings.value("user/sidebar/room_list_width", -1).toInt();
+        communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt();
+
         hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
         hasAlertOnNotification_  = settings.value("user/alert_on_notification", false).toBool();
-        startInTray_             = settings.value("user/window/start_in_tray", false).toBool();
         groupView_               = settings.value("user/group_view", true).toBool();
         hiddenTags_              = settings.value("user/hidden_tags", QStringList{}).toStringList();
         buttonsInTimeline_       = settings.value("user/timeline/buttons", true).toBool();
@@ -248,6 +252,24 @@ UserSettings::setTimelineMaxWidth(int state)
         emit timelineMaxWidthChanged(state);
         save();
 }
+void
+UserSettings::setCommunityListWidth(int state)
+{
+        if (state == communityListWidth_)
+                return;
+        communityListWidth_ = state;
+        emit communityListWidthChanged(state);
+        save();
+}
+void
+UserSettings::setRoomListWidth(int state)
+{
+        if (state == roomListWidth_)
+                return;
+        roomListWidth_ = state;
+        emit roomListWidthChanged(state);
+        save();
+}
 
 void
 UserSettings::setDesktopNotifications(bool state)
@@ -545,49 +567,14 @@ UserSettings::applyTheme()
 {
         QFile stylefile;
 
-        static QPalette original;
         if (this->theme() == "light") {
                 stylefile.setFileName(":/styles/styles/nheko.qss");
-                QPalette lightActive(
-                  /*windowText*/ QColor("#333"),
-                  /*button*/ QColor("white"),
-                  /*light*/ QColor(0xef, 0xef, 0xef),
-                  /*dark*/ QColor(110, 110, 110),
-                  /*mid*/ QColor(220, 220, 220),
-                  /*text*/ QColor("#333"),
-                  /*bright_text*/ QColor("#333"),
-                  /*base*/ QColor("#fff"),
-                  /*window*/ QColor("white"));
-                lightActive.setColor(QPalette::AlternateBase, QColor("#eee"));
-                lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
-                lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
-                lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
-                lightActive.setColor(QPalette::Link, QColor("#0077b5"));
-                lightActive.setColor(QPalette::ButtonText, QColor("#333"));
-                QApplication::setPalette(lightActive);
         } else if (this->theme() == "dark") {
                 stylefile.setFileName(":/styles/styles/nheko-dark.qss");
-                QPalette darkActive(
-                  /*windowText*/ QColor("#caccd1"),
-                  /*button*/ QColor(0xff, 0xff, 0xff),
-                  /*light*/ QColor("#caccd1"),
-                  /*dark*/ QColor(110, 110, 110),
-                  /*mid*/ QColor("#202228"),
-                  /*text*/ QColor("#caccd1"),
-                  /*bright_text*/ QColor(0xff, 0xff, 0xff),
-                  /*base*/ QColor("#202228"),
-                  /*window*/ QColor("#2d3139"));
-                darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139"));
-                darkActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
-                darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
-                darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
-                darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
-                darkActive.setColor(QPalette::ButtonText, "#727274");
-                QApplication::setPalette(darkActive);
         } else {
                 stylefile.setFileName(":/styles/styles/system.qss");
-                QApplication::setPalette(original);
         }
+        QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString()));
 
         stylefile.open(QFile::ReadOnly);
         QString stylesheet = QString(stylefile.readAll());
@@ -606,6 +593,11 @@ UserSettings::save()
         settings.setValue("start_in_tray", startInTray_);
         settings.endGroup(); // window
 
+        settings.beginGroup("sidebar");
+        settings.setValue("community_list_width", communityListWidth_);
+        settings.setValue("room_list_width", roomListWidth_);
+        settings.endGroup(); // window
+
         settings.beginGroup("timeline");
         settings.setValue("buttons", buttonsInTimeline_);
         settings.setValue("message_hover_highlight", messageHoverHighlight_);
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 3ad0293b..acb08569 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -61,6 +61,10 @@ class UserSettings : public QObject
                      NOTIFY privacyScreenTimeoutChanged)
         Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY
                      timelineMaxWidthChanged)
+        Q_PROPERTY(
+          int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged)
+        Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY
+                     communityListWidthChanged)
         Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged)
         Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
         Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged)
@@ -129,6 +133,8 @@ public:
         void setSortByImportance(bool state);
         void setButtonsInTimeline(bool state);
         void setTimelineMaxWidth(int state);
+        void setCommunityListWidth(int state);
+        void setRoomListWidth(int state);
         void setDesktopNotifications(bool state);
         void setAlertOnNotification(bool state);
         void setAvatarCircles(bool state);
@@ -178,6 +184,8 @@ public:
                 return hasDesktopNotifications() || hasAlertOnNotification();
         }
         int timelineMaxWidth() const { return timelineMaxWidth_; }
+        int communityListWidth() const { return communityListWidth_; }
+        int roomListWidth() const { return roomListWidth_; }
         double fontSize() const { return baseFontSize_; }
         QString font() const { return font_; }
         QString emojiFont() const
@@ -227,6 +235,8 @@ signals:
         void privacyScreenChanged(bool state);
         void privacyScreenTimeoutChanged(int state);
         void timelineMaxWidthChanged(int state);
+        void roomListWidthChanged(int state);
+        void communityListWidthChanged(int state);
         void mobileModeChanged(bool mode);
         void fontSizeChanged(double state);
         void fontChanged(QString state);
@@ -276,6 +286,8 @@ private:
         bool shareKeysWithTrustedUsers_;
         bool mobileMode_;
         int timelineMaxWidth_;
+        int roomListWidth_;
+        int communityListWidth_;
         double baseFontSize_;
         QString font_;
         QString emojiFont_;
diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp
deleted file mode 100644
index 2daa6143..00000000
--- a/src/popups/PopupItem.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-
-#include "../Utils.h"
-#include "../ui/Avatar.h"
-#include "PopupItem.h"
-
-constexpr int PopupHMargin    = 4;
-constexpr int PopupItemMargin = 3;
-
-PopupItem::PopupItem(QWidget *parent)
-  : QWidget(parent)
-  , avatar_{new Avatar(this, conf::popup::avatar)}
-  , hovering_{false}
-{
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setContentsMargins(
-          PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin);
-
-        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-}
-
-void
-PopupItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-
-        if (underMouse() || hovering_)
-                p.fillRect(rect(), hoverColor_);
-}
-
-RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res)
-  : PopupItem(parent)
-  , roomId_{QString::fromStdString(res.room_id)}
-{
-        auto name = QFontMetrics(QFont()).elidedText(
-          QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10);
-
-        avatar_->setLetter(utils::firstChar(name));
-
-        roomName_ = new QLabel(name, this);
-        roomName_->setMargin(0);
-
-        topLayout_->addWidget(avatar_);
-        topLayout_->addWidget(roomName_, 1);
-
-        if (!res.info.avatar_url.empty())
-                avatar_->setImage(QString::fromStdString(res.info.avatar_url));
-}
-
-void
-RoomItem::updateItem(const RoomSearchResult &result)
-{
-        roomId_ = QString::fromStdString(std::move(result.room_id));
-
-        auto name =
-          QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)),
-                                           Qt::ElideRight,
-                                           parentWidget()->width() - 10);
-
-        roomName_->setText(name);
-
-        // if there is not an avatar set for the room, we want to at least show the letter
-        // correctly!
-        avatar_->setLetter(utils::firstChar(name));
-        if (!result.info.avatar_url.empty())
-                avatar_->setImage(QString::fromStdString(result.info.avatar_url));
-}
-
-void
-RoomItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() != Qt::RightButton)
-                emit clicked(selectedText());
-
-        QWidget::mousePressEvent(event);
-}
diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h
deleted file mode 100644
index fc24915e..00000000
--- a/src/popups/PopupItem.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include "../AvatarProvider.h"
-#include "../ChatPage.h"
-
-class Avatar;
-struct SearchResult;
-class QLabel;
-class QHBoxLayout;
-
-class PopupItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
-        Q_PROPERTY(bool hovering READ hovering WRITE setHovering)
-
-public:
-        PopupItem(QWidget *parent);
-
-        QString selectedText() const { return QString(); }
-        QColor hoverColor() const { return hoverColor_; }
-        void setHoverColor(QColor &color) { hoverColor_ = color; }
-
-        bool hovering() const { return hovering_; }
-        void setHovering(const bool hover) { hovering_ = hover; };
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-signals:
-        void clicked(const QString &text);
-
-protected:
-        QHBoxLayout *topLayout_;
-        Avatar *avatar_;
-        QColor hoverColor_;
-
-        //! Set if the item is currently being
-        //! hovered during tab completion (cycling).
-        bool hovering_;
-};
-
-class RoomItem : public PopupItem
-{
-        Q_OBJECT
-
-public:
-        RoomItem(QWidget *parent, const RoomSearchResult &res);
-        QString selectedText() const { return roomId_; }
-        void updateItem(const RoomSearchResult &res);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-
-private:
-        QLabel *roomName_;
-        QString roomId_;
-        RoomSearchResult info_;
-};
diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp
deleted file mode 100644
index 7b545d61..00000000
--- a/src/popups/SuggestionsPopup.cpp
+++ /dev/null
@@ -1,164 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-
-#include "../Config.h"
-#include "../Utils.h"
-#include "../ui/Avatar.h"
-#include "../ui/DropShadow.h"
-#include "ChatPage.h"
-#include "PopupItem.h"
-#include "SuggestionsPopup.h"
-
-SuggestionsPopup::SuggestionsPopup(QWidget *parent)
-  : QWidget(parent)
-{
-        setAttribute(Qt::WA_ShowWithoutActivating, true);
-        setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
-
-        layout_ = new QVBoxLayout(this);
-        layout_->setMargin(0);
-        layout_->setSpacing(0);
-}
-
-QString
-SuggestionsPopup::displayName(QString room, QString user)
-{
-        return cache::displayName(room, user);
-}
-
-void
-SuggestionsPopup::addRooms(const std::vector &rooms)
-{
-        if (rooms.empty()) {
-                hide();
-                return;
-        }
-
-        const int layoutCount = (int)layout_->count();
-        const int roomCount   = (int)rooms.size();
-
-        // Remove the extra widgets from the layout.
-        if (roomCount < layoutCount)
-                removeLayoutItemsAfter(roomCount - 1);
-
-        for (int i = 0; i < roomCount; ++i) {
-                auto item = layout_->itemAt(i);
-
-                // Create a new widget if there isn't already one in that
-                // layout position.
-                if (!item) {
-                        auto room = new RoomItem(this, rooms.at(i));
-                        connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected);
-                        layout_->addWidget(room);
-                } else {
-                        // Update the current widget with the new data.
-                        auto room = qobject_cast(item->widget());
-                        if (room)
-                                room->updateItem(rooms.at(i));
-                }
-        }
-
-        resetSelection();
-        adjustSize();
-
-        resize(geometry().width(), 40 * (int)rooms.size());
-
-        selectNextSuggestion();
-}
-
-void
-SuggestionsPopup::hoverSelection()
-{
-        resetHovering();
-        setHovering(selectedItem_);
-        update();
-}
-
-void
-SuggestionsPopup::selectHoveredSuggestion()
-{
-        const auto item = layout_->itemAt(selectedItem_);
-        if (!item)
-                return;
-
-        const auto &widget = qobject_cast(item->widget());
-        emit itemSelected(displayName(ChatPage::instance()->currentRoom(), widget->selectedText()));
-
-        resetSelection();
-}
-
-void
-SuggestionsPopup::selectNextSuggestion()
-{
-        selectedItem_++;
-        if (selectedItem_ >= layout_->count())
-                selectFirstItem();
-
-        hoverSelection();
-}
-
-void
-SuggestionsPopup::selectPreviousSuggestion()
-{
-        selectedItem_--;
-        if (selectedItem_ < 0)
-                selectLastItem();
-
-        hoverSelection();
-}
-
-void
-SuggestionsPopup::resetHovering()
-{
-        for (int i = 0; i < layout_->count(); ++i) {
-                const auto item = qobject_cast(layout_->itemAt(i)->widget());
-
-                if (item)
-                        item->setHovering(false);
-        }
-}
-
-void
-SuggestionsPopup::setHovering(int pos)
-{
-        const auto &item   = layout_->itemAt(pos);
-        const auto &widget = qobject_cast(item->widget());
-
-        if (widget)
-                widget->setHovering(true);
-}
-
-void
-SuggestionsPopup::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-SuggestionsPopup::selectLastItem()
-{
-        selectedItem_ = layout_->count() - 1;
-}
-
-void
-SuggestionsPopup::removeLayoutItemsAfter(size_t startingPos)
-{
-        size_t posToRemove = layout_->count() - 1;
-
-        QLayoutItem *item;
-        while (startingPos <= posToRemove &&
-               (item = layout_->takeAt((int)posToRemove)) != nullptr) {
-                delete item->widget();
-                delete item;
-
-                posToRemove = layout_->count() - 1;
-        }
-}
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
deleted file mode 100644
index 281edddb..00000000
--- a/src/popups/SuggestionsPopup.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include "CacheStructs.h"
-
-class QVBoxLayout;
-class QLayoutItem;
-
-class SuggestionsPopup : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit SuggestionsPopup(QWidget *parent = nullptr);
-
-        void selectHoveredSuggestion();
-
-public slots:
-        void addRooms(const std::vector &rooms);
-
-        //! Move to the next available suggestion item.
-        void selectNextSuggestion();
-        //! Move to the previous available suggestion item.
-        void selectPreviousSuggestion();
-        //! Remove hovering from all items.
-        void resetHovering();
-        //! Set hovering to the item in the given layout position.
-        void setHovering(int pos);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-signals:
-        void itemSelected(const QString &user);
-
-private:
-        QString displayName(QString roomid, QString userid);
-        void hoverSelection();
-        void resetSelection() { selectedItem_ = -1; }
-        void selectFirstItem() { selectedItem_ = 0; }
-        void selectLastItem();
-        void removeLayoutItemsAfter(size_t startingPos);
-
-        QVBoxLayout *layout_;
-
-        //! Counter for tab completion (cycling).
-        int selectedItem_ = -1;
-};
diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp
deleted file mode 100644
index 56b57503..00000000
--- a/src/popups/UserMentions.cpp
+++ /dev/null
@@ -1,178 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "EventAccessors.h"
-#include "Logging.h"
-#include "UserMentions.h"
-
-using namespace popups;
-
-UserMentions::UserMentions(QWidget *parent)
-  : QWidget{parent}
-{
-        setAttribute(Qt::WA_ShowWithoutActivating, true);
-        setWindowFlags(Qt::FramelessWindowHint | Qt::Popup);
-
-        tab_layout_ = new QTabWidget(this);
-
-        top_layout_ = new QVBoxLayout(this);
-        top_layout_->setSpacing(0);
-        top_layout_->setMargin(0);
-
-        local_scroll_area_ = new QScrollArea(this);
-        local_scroll_area_->setWidgetResizable(true);
-        local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        local_scroll_widget_ = new QWidget(this);
-        local_scroll_widget_->setObjectName("local_scroll_widget");
-
-        all_scroll_area_ = new QScrollArea(this);
-        all_scroll_area_->setWidgetResizable(true);
-        all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        all_scroll_widget_ = new QWidget(this);
-        all_scroll_widget_->setObjectName("all_scroll_widget");
-
-        // Height of the typing display.
-        QFont f;
-        f.setPointSizeF(f.pointSizeF() * 0.9);
-        const int bottomMargin = QFontMetrics(f).height() + 6;
-
-        local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_);
-        local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        local_scroll_layout_->setSpacing(0);
-        local_scroll_layout_->setObjectName("localscrollarea");
-
-        all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_);
-        all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        all_scroll_layout_->setSpacing(0);
-        all_scroll_layout_->setObjectName("allscrollarea");
-
-        local_scroll_area_->setWidget(local_scroll_widget_);
-        local_scroll_area_->setAlignment(Qt::AlignBottom);
-
-        all_scroll_area_->setWidget(all_scroll_widget_);
-        all_scroll_area_->setAlignment(Qt::AlignBottom);
-
-        tab_layout_->addTab(local_scroll_area_, tr("This Room"));
-        tab_layout_->addTab(all_scroll_area_, tr("All Rooms"));
-        top_layout_->addWidget(tab_layout_);
-
-        setLayout(top_layout_);
-}
-
-void
-UserMentions::initializeMentions(const QMap ¬ifs)
-{
-        nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
-
-        for (const auto &item : notifs) {
-                for (const auto ¬if : item.notifications) {
-                        const auto event_id =
-                          QString::fromStdString(mtx::accessors::event_id(notif.event));
-
-                        try {
-                                const auto room_id = QString::fromStdString(notif.room_id);
-                                const auto user_id =
-                                  QString::fromStdString(mtx::accessors::sender(notif.event));
-                                const auto body =
-                                  QString::fromStdString(mtx::accessors::body(notif.event));
-
-                                pushItem(event_id,
-                                         user_id,
-                                         body,
-                                         room_id,
-                                         ChatPage::instance()->currentRoom());
-
-                        } catch (const lmdb::error &e) {
-                                nhlog::db()->warn("error while sending desktop notification: {}",
-                                                  e.what());
-                        }
-                }
-        }
-}
-
-void
-UserMentions::showPopup()
-{
-        for (auto widget : all_scroll_layout_->findChildren()) {
-                delete widget;
-        }
-        for (auto widget : local_scroll_layout_->findChildren()) {
-                delete widget;
-        }
-
-        auto notifs = cache::getTimelineMentions();
-
-        initializeMentions(notifs);
-        show();
-}
-
-void
-UserMentions::pushItem(const QString &event_id,
-                       const QString &user_id,
-                       const QString &body,
-                       const QString &room_id,
-                       const QString ¤t_room_id)
-{
-        (void)event_id;
-        (void)user_id;
-        (void)body;
-        (void)room_id;
-        (void)current_room_id;
-        //        setUpdatesEnabled(false);
-        //
-        //        // Add to the 'all' section
-        //        TimelineItem *view_item = new TimelineItem(
-        //          mtx::events::MessageType::Text, user_id, body, true, room_id,
-        //          all_scroll_widget_);
-        //        view_item->setEventId(event_id);
-        //        view_item->hide();
-        //
-        //        all_scroll_layout_->addWidget(view_item);
-        //        QTimer::singleShot(0, this, [view_item, this]() {
-        //                view_item->show();
-        //                view_item->adjustSize();
-        //                setUpdatesEnabled(true);
-        //        });
-        //
-        //        // if it matches the current room... add it to the current room as well.
-        //        if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
-        //                // Add to the 'local' section
-        //                TimelineItem *local_view_item = new
-        //                TimelineItem(mtx::events::MessageType::Text,
-        //                                                                 user_id,
-        //                                                                 body,
-        //                                                                 true,
-        //                                                                 room_id,
-        //                                                                 local_scroll_widget_);
-        //                local_view_item->setEventId(event_id);
-        //                local_view_item->hide();
-        //                local_scroll_layout_->addWidget(local_view_item);
-        //
-        //                QTimer::singleShot(0, this, [local_view_item]() {
-        //                        local_view_item->show();
-        //                        local_view_item->adjustSize();
-        //                });
-        //        }
-}
-
-void
-UserMentions::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h
deleted file mode 100644
index f0b662d8..00000000
--- a/src/popups/UserMentions.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-#include 
-
-class QPaintEvent;
-class QTabWidget;
-class QScrollArea;
-class QVBoxLayout;
-
-namespace popups {
-
-class UserMentions : public QWidget
-{
-        Q_OBJECT
-public:
-        UserMentions(QWidget *parent = nullptr);
-
-        void initializeMentions(const QMap ¬ifs);
-        void showPopup();
-
-protected:
-        void paintEvent(QPaintEvent *) override;
-
-private:
-        void pushItem(const QString &event_id,
-                      const QString &user_id,
-                      const QString &body,
-                      const QString &room_id,
-                      const QString ¤t_room_id);
-        QTabWidget *tab_layout_;
-        QVBoxLayout *top_layout_;
-        QVBoxLayout *local_scroll_layout_;
-        QVBoxLayout *all_scroll_layout_;
-
-        QScrollArea *local_scroll_area_;
-        QWidget *local_scroll_widget_;
-
-        QScrollArea *all_scroll_area_;
-        QWidget *all_scroll_widget_;
-};
-}
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
new file mode 100644
index 00000000..96a450ea
--- /dev/null
+++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,188 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "CommunitiesModel.h"
+
+#include 
+
+#include "Cache.h"
+#include "UserSettingsPage.h"
+
+CommunitiesModel::CommunitiesModel(QObject *parent)
+  : QAbstractListModel(parent)
+{}
+
+QHash
+CommunitiesModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {DisplayName, "displayName"},
+          {Tooltip, "tooltip"},
+          {ChildrenHidden, "childrenHidden"},
+          {Hidden, "hidden"},
+          {Id, "id"},
+        };
+}
+
+QVariant
+CommunitiesModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() == 0) {
+                switch (role) {
+                case CommunitiesModel::Roles::AvatarUrl:
+                        return QString(":/icons/icons/ui/world.png");
+                case CommunitiesModel::Roles::DisplayName:
+                        return tr("All rooms");
+                case CommunitiesModel::Roles::Tooltip:
+                        return tr("Shows all rooms without filtering.");
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return false;
+                case CommunitiesModel::Roles::Hidden:
+                        return false;
+                case CommunitiesModel::Roles::Id:
+                        return "";
+                }
+        } else if (index.row() - 1 < tags_.size()) {
+                auto tag = tags_.at(index.row() - 1);
+                if (tag == "m.favourite") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/star.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Favourites");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Rooms you have favourited.");
+                        }
+                } else if (tag == "m.lowpriority") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/star.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Low Priority");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Rooms with low priority.");
+                        }
+                } else if (tag == "m.server_notice") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/tag.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Server Notices");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Messages from your server or administrator.");
+                        }
+                } else {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/tag.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tag.mid(2);
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tag.mid(2);
+                        }
+                }
+
+                switch (role) {
+                case CommunitiesModel::Roles::Hidden:
+                        return hiddentTagIds_.contains("tag:" + tag);
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return true;
+                case CommunitiesModel::Roles::Id:
+                        return "tag:" + tag;
+                }
+        }
+        return QVariant();
+}
+
+void
+CommunitiesModel::initializeSidebar()
+{
+        std::set ts;
+        for (const auto &e : cache::roomInfo()) {
+                for (const auto &t : e.tags) {
+                        if (t.find("u.") == 0 || t.find("m." == 0)) {
+                                ts.insert(t);
+                        }
+                }
+        }
+
+        beginResetModel();
+        tags_.clear();
+        for (const auto &t : ts)
+                tags_.push_back(QString::fromStdString(t));
+
+        hiddentTagIds_ = UserSettings::instance()->hiddenTags();
+        endResetModel();
+
+        emit tagsChanged();
+        emit hiddenTagsChanged();
+}
+
+void
+CommunitiesModel::clear()
+{
+        beginResetModel();
+        tags_.clear();
+        endResetModel();
+        resetCurrentTagId();
+
+        emit tagsChanged();
+}
+
+void
+CommunitiesModel::sync(const mtx::responses::Rooms &rooms)
+{
+        bool tagsUpdated = false;
+
+        for (const auto &[roomid, room] : rooms.join) {
+                (void)roomid;
+                for (const auto &e : room.account_data.events)
+                        if (std::holds_alternative<
+                              mtx::events::AccountDataEvent>(e)) {
+                                tagsUpdated = true;
+                        }
+        }
+
+        if (tagsUpdated)
+                initializeSidebar();
+}
+
+void
+CommunitiesModel::setCurrentTagId(QString tagId)
+{
+        if (tagId.startsWith("tag:")) {
+                auto tag = tagId.mid(4);
+                for (const auto &t : tags_) {
+                        if (t == tag) {
+                                this->currentTagId_ = tagId;
+                                emit currentTagIdChanged(currentTagId_);
+                                return;
+                        }
+                }
+        }
+
+        this->currentTagId_ = "";
+        emit currentTagIdChanged(currentTagId_);
+}
+
+void
+CommunitiesModel::toggleTagId(QString tagId)
+{
+        if (hiddentTagIds_.contains(tagId)) {
+                hiddentTagIds_.removeOne(tagId);
+                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+        } else {
+                hiddentTagIds_.push_back(tagId);
+                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+        }
+
+        if (tagId.startsWith("tag:")) {
+                auto idx = tags_.indexOf(tagId.mid(4));
+                if (idx != -1)
+                        emit dataChanged(index(idx), index(idx), {Hidden});
+        }
+
+        emit hiddenTagsChanged();
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
new file mode 100644
index 00000000..c98b5955
--- /dev/null
+++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+class CommunitiesModel : public QAbstractListModel
+{
+        Q_OBJECT
+        Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY
+                     currentTagIdChanged RESET resetCurrentTagId)
+        Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged)
+
+public:
+        enum Roles
+        {
+                AvatarUrl = Qt::UserRole,
+                DisplayName,
+                Tooltip,
+                ChildrenHidden,
+                Hidden,
+                Id,
+        };
+
+        CommunitiesModel(QObject *parent = nullptr);
+        QHash roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override
+        {
+                (void)parent;
+                return 1 + tags_.size();
+        }
+        QVariant data(const QModelIndex &index, int role) const override;
+
+public slots:
+        void initializeSidebar();
+        void sync(const mtx::responses::Rooms &rooms);
+        void clear();
+        QString currentTagId() const { return currentTagId_; }
+        void setCurrentTagId(QString tagId);
+        void resetCurrentTagId()
+        {
+                currentTagId_.clear();
+                emit currentTagIdChanged(currentTagId_);
+        }
+        QStringList tags() const { return tags_; }
+        void toggleTagId(QString tagId);
+
+signals:
+        void currentTagIdChanged(QString tagId);
+        void hiddenTagsChanged();
+        void tagsChanged();
+
+private:
+        QStringList tags_;
+        QString currentTagId_;
+        QStringList hiddentTagIds_;
+};
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 883d384c..4a9f0fff 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -770,7 +770,7 @@ EventStore::decryptEvent(const IdIndex &idx,
 }
 
 mtx::events::collections::TimelineEvents *
-EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits)
+EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits)
 {
         if (this->thread() != QThread::currentThread())
                 nhlog::db()->warn("{} called from a different thread!", __func__);
@@ -778,7 +778,7 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt,
         if (id.empty())
                 return nullptr;
 
-        IdIndex index{room_id_, std::string(id)};
+        IdIndex index{room_id_, std::move(id)};
         if (resolve_edits) {
                 auto edits_ = edits(index.id);
                 if (!edits_.empty()) {
@@ -796,14 +796,12 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt,
                         http::client()->get_event(
                           room_id_,
                           index.id,
-                          [this,
-                           relatedTo = std::string(related_to.data(), related_to.size()),
-                           id = index.id](const mtx::events::collections::TimelineEvents &timeline,
-                                          mtx::http::RequestErr err) {
+                          [this, relatedTo = std::string(related_to), id = index.id](
+                            const mtx::events::collections::TimelineEvents &timeline,
+                            mtx::http::RequestErr err) {
                                   if (err) {
                                           nhlog::net()->error(
-                                            "Failed to retrieve event with id {}, which "
-                                            "was "
+                                            "Failed to retrieve event with id {}, which was "
                                             "requested to show the replyTo for event {}",
                                             relatedTo,
                                             id);
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index c7a7588b..d9bb86cb 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -70,7 +70,7 @@ public:
 
         // optionally returns the event or nullptr and fetches it, after which it emits a
         // relatedFetched event
-        mtx::events::collections::TimelineEvents *get(std::string_view id,
+        mtx::events::collections::TimelineEvents *get(std::string id,
                                                       std::string_view related_to,
                                                       bool decrypt       = true,
                                                       bool resolve_edits = true);
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..c309daab 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -20,6 +20,7 @@
 #include "Cache.h"
 #include "ChatPage.h"
 #include "CompletionProxyModel.h"
+#include "Config.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
@@ -508,8 +509,7 @@ InputBar::command(QString command, QString args)
         } else if (command == "react") {
                 auto eventId = room->reply();
                 if (!eventId.isEmpty())
-                        ChatPage::instance()->timelineManager()->queueReactionMessage(
-                          eventId, args.trimmed());
+                        reaction(eventId, args.trimmed());
         } else if (command == "join") {
                 ChatPage::instance()->joinRoom(args);
         } else if (command == "part" || command == "leave") {
@@ -715,3 +715,35 @@ InputBar::stopTyping()
                 }
         });
 }
+
+void
+InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
+{
+        auto reactions = room->reactions(reactedEvent.toStdString());
+
+        QString selfReactedEvent;
+        for (const auto &reaction : reactions) {
+                if (reactionKey == reaction.key_) {
+                        selfReactedEvent = reaction.selfReactedEvent_;
+                        break;
+                }
+        }
+
+        if (selfReactedEvent.startsWith("m"))
+                return;
+
+        // If selfReactedEvent is empty, that means we haven't previously reacted
+        if (selfReactedEvent.isEmpty()) {
+                mtx::events::msg::Reaction reaction;
+                mtx::common::Relation rel;
+                rel.rel_type = mtx::common::RelationType::Annotation;
+                rel.event_id = reactedEvent.toStdString();
+                rel.key      = reactionKey.toStdString();
+                reaction.relations.relations.push_back(rel);
+
+                room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
+                // Otherwise, we have previously reacted and the reaction should be redacted
+        } else {
+                room->redactEvent(selfReactedEvent);
+        }
+}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots:
         void message(QString body,
                      MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
                      bool rainbowify              = false);
+        void reaction(const QString &reactedEvent, const QString &reactionKey);
 
 private slots:
         void startTyping();
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
new file mode 100644
index 00000000..0f980c6c
--- /dev/null
+++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,590 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomlistModel.h"
+
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "MatrixClient.h"
+#include "MxcImageProvider.h"
+#include "TimelineModel.h"
+#include "TimelineViewManager.h"
+#include "UserSettingsPage.h"
+
+RoomlistModel::RoomlistModel(TimelineViewManager *parent)
+  : QAbstractListModel(parent)
+  , manager(parent)
+{
+        connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
+                auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
+                QHash>::iterator i;
+                for (i = models.begin(); i != models.end(); ++i) {
+                        auto ptr = i.value();
+
+                        if (!ptr.isNull()) {
+                                ptr->setDecryptDescription(decrypt);
+                                ptr->updateLastMessage();
+                        }
+                }
+        });
+
+        connect(this,
+                &RoomlistModel::totalUnreadMessageCountUpdated,
+                ChatPage::instance(),
+                &ChatPage::unreadMessages);
+}
+
+QHash
+RoomlistModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {RoomName, "roomName"},
+          {RoomId, "roomId"},
+          {LastMessage, "lastMessage"},
+          {Time, "time"},
+          {Timestamp, "timestamp"},
+          {HasUnreadMessages, "hasUnreadMessages"},
+          {HasLoudNotification, "hasLoudNotification"},
+          {NotificationCount, "notificationCount"},
+          {IsInvite, "isInvite"},
+          {IsSpace, "isSpace"},
+          {Tags, "tags"},
+        };
+}
+
+QVariant
+RoomlistModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) {
+                auto roomid = roomids.at(index.row());
+
+                if (models.contains(roomid)) {
+                        auto room = models.value(roomid);
+                        switch (role) {
+                        case Roles::AvatarUrl:
+                                return room->roomAvatarUrl();
+                        case Roles::RoomName:
+                                return room->plainRoomName();
+                        case Roles::RoomId:
+                                return room->roomId();
+                        case Roles::LastMessage:
+                                return room->lastMessage().body;
+                        case Roles::Time:
+                                return room->lastMessage().descriptiveTime;
+                        case Roles::Timestamp:
+                                return QVariant(
+                                  static_cast(room->lastMessage().timestamp));
+                        case Roles::HasUnreadMessages:
+                                return this->roomReadStatus.count(roomid) &&
+                                       this->roomReadStatus.at(roomid);
+                        case Roles::HasLoudNotification:
+                                return room->hasMentions();
+                        case Roles::NotificationCount:
+                                return room->notificationCount();
+                        case Roles::IsInvite:
+                        case Roles::IsSpace:
+                                return false;
+                        case Roles::Tags: {
+                                auto info = cache::singleRoomInfo(roomid.toStdString());
+                                QStringList list;
+                                for (const auto &t : info.tags)
+                                        list.push_back(QString::fromStdString(t));
+                                return list;
+                        }
+                        default:
+                                return {};
+                        }
+                } else if (invites.contains(roomid)) {
+                        auto room = invites.value(roomid);
+                        switch (role) {
+                        case Roles::AvatarUrl:
+                                return QString::fromStdString(room.avatar_url);
+                        case Roles::RoomName:
+                                return QString::fromStdString(room.name);
+                        case Roles::RoomId:
+                                return roomid;
+                        case Roles::LastMessage:
+                                return room.msgInfo.body;
+                        case Roles::Time:
+                                return room.msgInfo.descriptiveTime;
+                        case Roles::Timestamp:
+                                return QVariant(static_cast(room.msgInfo.timestamp));
+                        case Roles::HasUnreadMessages:
+                        case Roles::HasLoudNotification:
+                                return false;
+                        case Roles::NotificationCount:
+                                return 0;
+                        case Roles::IsInvite:
+                                return true;
+                        case Roles::IsSpace:
+                                return false;
+                        case Roles::Tags:
+                                return QStringList();
+                        default:
+                                return {};
+                        }
+                } else {
+                        return {};
+                }
+        } else {
+                return {};
+        }
+}
+
+void
+RoomlistModel::updateReadStatus(const std::map roomReadStatus_)
+{
+        std::vector roomsToUpdate;
+        roomsToUpdate.resize(roomReadStatus_.size());
+        for (const auto &[roomid, roomUnread] : roomReadStatus_) {
+                if (roomUnread != roomReadStatus[roomid]) {
+                        roomsToUpdate.push_back(this->roomidToIndex(roomid));
+                }
+
+                this->roomReadStatus[roomid] = roomUnread;
+        }
+
+        for (auto idx : roomsToUpdate) {
+                emit dataChanged(index(idx),
+                                 index(idx),
+                                 {
+                                   Roles::HasUnreadMessages,
+                                 });
+        }
+}
+void
+RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
+{
+        if (!models.contains(room_id)) {
+                // ensure we get read status updates and are only connected once
+                connect(cache::client(),
+                        &Cache::roomReadStatus,
+                        this,
+                        &RoomlistModel::updateReadStatus,
+                        Qt::UniqueConnection);
+
+                QSharedPointer newRoom(new TimelineModel(manager, room_id));
+                newRoom->setDecryptDescription(
+                  ChatPage::instance()->userSettings()->decryptSidebar());
+
+                connect(newRoom.data(),
+                        &TimelineModel::newEncryptedImage,
+                        manager->imageProvider(),
+                        &MxcImageProvider::addEncryptionInfo);
+                connect(newRoom.data(),
+                        &TimelineModel::forwardToRoom,
+                        manager,
+                        &TimelineViewManager::forwardMessageToRoom);
+                connect(
+                  newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() {
+                          auto idx = this->roomidToIndex(room_id);
+                          emit dataChanged(index(idx),
+                                           index(idx),
+                                           {
+                                             Roles::HasLoudNotification,
+                                             Roles::LastMessage,
+                                             Roles::Timestamp,
+                                             Roles::NotificationCount,
+                                             Qt::DisplayRole,
+                                           });
+                  });
+                connect(
+                  newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() {
+                          auto idx = this->roomidToIndex(room_id);
+                          emit dataChanged(index(idx),
+                                           index(idx),
+                                           {
+                                             Roles::AvatarUrl,
+                                           });
+                  });
+                connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() {
+                        auto idx = this->roomidToIndex(room_id);
+                        emit dataChanged(index(idx),
+                                         index(idx),
+                                         {
+                                           Roles::RoomName,
+                                         });
+                });
+                connect(
+                  newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() {
+                          auto idx = this->roomidToIndex(room_id);
+                          emit dataChanged(index(idx),
+                                           index(idx),
+                                           {
+                                             Roles::HasLoudNotification,
+                                             Roles::NotificationCount,
+                                             Qt::DisplayRole,
+                                           });
+
+                          int total_unread_msgs = 0;
+
+                          for (const auto &room : models) {
+                                  if (!room.isNull())
+                                          total_unread_msgs += room->notificationCount();
+                          }
+
+                          emit totalUnreadMessageCountUpdated(total_unread_msgs);
+                  });
+
+                newRoom->updateLastMessage();
+
+                bool wasInvite = invites.contains(room_id);
+                if (!suppressInsertNotification && !wasInvite)
+                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+
+                models.insert(room_id, std::move(newRoom));
+
+                if (wasInvite) {
+                        auto idx = roomidToIndex(room_id);
+                        invites.remove(room_id);
+                        emit dataChanged(index(idx), index(idx));
+                } else {
+                        roomids.push_back(room_id);
+                }
+
+                if (!suppressInsertNotification && !wasInvite)
+                        endInsertRows();
+        }
+}
+
+void
+RoomlistModel::sync(const mtx::responses::Rooms &rooms)
+{
+        for (const auto &[room_id, room] : rooms.join) {
+                // addRoom will only add the room, if it doesn't exist
+                addRoom(QString::fromStdString(room_id));
+                const auto &room_model = models.value(QString::fromStdString(room_id));
+                room_model->sync(room);
+                // room_model->addEvents(room.timeline);
+                connect(room_model.data(),
+                        &TimelineModel::newCallEvent,
+                        manager->callManager(),
+                        &CallManager::syncEvent,
+                        Qt::UniqueConnection);
+
+                if (ChatPage::instance()->userSettings()->typingNotifications()) {
+                        for (const auto &ev : room.ephemeral.events) {
+                                if (auto t = std::get_if<
+                                      mtx::events::EphemeralEvent>(
+                                      &ev)) {
+                                        std::vector typing;
+                                        typing.reserve(t->content.user_ids.size());
+                                        for (const auto &user : t->content.user_ids) {
+                                                if (user != http::client()->user_id().to_string())
+                                                        typing.push_back(
+                                                          QString::fromStdString(user));
+                                        }
+                                        room_model->updateTypingUsers(typing);
+                                }
+                        }
+                }
+        }
+
+        for (const auto &[room_id, room] : rooms.leave) {
+                (void)room;
+                auto idx = this->roomidToIndex(QString::fromStdString(room_id));
+                if (idx != -1) {
+                        beginRemoveRows(QModelIndex(), idx, idx);
+                        roomids.erase(roomids.begin() + idx);
+                        if (models.contains(QString::fromStdString(room_id)))
+                                models.remove(QString::fromStdString(room_id));
+                        else if (invites.contains(QString::fromStdString(room_id)))
+                                invites.remove(QString::fromStdString(room_id));
+                        endRemoveRows();
+                }
+        }
+
+        for (const auto &[room_id, room] : rooms.invite) {
+                (void)room;
+                auto qroomid = QString::fromStdString(room_id);
+
+                auto invite = cache::client()->invite(room_id);
+                if (!invite)
+                        continue;
+
+                if (invites.contains(qroomid)) {
+                        invites[qroomid] = *invite;
+                        auto idx         = roomidToIndex(qroomid);
+                        emit dataChanged(index(idx), index(idx));
+                } else {
+                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+                        invites.insert(qroomid, *invite);
+                        roomids.push_back(std::move(qroomid));
+                        endInsertRows();
+                }
+        }
+}
+
+void
+RoomlistModel::initializeRooms()
+{
+        beginResetModel();
+        models.clear();
+        roomids.clear();
+        invites.clear();
+        currentRoom_ = nullptr;
+
+        invites = cache::client()->invites();
+        for (const auto &id : invites.keys())
+                roomids.push_back(id);
+
+        for (const auto &id : cache::client()->roomIds())
+                addRoom(id, true);
+
+        endResetModel();
+}
+
+void
+RoomlistModel::clear()
+{
+        beginResetModel();
+        models.clear();
+        invites.clear();
+        roomids.clear();
+        currentRoom_ = nullptr;
+        emit currentRoomChanged();
+        endResetModel();
+}
+
+void
+RoomlistModel::acceptInvite(QString roomid)
+{
+        if (invites.contains(roomid)) {
+                auto idx = roomidToIndex(roomid);
+
+                if (idx != -1) {
+                        beginRemoveRows(QModelIndex(), idx, idx);
+                        roomids.erase(roomids.begin() + idx);
+                        invites.remove(roomid);
+                        endRemoveRows();
+                        ChatPage::instance()->joinRoom(roomid);
+                }
+        }
+}
+void
+RoomlistModel::declineInvite(QString roomid)
+{
+        if (invites.contains(roomid)) {
+                auto idx = roomidToIndex(roomid);
+
+                if (idx != -1) {
+                        beginRemoveRows(QModelIndex(), idx, idx);
+                        roomids.erase(roomids.begin() + idx);
+                        invites.remove(roomid);
+                        endRemoveRows();
+                        ChatPage::instance()->leaveRoom(roomid);
+                }
+        }
+}
+void
+RoomlistModel::leave(QString roomid)
+{
+        if (models.contains(roomid)) {
+                auto idx = roomidToIndex(roomid);
+
+                if (idx != -1) {
+                        beginRemoveRows(QModelIndex(), idx, idx);
+                        roomids.erase(roomids.begin() + idx);
+                        models.remove(roomid);
+                        endRemoveRows();
+                        ChatPage::instance()->leaveRoom(roomid);
+                }
+        }
+}
+
+void
+RoomlistModel::setCurrentRoom(QString roomid)
+{
+        nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
+        if (models.contains(roomid)) {
+                currentRoom_ = models.value(roomid);
+                emit currentRoomChanged();
+                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
+        }
+}
+
+namespace {
+enum NotificationImportance : short
+{
+        ImportanceDisabled = -1,
+        AllEventsRead      = 0,
+        NewMessage         = 1,
+        NewMentions        = 2,
+        Invite             = 3
+};
+}
+
+short int
+FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const
+{
+        // Returns the degree of importance of the unread messages in the room.
+        // If sorting by importance is disabled in settings, this only ever
+        // returns ImportanceDisabled or Invite
+        if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
+                return Invite;
+        } else if (!this->sortByImportance) {
+                return ImportanceDisabled;
+        } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) {
+                return NewMentions;
+        } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) {
+                return NewMessage;
+        } else {
+                return AllEventsRead;
+        }
+}
+bool
+FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+        QModelIndex const left_idx  = sourceModel()->index(left.row(), 0, QModelIndex());
+        QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex());
+
+        // Sort by "importance" (i.e. invites before mentions before
+        // notifs before new events before old events), then secondly
+        // by recency.
+
+        // Checking importance first
+        const auto a_importance = calculateImportance(left_idx);
+        const auto b_importance = calculateImportance(right_idx);
+        if (a_importance != b_importance) {
+                return a_importance > b_importance;
+        }
+
+        // Now sort by recency
+        // Zero if empty, otherwise the time that the event occured
+        uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong();
+        uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong();
+
+        if (a_recency != b_recency)
+                return a_recency > b_recency;
+        else
+                return left.row() < right.row();
+}
+
+FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
+  : QSortFilterProxyModel(parent)
+  , roomlistmodel(model)
+{
+        this->sortByImportance = UserSettings::instance()->sortByImportance();
+        setSourceModel(model);
+        setDynamicSortFilter(true);
+
+        QObject::connect(UserSettings::instance().get(),
+                         &UserSettings::roomSortingChanged,
+                         this,
+                         [this](bool sortByImportance_) {
+                                 this->sortByImportance = sortByImportance_;
+                                 invalidate();
+                         });
+
+        connect(roomlistmodel,
+                &RoomlistModel::currentRoomChanged,
+                this,
+                &FilteredRoomlistModel::currentRoomChanged);
+
+        sort(0);
+}
+
+void
+FilteredRoomlistModel::updateHiddenTagsAndSpaces()
+{
+        hiddenTags.clear();
+        hiddenSpaces.clear();
+        for (const auto &t : UserSettings::instance()->hiddenTags()) {
+                if (t.startsWith("tag:"))
+                        hiddenTags.push_back(t.mid(4));
+                else if (t.startsWith("space:"))
+                        hiddenSpaces.push_back(t.mid(6));
+        }
+
+        invalidateFilter();
+}
+
+bool
+FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
+{
+        if (filterType == FilterBy::Nothing) {
+                if (!hiddenTags.empty()) {
+                        auto tags =
+                          sourceModel()
+                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                            .toStringList();
+
+                        for (const auto &t : tags)
+                                if (hiddenTags.contains(t))
+                                        return false;
+                }
+
+                return true;
+        } else if (filterType == FilterBy::Tag) {
+                auto tags = sourceModel()
+                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                              .toStringList();
+
+                if (!tags.contains(filterStr))
+                        return false;
+                else if (!hiddenTags.empty()) {
+                        for (const auto &t : tags)
+                                if (t != filterStr && hiddenTags.contains(t))
+                                        return false;
+                }
+                return true;
+        } else {
+                return true;
+        }
+}
+
+void
+FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
+{
+        if (on) {
+                http::client()->put_tag(
+                  roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::ui()->error("Failed to add tag: {}, {}",
+                                                     tag.toStdString(),
+                                                     err->matrix_error.error);
+                          }
+                  });
+        } else {
+                http::client()->delete_tag(
+                  roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::ui()->error("Failed to delete tag: {}, {}",
+                                                     tag.toStdString(),
+                                                     err->matrix_error.error);
+                          }
+                  });
+        }
+}
+
+void
+FilteredRoomlistModel::nextRoom()
+{
+        auto r = currentRoom();
+
+        if (r) {
+                int idx = roomidToIndex(r->roomId());
+                idx++;
+                if (idx < rowCount()) {
+                        setCurrentRoom(
+                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
+                }
+        }
+}
+
+void
+FilteredRoomlistModel::previousRoom()
+{
+        auto r = currentRoom();
+
+        if (r) {
+                int idx = roomidToIndex(r->roomId());
+                idx--;
+                if (idx > 0) {
+                        setCurrentRoom(
+                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
+                }
+        }
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
new file mode 100644
index 00000000..b0244886
--- /dev/null
+++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include "TimelineModel.h"
+
+class TimelineViewManager;
+
+class RoomlistModel : public QAbstractListModel
+{
+        Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET
+                     resetCurrentRoom)
+public:
+        enum Roles
+        {
+                AvatarUrl = Qt::UserRole,
+                RoomName,
+                RoomId,
+                LastMessage,
+                Time,
+                Timestamp,
+                HasUnreadMessages,
+                HasLoudNotification,
+                NotificationCount,
+                IsInvite,
+                IsSpace,
+                Tags,
+        };
+
+        RoomlistModel(TimelineViewManager *parent = nullptr);
+        QHash roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override
+        {
+                (void)parent;
+                return (int)roomids.size();
+        }
+        QVariant data(const QModelIndex &index, int role) const override;
+        QSharedPointer getRoomById(QString id) const
+        {
+                if (models.contains(id))
+                        return models.value(id);
+                else
+                        return {};
+        }
+
+public slots:
+        void initializeRooms();
+        void sync(const mtx::responses::Rooms &rooms);
+        void clear();
+        int roomidToIndex(QString roomid)
+        {
+                for (int i = 0; i < (int)roomids.size(); i++) {
+                        if (roomids[i] == roomid)
+                                return i;
+                }
+
+                return -1;
+        }
+        void acceptInvite(QString roomid);
+        void declineInvite(QString roomid);
+        void leave(QString roomid);
+        TimelineModel *currentRoom() const { return currentRoom_.get(); }
+        void setCurrentRoom(QString roomid);
+        void resetCurrentRoom()
+        {
+                currentRoom_ = nullptr;
+                emit currentRoomChanged();
+        }
+
+private slots:
+        void updateReadStatus(const std::map roomReadStatus_);
+
+signals:
+        void totalUnreadMessageCountUpdated(int unreadMessages);
+        void currentRoomChanged();
+
+private:
+        void addRoom(const QString &room_id, bool suppressInsertNotification = false);
+
+        TimelineViewManager *manager = nullptr;
+        std::vector roomids;
+        QHash invites;
+        QHash> models;
+        std::map roomReadStatus;
+
+        QSharedPointer currentRoom_;
+
+        friend class FilteredRoomlistModel;
+};
+
+class FilteredRoomlistModel : public QSortFilterProxyModel
+{
+        Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET
+                     resetCurrentRoom)
+public:
+        FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
+        bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+        bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
+
+public slots:
+        int roomidToIndex(QString roomid)
+        {
+                return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid)))
+                  .row();
+        }
+        void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
+        void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
+        void leave(QString roomid) { roomlistmodel->leave(roomid); }
+        void toggleTag(QString roomid, QString tag, bool on);
+
+        TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
+        void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+        void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); }
+
+        void nextRoom();
+        void previousRoom();
+
+        void updateFilterTag(QString tagId)
+        {
+                if (tagId.startsWith("tag:")) {
+                        filterType = FilterBy::Tag;
+                        filterStr  = tagId.mid(4);
+                } else {
+                        filterType = FilterBy::Nothing;
+                        filterStr.clear();
+                }
+
+                invalidateFilter();
+        }
+
+        void updateHiddenTagsAndSpaces();
+
+signals:
+        void currentRoomChanged();
+
+private:
+        short int calculateImportance(const QModelIndex &idx) const;
+        RoomlistModel *roomlistmodel;
+        bool sortByImportance = true;
+
+        enum class FilterBy
+        {
+                Tag,
+                Space,
+                Nothing,
+        };
+        QString filterStr   = "";
+        FilterBy filterType = FilterBy::Nothing;
+        QStringList hiddenTags, hiddenSpaces;
+};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8df17457..f29f929e 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
   , room_id_(room_id)
   , manager_(manager)
 {
+        lastMessage_.timestamp = 0;
+
         connect(
           this,
           &TimelineModel::redactionFailed,
@@ -572,7 +574,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                                 !event_id(event).empty() && event_id(event).front() == '$');
         case IsEncrypted: {
                 auto id              = event_id(event);
-                auto encrypted_event = events.get(id, id, false);
+                auto encrypted_event = events.get(id, "", false);
                 return encrypted_event &&
                        std::holds_alternative<
                          mtx::events::EncryptedEvent>(
@@ -581,7 +583,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
 
         case Trustlevel: {
                 auto id              = event_id(event);
-                auto encrypted_event = events.get(id, id, false);
+                auto encrypted_event = events.get(id, "", false);
                 if (encrypted_event) {
                         if (auto encrypted =
                               std::get_if>(
@@ -723,6 +725,20 @@ TimelineModel::fetchMore(const QModelIndex &)
         events.fetchMore();
 }
 
+void
+TimelineModel::sync(const mtx::responses::JoinedRoom &room)
+{
+        this->syncState(room.state);
+        this->addEvents(room.timeline);
+
+        if (room.unread_notifications.highlight_count != highlight_count ||
+            room.unread_notifications.notification_count != notification_count) {
+                notification_count = room.unread_notifications.notification_count;
+                highlight_count    = room.unread_notifications.highlight_count;
+                emit notificationsChanged();
+        }
+}
+
 void
 TimelineModel::syncState(const mtx::responses::State &s)
 {
@@ -866,14 +882,17 @@ TimelineModel::updateLastMessage()
                 if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
                         auto time   = mtx::accessors::origin_server_ts(*event);
                         uint64_t ts = time.toMSecsSinceEpoch();
-                        emit manager_->updateRoomsLastMessage(
-                          room_id_,
+                        auto description =
                           DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
                                    QString::fromStdString(http::client()->user_id().to_string()),
                                    tr("You joined this room."),
                                    utils::descriptiveTime(time),
                                    ts,
-                                   time});
+                                   time};
+                        if (description != lastMessage_) {
+                                lastMessage_ = description;
+                                emit lastMessageChanged();
+                        }
                         return;
                 }
                 if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
@@ -884,7 +903,10 @@ TimelineModel::updateLastMessage()
                   QString::fromStdString(http::client()->user_id().to_string()),
                   cache::displayName(room_id_,
                                      QString::fromStdString(mtx::accessors::sender(*event))));
-                emit manager_->updateRoomsLastMessage(room_id_, description);
+                if (description != lastMessage_) {
+                        lastMessage_ = description;
+                        emit lastMessageChanged();
+                }
                 return;
         }
 }
@@ -1866,6 +1888,17 @@ TimelineModel::roomName() const
                   QString::fromStdString(info[room_id_].name).toHtmlEscaped());
 }
 
+QString
+TimelineModel::plainRoomName() const
+{
+        auto info = cache::getRoomInfo({room_id_.toStdString()});
+
+        if (!info.count(room_id_))
+                return "";
+        else
+                return QString::fromStdString(info[room_id_].name);
+}
+
 QString
 TimelineModel::roomAvatarUrl() const
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 92fccd2d..3ebbe120 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -14,6 +14,7 @@
 #include 
 
 #include "CacheCryptoStructs.h"
+#include "CacheStructs.h"
 #include "EventStore.h"
 #include "InputBar.h"
 #include "Permissions.h"
@@ -253,12 +254,15 @@ public:
         }
 
         void updateLastMessage();
+        void sync(const mtx::responses::JoinedRoom &room);
         void addEvents(const mtx::responses::Timeline &events);
         void syncState(const mtx::responses::State &state);
         template
         void sendMessageEvent(const T &content, mtx::events::EventType eventType);
         RelatedInfo relatedInfo(QString id);
 
+        DescInfo lastMessage() const { return lastMessage_; }
+
 public slots:
         void setCurrentIndex(int index);
         int currentIndex() const { return idToIndex(currentId); }
@@ -303,12 +307,16 @@ public slots:
         }
 
         QString roomName() const;
+        QString plainRoomName() const;
         QString roomTopic() const;
         InputBar *input() { return &input_; }
         Permissions *permissions() { return &permissions_; }
         QString roomAvatarUrl() const;
         QString roomId() const { return room_id_; }
 
+        bool hasMentions() { return highlight_count > 0; }
+        int notificationCount() { return notification_count; }
+
         QString scrollTarget() const;
 
 private slots:
@@ -328,6 +336,9 @@ signals:
         void newCallEvent(const mtx::events::collections::TimelineEvents &event);
         void scrollToIndex(int index);
 
+        void lastMessageChanged();
+        void notificationsChanged();
+
         void openRoomSettingsDialog(RoomSettings *settings);
 
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
@@ -372,7 +383,11 @@ private:
         QString eventIdToShow;
         int showEventTimerCounter = 0;
 
+        DescInfo lastMessage_{};
+
         friend struct SendMessageVisitor;
+
+        int notification_count = 0, highlight_count = 0;
 };
 
 template
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 0785e3e1..a45294d1 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -4,7 +4,6 @@
 
 #include "TimelineViewManager.h"
 
-#include 
 #include 
 #include 
 #include 
@@ -32,6 +31,7 @@
 #include "emoji/Provider.h"
 #include "ui/NhekoCursorShape.h"
 #include "ui/NhekoDropArea.h"
+#include "ui/NhekoGlobalObject.h"
 
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
 Q_DECLARE_METATYPE(std::vector)
@@ -86,21 +86,6 @@ removeReplyFallback(mtx::events::Event &e)
 }
 }
 
-void
-TimelineViewManager::updateEncryptedDescriptions()
-{
-        auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
-        QHash>::iterator i;
-        for (i = models.begin(); i != models.end(); ++i) {
-                auto ptr = i.value();
-
-                if (!ptr.isNull()) {
-                        ptr->setDecryptDescription(decrypt);
-                        ptr->updateLastMessage();
-                }
-        }
-}
-
 void
 TimelineViewManager::updateColorPalette()
 {
@@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const
 }
 
 TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
-  : imgProvider(new MxcImageProvider())
+  : QObject(parent)
+  , imgProvider(new MxcImageProvider())
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
+  , rooms_(new RoomlistModel(this))
+  , communities_(new CommunitiesModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                   QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
                   return ptr;
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = new FilteredRoomlistModel(self->rooms_);
+
+                  connect(self->communities_,
+                          &CommunitiesModel::currentTagIdChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateFilterTag);
+                  connect(self->communities_,
+                          &CommunitiesModel::hiddenTagsChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
+                  return ptr;
+          });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->communities_;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   auto ptr = ChatPage::instance()->userSettings().data();
@@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   return new Clipboard();
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  return new Nheko();
+          });
 
         qRegisterMetaType();
         qRegisterMetaType>();
@@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                                          "Error: Only enums");
 
 #ifdef USE_QUICK_VIEW
-        view      = new QQuickView();
+        view      = new QQuickView(parent);
         container = QWidget::createWindowContainer(view, parent);
 #else
         view      = new QQuickWidget(parent);
@@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         view->engine()->addImageProvider("MxcImage", imgProvider);
         view->engine()->addImageProvider("colorimage", colorImgProvider);
         view->engine()->addImageProvider("blurhash", blurhashProvider);
-        view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
+        view->setSource(QUrl("qrc:///qml/Root.qml"));
 
         connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
-        connect(parent,
-                &ChatPage::decryptSidebarChanged,
-                this,
-                &TimelineViewManager::updateEncryptedDescriptions);
         connect(
           dynamic_cast(parent),
           &ChatPage::receivedRoomDeviceVerificationRequest,
@@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
-        for (const auto &[room_id, room] : rooms.join) {
-                // addRoom will only add the room, if it doesn't exist
-                addRoom(QString::fromStdString(room_id));
-                const auto &room_model = models.value(QString::fromStdString(room_id));
-                if (!isInitialSync_)
-                        connect(room_model.data(),
-                                &TimelineModel::newCallEvent,
-                                callManager_,
-                                &CallManager::syncEvent);
-                room_model->syncState(room.state);
-                room_model->addEvents(room.timeline);
-                if (!isInitialSync_)
-                        disconnect(room_model.data(),
-                                   &TimelineModel::newCallEvent,
-                                   callManager_,
-                                   &CallManager::syncEvent);
+        this->rooms_->sync(rooms_res);
+        this->communities_->sync(rooms_res);
 
-                if (ChatPage::instance()->userSettings()->typingNotifications()) {
-                        for (const auto &ev : room.ephemeral.events) {
-                                if (auto t = std::get_if<
-                                      mtx::events::EphemeralEvent>(
-                                      &ev)) {
-                                        std::vector typing;
-                                        typing.reserve(t->content.user_ids.size());
-                                        for (const auto &user : t->content.user_ids) {
-                                                if (user != http::client()->user_id().to_string())
-                                                        typing.push_back(
-                                                          QString::fromStdString(user));
-                                        }
-                                        room_model->updateTypingUsers(typing);
-                                }
-                        }
-                }
+        if (isInitialSync_) {
+                this->isInitialSync_ = false;
+                emit initialSyncChanged(false);
         }
-
-        this->isInitialSync_ = false;
-        emit initialSyncChanged(false);
-}
-
-void
-TimelineViewManager::addRoom(const QString &room_id)
-{
-        if (!models.contains(room_id)) {
-                QSharedPointer newRoom(new TimelineModel(this, room_id));
-                newRoom->setDecryptDescription(
-                  ChatPage::instance()->userSettings()->decryptSidebar());
-
-                connect(newRoom.data(),
-                        &TimelineModel::newEncryptedImage,
-                        imgProvider,
-                        &MxcImageProvider::addEncryptionInfo);
-                connect(newRoom.data(),
-                        &TimelineModel::forwardToRoom,
-                        this,
-                        &TimelineViewManager::forwardMessageToRoom);
-                models.insert(room_id, std::move(newRoom));
-        }
-}
-
-void
-TimelineViewManager::setHistoryView(const QString &room_id)
-{
-        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
-
-        auto room = models.find(room_id);
-        if (room != models.end()) {
-                timeline_ = room.value().data();
-                emit activeTimelineChanged(timeline_);
-                container->setFocus();
-                nhlog::ui()->info("Activated room {}", room_id.toStdString());
-        }
-}
-
-void
-TimelineViewManager::highlightRoom(const QString &room_id)
-{
-        ChatPage::instance()->highlightRoom(room_id);
 }
 
 void
 TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
 {
-        auto room = models.find(room_id);
-        if (room != models.end()) {
-                if (timeline_ != room.value().data()) {
-                        timeline_ = room.value().data();
-                        emit activeTimelineChanged(timeline_);
+        if (auto room = rooms_->getRoomById(room_id)) {
+                if (rooms_->currentRoom() != room) {
+                        rooms_->setCurrentRoom(room_id);
                         container->setFocus();
                         nhlog::ui()->info("Activated room {}", room_id.toStdString());
                 }
 
-                timeline_->showEvent(event_id);
+                room->showEvent(event_id);
         }
 }
 
@@ -457,12 +393,14 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
 
         auto imgDialog = new dialogs::ImageOverlay(pixmap);
         imgDialog->showFullScreen();
-        connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
+
+        auto room = rooms_->currentRoom();
+        connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() {
                 // hide the overlay while presenting the save dialog for better
                 // cross platform support.
                 imgDialog->hide();
 
-                if (!timeline_->saveMedia(eventId)) {
+                if (!room->saveMedia(eventId)) {
                         imgDialog->show();
                 } else {
                         imgDialog->close();
@@ -470,56 +408,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
         });
 }
 
-void
-TimelineViewManager::openLink(QString link) const
-{
-        QUrl url(link);
-        if (url.scheme() == "https" && url.host() == "matrix.to") {
-                // handle matrix.to links internally
-                QString p = url.fragment(QUrl::FullyEncoded);
-                if (p.startsWith("/"))
-                        p.remove(0, 1);
-
-                auto temp = p.split("?");
-                QString query;
-                if (temp.size() >= 2)
-                        query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
-
-                temp            = temp.first().split("/");
-                auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
-                QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
-                if (!identifier.isEmpty()) {
-                        if (identifier.startsWith("@")) {
-                                QByteArray uri =
-                                  "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        } else if (identifier.startsWith("#")) {
-                                QByteArray uri =
-                                  "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!eventId.isEmpty())
-                                        uri.append("/e/" +
-                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        } else if (identifier.startsWith("!")) {
-                                QByteArray uri = "matrix:roomid/" +
-                                                 QUrl::toPercentEncoding(identifier.remove(0, 1));
-                                if (!eventId.isEmpty())
-                                        uri.append("/e/" +
-                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
-                                if (!query.isEmpty())
-                                        uri.append("?" + query.toUtf8());
-                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
-                        }
-                }
-        } else {
-                QDesktopServices::openUrl(url);
-        }
-}
-
 void
 TimelineViewManager::openInviteUsersDialog()
 {
@@ -527,14 +415,14 @@ TimelineViewManager::openInviteUsersDialog()
           [this](const QStringList &invitees) { emit inviteUsers(invitees); });
 }
 void
-TimelineViewManager::openMemberListDialog() const
+TimelineViewManager::openMemberListDialog(QString roomid) const
 {
-        MainWindow::instance()->openMemberListDialog(timeline_->roomId());
+        MainWindow::instance()->openMemberListDialog(roomid);
 }
 void
-TimelineViewManager::openLeaveRoomDialog() const
+TimelineViewManager::openLeaveRoomDialog(QString roomid) const
 {
-        MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
+        MainWindow::instance()->openLeaveRoomDialog(roomid);
 }
 
 void
@@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid)
                         if (std::find(room_members.begin(),
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
-                                auto model = models.value(QString::fromStdString(room_id));
-                                auto flow  = DeviceVerificationFlow::InitiateUserVerification(
-                                  this, model.data(), userid);
-                                connect(model.data(),
-                                        &TimelineModel::updateFlowEventId,
-                                        this,
-                                        [this, flow](std::string eventId) {
-                                                dvList[QString::fromStdString(eventId)] = flow;
-                                        });
-                                emit newDeviceVerificationRequest(flow.data());
-                                return;
+                                if (auto model =
+                                      rooms_->getRoomById(QString::fromStdString(room_id))) {
+                                        auto flow =
+                                          DeviceVerificationFlow::InitiateUserVerification(
+                                            this, model.data(), userid);
+                                        connect(model.data(),
+                                                &TimelineModel::updateFlowEventId,
+                                                this,
+                                                [this, flow](std::string eventId) {
+                                                        dvList[QString::fromStdString(eventId)] =
+                                                          flow;
+                                                });
+                                        emit newDeviceVerificationRequest(flow.data());
+                                        return;
+                                }
                         }
                 }
         }
@@ -593,26 +485,24 @@ void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &event_ids)
 {
-        auto room = models.find(room_id);
-        if (room != models.end()) {
-                room.value()->markEventsAsRead(event_ids);
+        if (auto room = rooms_->getRoomById(room_id)) {
+                room->markEventsAsRead(event_ids);
         }
 }
 
 void
 TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        auto room = models.find(QString::fromStdString(room_id));
-        if (room != models.end()) {
-                room.value()->receivedSessionKey(session_id);
+        if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
+                room->receivedSessionKey(session_id);
         }
 }
 
 void
-TimelineViewManager::initWithMessages(const std::vector &roomIds)
+TimelineViewManager::initializeRoomlist()
 {
-        for (const auto &roomId : roomIds)
-                addRoom(roomId);
+        rooms_->initializeRooms();
+        communities_->initializeSidebar();
 }
 
 void
@@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid,
                                 const QString &repliedToEvent,
                                 const QString &replyBody)
 {
-        auto room = models.find(roomid);
-        if (room != models.end()) {
-                room.value()->setReply(repliedToEvent);
-                room.value()->input()->message(replyBody);
+        if (auto room = rooms_->getRoomById(roomid)) {
+                room->setReply(repliedToEvent);
+                room->input()->message(replyBody);
         }
 }
 
-void
-TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
-{
-        if (!timeline_)
-                return;
-
-        auto reactions = timeline_->reactions(reactedEvent.toStdString());
-
-        QString selfReactedEvent;
-        for (const auto &reaction : reactions) {
-                if (reactionKey == reaction.key_) {
-                        selfReactedEvent = reaction.selfReactedEvent_;
-                        break;
-                }
-        }
-
-        if (selfReactedEvent.startsWith("m"))
-                return;
-
-        // If selfReactedEvent is empty, that means we haven't previously reacted
-        if (selfReactedEvent.isEmpty()) {
-                mtx::events::msg::Reaction reaction;
-                mtx::common::Relation rel;
-                rel.rel_type = mtx::common::RelationType::Annotation;
-                rel.event_id = reactedEvent.toStdString();
-                rel.key      = reactionKey.toStdString();
-                reaction.relations.relations.push_back(rel);
-
-                timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
-                // Otherwise, we have previously reacted and the reaction should be redacted
-        } else {
-                timeline_->redactEvent(selfReactedEvent);
-        }
-}
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallInvite &callInvite)
 {
-        models.value(roomid)->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
+        if (auto room = rooms_->getRoomById(roomid))
+                room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallCandidates &callCandidates)
 {
-        models.value(roomid)->sendMessageEvent(callCandidates,
-                                               mtx::events::EventType::CallCandidates);
+        if (auto room = rooms_->getRoomById(roomid))
+                room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallAnswer &callAnswer)
 {
-        models.value(roomid)->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
+        if (auto room = rooms_->getRoomById(roomid))
+                room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
 }
 
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallHangUp &callHangUp)
 {
-        models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
+        if (auto room = rooms_->getRoomById(roomid))
+                room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
 
 void
@@ -738,7 +596,7 @@ void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
 {
-        auto room                                                = models.find(roomId);
+        auto room                                                = rooms_->getRoomById(roomId);
         auto content                                             = mtx::accessors::url(*e);
         std::optional encryptionInfo = mtx::accessors::file(*e);
 
@@ -781,12 +639,15 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                                               ev.content.url = url;
                                                       }
 
-                                                      auto room = models.find(roomId);
-                                                      removeReplyFallback(ev);
-                                                      ev.content.relations.relations.clear();
-                                                      room.value()->sendMessageEvent(
-                                                        ev.content,
-                                                        mtx::events::EventType::RoomMessage);
+                                                      if (auto room = rooms_->getRoomById(roomId)) {
+                                                              removeReplyFallback(ev);
+                                                              ev.content.relations.relations
+                                                                .clear();
+                                                              room->sendMessageEvent(
+                                                                ev.content,
+                                                                mtx::events::EventType::
+                                                                  RoomMessage);
+                                                      }
                                               }
                                       },
                                       *e);
@@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                 mtx::events::EventType::RoomMessage) {
                           e.content.relations.relations.clear();
                           removeReplyFallback(e);
-                          room.value()->sendMessageEvent(e.content,
-                                                         mtx::events::EventType::RoomMessage);
+                          room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
                   }
           },
           *e);
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b23a61db..556bcf4c 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,8 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/CommunitiesModel.h"
+#include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
 class BlurhashProvider;
@@ -34,12 +36,8 @@ class TimelineViewManager : public QObject
 {
         Q_OBJECT
 
-        Q_PROPERTY(
-          TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
         Q_PROPERTY(
           bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
-        Q_PROPERTY(
-          bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged)
         Q_PROPERTY(
           bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged)
 
@@ -48,48 +46,38 @@ public:
         QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
-        void addRoom(const QString &room_id);
 
-        void clearAll()
-        {
-                timeline_ = nullptr;
-                emit activeTimelineChanged(nullptr);
-                models.clear();
-        }
+        MxcImageProvider *imageProvider() { return imgProvider; }
+        CallManager *callManager() { return callManager_; }
+
+        void clearAll() { rooms_->clear(); }
 
-        Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
-        bool isNarrowView() const { return isNarrowView_; }
         bool isWindowFocused() const { return isWindowFocused_; }
         Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId);
         Q_INVOKABLE QColor userColor(QString id, QColor background);
         Q_INVOKABLE QString escapeEmoji(QString str) const;
+        Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }
 
         Q_INVOKABLE QString userPresence(QString id) const;
         Q_INVOKABLE QString userStatus(QString id) const;
 
-        Q_INVOKABLE void openLink(QString link) const;
-
         Q_INVOKABLE void focusMessageInput();
         Q_INVOKABLE void openInviteUsersDialog();
-        Q_INVOKABLE void openMemberListDialog() const;
-        Q_INVOKABLE void openLeaveRoomDialog() const;
+        Q_INVOKABLE void openMemberListDialog(QString roomid) const;
+        Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
         Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
 
         void verifyUser(QString userid);
         void verifyDevice(QString userid, QString deviceid);
 
 signals:
-        void clearRoomMessageCount(QString roomid);
-        void updateRoomsLastMessage(QString roomid, const DescInfo &info);
         void activeTimelineChanged(TimelineModel *timeline);
         void initialSyncChanged(bool isInitialSync);
         void replyingEventChanged(QString replyingEvent);
         void replyClosed();
         void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
         void inviteUsers(QStringList users);
-        void showRoomList();
-        void narrowViewChanged();
         void focusChanged();
         void focusInput();
         void openImageOverlayInternalCb(QString eventId, QImage img);
@@ -98,58 +86,32 @@ signals:
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector &event_ids);
         void receivedSessionKey(const std::string &room_id, const std::string &session_id);
-        void initWithMessages(const std::vector &roomIds);
+        void initializeRoomlist();
         void chatFocusChanged(bool focused)
         {
                 isWindowFocused_ = focused;
                 emit focusChanged();
         }
 
-        void setHistoryView(const QString &room_id);
-        void highlightRoom(const QString &room_id);
         void showEvent(const QString &room_id, const QString &event_id);
         void focusTimeline();
-        TimelineModel *getHistoryView(const QString &room_id)
-        {
-                auto room = models.find(room_id);
-                if (room != models.end())
-                        return room.value().data();
-                else
-                        return nullptr;
-        }
 
         void updateColorPalette();
         void queueReply(const QString &roomid,
                         const QString &repliedToEvent,
                         const QString &replyBody);
-        void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
 
-        void updateEncryptedDescriptions();
         void setVideoCallItem();
 
-        void enableBackButton()
-        {
-                if (isNarrowView_)
-                        return;
-                isNarrowView_ = true;
-                emit narrowViewChanged();
-        }
-        void disableBackButton()
-        {
-                if (!isNarrowView_)
-                        return;
-                isNarrowView_ = false;
-                emit narrowViewChanged();
-        }
-
-        void backToRooms() { emit showRoomList(); }
         QObject *completerFor(QString completerName, QString roomId = "");
         void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
 
+        RoomlistModel *rooms() { return rooms_; }
+
 private slots:
         void openImageOverlayInternal(QString eventId, QImage img);
 
@@ -165,14 +127,14 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        QHash> models;
-        TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
         bool isInitialSync_   = true;
-        bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
+        RoomlistModel *rooms_          = nullptr;
+        CommunitiesModel *communities_ = nullptr;
+
         QHash userColors;
 
         QHash> dvList;
diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index 54f48d3c..bbcedd7e 100644
--- a/src/ui/NhekoDropArea.cpp
+++ b/src/ui/NhekoDropArea.cpp
@@ -35,7 +35,7 @@ void
 NhekoDropArea::dropEvent(QDropEvent *event)
 {
         if (event) {
-                auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_);
+                auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
                 if (model) {
                         model->input()->insertMimeData(event->mimeData());
                 }
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
new file mode 100644
index 00000000..fea10839
--- /dev/null
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "NhekoGlobalObject.h"
+
+#include 
+#include 
+
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "UserSettingsPage.h"
+#include "Utils.h"
+
+Nheko::Nheko()
+{
+        connect(
+          UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged);
+        connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile);
+}
+
+void
+Nheko::updateUserProfile()
+{
+        if (cache::client() && cache::client()->isInitialized())
+                currentUser_.reset(
+                  new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager()));
+        else
+                currentUser_.reset();
+        emit profileChanged();
+}
+
+QPalette
+Nheko::colors() const
+{
+        return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString());
+}
+
+QPalette
+Nheko::inactiveColors() const
+{
+        auto p = colors();
+        p.setCurrentColorGroup(QPalette::ColorGroup::Inactive);
+        return p;
+}
+
+Theme
+Nheko::theme() const
+{
+        return Theme(UserSettings::instance()->theme().toStdString());
+}
+
+void
+Nheko::openLink(QString link) const
+{
+        QUrl url(link);
+        if (url.scheme() == "https" && url.host() == "matrix.to") {
+                // handle matrix.to links internally
+                QString p = url.fragment(QUrl::FullyEncoded);
+                if (p.startsWith("/"))
+                        p.remove(0, 1);
+
+                auto temp = p.split("?");
+                QString query;
+                if (temp.size() >= 2)
+                        query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
+
+                temp            = temp.first().split("/");
+                auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
+                QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
+                if (!identifier.isEmpty()) {
+                        if (identifier.startsWith("@")) {
+                                QByteArray uri =
+                                  "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                                if (!query.isEmpty())
+                                        uri.append("?" + query.toUtf8());
+                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
+                        } else if (identifier.startsWith("#")) {
+                                QByteArray uri =
+                                  "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                                if (!eventId.isEmpty())
+                                        uri.append("/e/" +
+                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                                if (!query.isEmpty())
+                                        uri.append("?" + query.toUtf8());
+                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
+                        } else if (identifier.startsWith("!")) {
+                                QByteArray uri = "matrix:roomid/" +
+                                                 QUrl::toPercentEncoding(identifier.remove(0, 1));
+                                if (!eventId.isEmpty())
+                                        uri.append("/e/" +
+                                                   QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                                if (!query.isEmpty())
+                                        uri.append("?" + query.toUtf8());
+                                ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
+                        }
+                }
+        } else {
+                QDesktopServices::openUrl(url);
+        }
+}
+void
+Nheko::setStatusMessage(QString msg) const
+{
+        ChatPage::instance()->setStatus(msg);
+}
+
+UserProfile *
+Nheko::currentUser() const
+{
+        nhlog::ui()->debug("Profile requested");
+
+        return currentUser_.get();
+}
+
+void
+Nheko::showUserSettingsPage() const
+{
+        ChatPage::instance()->showUserSettingsPage();
+}
+
+void
+Nheko::openLogoutDialog() const
+{
+        MainWindow::instance()->openLogoutDialog();
+}
+
+void
+Nheko::openCreateRoomDialog() const
+{
+        MainWindow::instance()->openCreateRoomDialog(
+          [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); });
+}
+
+void
+Nheko::openJoinRoomDialog() const
+{
+        MainWindow::instance()->openJoinRoomDialog(
+          [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
new file mode 100644
index 00000000..14135fd1
--- /dev/null
+++ b/src/ui/NhekoGlobalObject.h
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+
+#include "Theme.h"
+#include "UserProfile.h"
+
+class Nheko : public QObject
+{
+        Q_OBJECT
+
+        Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
+        Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
+        Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged)
+        Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
+        Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT)
+        Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT)
+        Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT)
+
+        Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged)
+
+public:
+        Nheko();
+
+        QPalette colors() const;
+        QPalette inactiveColors() const;
+        Theme theme() const;
+
+        int avatarSize() const { return 40; }
+
+        int paddingSmall() const { return 4; }
+        int paddingMedium() const { return 8; }
+        int paddingLarge() const { return 20; }
+        UserProfile *currentUser() const;
+
+        Q_INVOKABLE void openLink(QString link) const;
+        Q_INVOKABLE void setStatusMessage(QString msg) const;
+        Q_INVOKABLE void showUserSettingsPage() const;
+        Q_INVOKABLE void openLogoutDialog() const;
+        Q_INVOKABLE void openCreateRoomDialog() const;
+        Q_INVOKABLE void openJoinRoomDialog() const;
+
+public slots:
+        void updateUserProfile();
+
+signals:
+        void colorsChanged();
+        void profileChanged();
+
+private:
+        QScopedPointer currentUser_;
+};
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 4341bd63..26119393 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -2,76 +2,73 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-#include 
-
 #include "Theme.h"
 
-Theme::Theme(QObject *parent)
-  : QObject(parent)
+Q_DECLARE_METATYPE(Theme)
+
+QPalette
+Theme::paletteFromTheme(std::string_view theme)
 {
-        setColor("Black", ui::Color::Black);
-
-        setColor("BrightWhite", ui::Color::BrightWhite);
-        setColor("FadedWhite", ui::Color::FadedWhite);
-        setColor("MediumWhite", ui::Color::MediumWhite);
-
-        setColor("BrightGreen", ui::Color::BrightGreen);
-        setColor("DarkGreen", ui::Color::DarkGreen);
-        setColor("LightGreen", ui::Color::LightGreen);
-
-        setColor("Gray", ui::Color::Gray);
-        setColor("Red", ui::Color::Red);
-        setColor("Blue", ui::Color::Blue);
-
-        setColor("Transparent", ui::Color::Transparent);
-}
-
-QColor
-Theme::rgba(int r, int g, int b, qreal a) const
-{
-        QColor color(r, g, b);
-        color.setAlphaF(a);
-
-        return color;
-}
-
-QColor
-Theme::getColor(const QString &key) const
-{
-        if (!colors_.contains(key)) {
-                qWarning() << "Color with key" << key << "could not be found";
-                return QColor();
+        [[maybe_unused]] static auto meta = qRegisterMetaType("Theme");
+        static QPalette original;
+        if (theme == "light") {
+                QPalette lightActive(
+                  /*windowText*/ QColor("#333"),
+                  /*button*/ QColor("white"),
+                  /*light*/ QColor(0xef, 0xef, 0xef),
+                  /*dark*/ QColor(70, 77, 93),
+                  /*mid*/ QColor(220, 220, 220),
+                  /*text*/ QColor("#333"),
+                  /*bright_text*/ QColor("#f2f5f8"),
+                  /*base*/ QColor("#fff"),
+                  /*window*/ QColor("white"));
+                lightActive.setColor(QPalette::AlternateBase, QColor("#eee"));
+                lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
+                lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5"));
+                lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
+                lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
+                lightActive.setColor(QPalette::Link, QColor("#0077b5"));
+                lightActive.setColor(QPalette::ButtonText, QColor("#555459"));
+                return lightActive;
+        } else if (theme == "dark") {
+                QPalette darkActive(
+                  /*windowText*/ QColor("#caccd1"),
+                  /*button*/ QColor(0xff, 0xff, 0xff),
+                  /*light*/ QColor("#caccd1"),
+                  /*dark*/ QColor(60, 70, 77),
+                  /*mid*/ QColor("#202228"),
+                  /*text*/ QColor("#caccd1"),
+                  /*bright_text*/ QColor("#f4f5f8"),
+                  /*base*/ QColor("#202228"),
+                  /*window*/ QColor("#2d3139"));
+                darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139"));
+                darkActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
+                darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8"));
+                darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
+                darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
+                darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
+                darkActive.setColor(QPalette::ButtonText, "#727274");
+                return darkActive;
+        } else {
+                return original;
         }
-
-        return colors_.value(key);
 }
 
-void
-Theme::setColor(const QString &key, const QColor &color)
+Theme::Theme(std::string_view theme)
 {
-        colors_.insert(key, color);
-}
-
-void
-Theme::setColor(const QString &key, ui::Color color)
-{
-        static const QColor palette[] = {
-          QColor("#171919"),
-
-          QColor("#EBEBEB"),
-          QColor("#C9C9C9"),
-          QColor("#929292"),
-
-          QColor("#1C3133"),
-          QColor("#577275"),
-          QColor("#46A451"),
-
-          QColor("#5D6565"),
-          QColor("#E22826"),
-          QColor("#81B3A9"),
-
-          rgba(0, 0, 0, 0),
-        };
-
-        colors_.insert(key, palette[static_cast(color)]);
+        auto p     = paletteFromTheme(theme);
+        separator_ = p.mid().color();
+        if (theme == "light") {
+                sidebarBackground_ = QColor("#233649");
+                alternateButton_   = QColor("#ccc");
+                red_               = QColor("#a82353");
+        } else if (theme == "dark") {
+                sidebarBackground_ = QColor("#2d3139");
+                alternateButton_   = QColor("#414A59");
+                red_               = QColor("#a82353");
+        } else {
+                sidebarBackground_ = p.window().color();
+                alternateButton_   = p.dark().color();
+                red_               = QColor("red");
+        }
 }
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 3243c076..b5bcd4dd 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -5,8 +5,7 @@
 #pragma once
 
 #include 
-#include 
-#include 
+#include 
 
 namespace ui {
 enum class AvatarType
@@ -60,36 +59,25 @@ enum class ProgressType
         IndeterminateProgress
 };
 
-enum class Color
-{
-        Black,
-        BrightWhite,
-        FadedWhite,
-        MediumWhite,
-        DarkGreen,
-        LightGreen,
-        BrightGreen,
-        Gray,
-        Red,
-        Blue,
-        Transparent
-};
-
 } // namespace ui
 
-class Theme : public QObject
+class Theme : public QPalette
 {
-        Q_OBJECT
+        Q_GADGET
+        Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT)
+        Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT)
+        Q_PROPERTY(QColor separator READ separator CONSTANT)
+        Q_PROPERTY(QColor red READ red CONSTANT)
 public:
-        explicit Theme(QObject *parent = nullptr);
+        Theme() {}
+        explicit Theme(std::string_view theme);
+        static QPalette paletteFromTheme(std::string_view theme);
 
-        QColor getColor(const QString &key) const;
-
-        void setColor(const QString &key, const QColor &color);
-        void setColor(const QString &key, ui::Color color);
+        QColor sidebarBackground() const { return sidebarBackground_; }
+        QColor alternateButton() const { return alternateButton_; }
+        QColor separator() const { return separator_; }
+        QColor red() const { return red_; }
 
 private:
-        QColor rgba(int r, int g, int b, qreal a) const;
-
-        QHash colors_;
+        QColor sidebarBackground_, separator_, red_, alternateButton_;
 };
diff --git a/src/ui/ThemeManager.cpp b/src/ui/ThemeManager.cpp
index 834f5083..b7b3df40 100644
--- a/src/ui/ThemeManager.cpp
+++ b/src/ui/ThemeManager.cpp
@@ -6,18 +6,37 @@
 
 #include "ThemeManager.h"
 
-ThemeManager::ThemeManager() { setTheme(new Theme); }
-
-void
-ThemeManager::setTheme(Theme *theme)
-{
-        theme_ = theme;
-        theme_->setParent(this);
-}
+ThemeManager::ThemeManager() {}
 
 QColor
 ThemeManager::themeColor(const QString &key) const
 {
-        Q_ASSERT(theme_);
-        return theme_->getColor(key);
+        if (key == "Black")
+                return QColor("#171919");
+
+        else if (key == "BrightWhite")
+                return QColor("#EBEBEB");
+        else if (key == "FadedWhite")
+                return QColor("#C9C9C9");
+        else if (key == "MediumWhite")
+                return QColor("#929292");
+
+        else if (key == "BrightGreen")
+                return QColor("#1C3133");
+        else if (key == "DarkGreen")
+                return QColor("#577275");
+        else if (key == "LightGreen")
+                return QColor("#46A451");
+
+        else if (key == "Gray")
+                return QColor("#5D6565");
+        else if (key == "Red")
+                return QColor("#E22826");
+        else if (key == "Blue")
+                return QColor("#81B3A9");
+
+        else if (key == "Transparent")
+                return QColor(0, 0, 0, 0);
+
+        return (QColor(0, 0, 0, 0));
 }
diff --git a/src/ui/ThemeManager.h b/src/ui/ThemeManager.h
index f2099730..cbb355fd 100644
--- a/src/ui/ThemeManager.h
+++ b/src/ui/ThemeManager.h
@@ -6,8 +6,6 @@
 
 #include 
 
-#include "Theme.h"
-
 class ThemeManager : public QCommonStyle
 {
         Q_OBJECT
@@ -15,7 +13,6 @@ class ThemeManager : public QCommonStyle
 public:
         inline static ThemeManager &instance();
 
-        void setTheme(Theme *theme);
         QColor themeColor(const QString &key) const;
 
 private:
@@ -23,8 +20,6 @@ private:
 
         ThemeManager(ThemeManager const &);
         void operator=(ThemeManager const &);
-
-        Theme *theme_;
 };
 
 inline ThemeManager &
diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index 0f330964..3d9c4b6a 100644
--- a/src/ui/UserProfile.cpp
+++ b/src/ui/UserProfile.cpp
@@ -27,9 +27,22 @@ UserProfile::UserProfile(QString roomid,
   , manager(manager_)
   , model(parent)
 {
-        fetchDeviceList(this->userid_);
         globalAvatarUrl = "";
 
+        connect(this,
+                &UserProfile::globalUsernameRetrieved,
+                this,
+                &UserProfile::setGlobalUsername,
+                Qt::QueuedConnection);
+
+        if (isGlobalUserProfile()) {
+                getGlobalProfileData();
+        }
+
+        if (!cache::client() || !cache::client()->isDatabaseReady() ||
+            !ChatPage::instance()->timelineManager())
+                return;
+
         connect(cache::client(),
                 &Cache::verificationStatusChanged,
                 this,
@@ -53,17 +66,9 @@ UserProfile::UserProfile(QString roomid,
                                     : verification::VERIFIED;
                         }
                         deviceList_.reset(deviceList_.deviceList_);
+                        emit devicesChanged();
                 });
-
-        connect(this,
-                &UserProfile::globalUsernameRetrieved,
-                this,
-                &UserProfile::setGlobalUsername,
-                Qt::QueuedConnection);
-
-        if (isGlobalUserProfile()) {
-                getGlobalProfileData();
-        }
+        fetchDeviceList(this->userid_);
 }
 
 QHash
@@ -123,10 +128,7 @@ UserProfile::displayName()
 QString
 UserProfile::avatarUrl()
 {
-        return (isGlobalUserProfile() && globalAvatarUrl != "")
-                 ? globalAvatarUrl
-                 : cache::avatarUrl(roomid_, userid_);
-        ;
+        return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
 }
 
 bool
@@ -157,6 +159,9 @@ UserProfile::fetchDeviceList(const QString &userID)
 {
         auto localUser = utils::localUser();
 
+        if (!cache::client() || !cache::client()->isDatabaseReady())
+                return;
+
         cache::client()->query_keys(
           userID.toStdString(),
           [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys,
@@ -217,6 +222,7 @@ UserProfile::fetchDeviceList(const QString &userID)
                             }
 
                             this->deviceList_.queueReset(std::move(deviceInfo));
+                            emit devicesChanged();
                     });
           });
 }
diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
index bf71d0de..721d7230 100644
--- a/src/ui/UserProfile.h
+++ b/src/ui/UserProfile.h
@@ -90,7 +90,7 @@ class UserProfile : public QObject
         Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged)
         Q_PROPERTY(QString userid READ userid CONSTANT)
         Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
-        Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT)
+        Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged)
         Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT)
         Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged)
         Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
@@ -133,6 +133,7 @@ signals:
         void avatarUrlChanged();
         void displayError(const QString &errorMessage);
         void globalUsernameRetrieved(const QString &globalUser);
+        void devicesChanged();
 
 public slots:
         void updateAvatarUrl();