From a7f8b23b524c5e3af72e42fde118706e94a454f3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 13 May 2021 08:23:56 +0200 Subject: [PATCH 01/38] Make palette global in Qml --- CMakeLists.txt | 16 ++++++----- resources/qml/Avatar.qml | 6 ++--- resources/qml/Completer.qml | 22 +++++++-------- resources/qml/EncryptionIndicator.qml | 2 +- resources/qml/ForwardCompleter.qml | 12 ++++----- resources/qml/ImageButton.qml | 4 +-- resources/qml/MatrixText.qml | 2 +- resources/qml/MatrixTextField.qml | 8 +++--- resources/qml/MessageInput.qml | 10 +++---- resources/qml/MessageView.qml | 18 ++++++------- resources/qml/NhekoBusyIndicator.qml | 3 ++- resources/qml/QuickSwitcher.qml | 4 +-- resources/qml/Reactions.qml | 16 +++++------ resources/qml/ReplyPopup.qml | 4 +-- resources/qml/RoomSettings.qml | 6 ++--- resources/qml/TimelineRow.qml | 8 +++--- resources/qml/TimelineView.qml | 24 +++++++---------- resources/qml/TopBar.qml | 4 +-- resources/qml/TypingIndicator.qml | 6 ++--- resources/qml/UserProfile.qml | 12 ++++----- resources/qml/delegates/FileMessage.qml | 8 +++--- resources/qml/delegates/ImageMessage.qml | 6 ++--- resources/qml/delegates/MessageDelegate.qml | 2 +- resources/qml/delegates/NoticeMessage.qml | 4 ++- resources/qml/delegates/Pill.qml | 4 +-- resources/qml/delegates/Placeholder.qml | 3 ++- .../qml/delegates/PlayableMediaMessage.qml | 14 +++++----- resources/qml/delegates/Reply.qml | 2 +- resources/qml/delegates/TextMessage.qml | 2 +- .../AwaitingVerificationConfirmation.qml | 2 +- .../DeviceVerification.qml | 2 +- .../device-verification/DigitVerification.qml | 8 +++--- .../device-verification/EmojiVerification.qml | 6 ++--- resources/qml/device-verification/Failed.qml | 2 +- .../NewVerificationRequest.qml | 2 +- resources/qml/device-verification/Success.qml | 2 +- resources/qml/device-verification/Waiting.qml | 4 +-- resources/qml/emoji/EmojiPicker.qml | 16 +++++------ resources/qml/voip/CallDevices.qml | 10 +++---- resources/qml/voip/CallInvite.qml | 16 +++++------ resources/qml/voip/CallInviteBar.qml | 4 +-- resources/qml/voip/DeviceError.qml | 8 +++--- resources/qml/voip/PlaceCall.qml | 12 ++++----- resources/qml/voip/ScreenShare.qml | 12 ++++----- src/timeline/TimelineViewManager.cpp | 5 ++++ src/ui/NhekoGlobalObject.cpp | 27 +++++++++++++++++++ src/ui/NhekoGlobalObject.h | 25 +++++++++++++++++ 47 files changed, 226 insertions(+), 169 deletions(-) create mode 100644 src/ui/NhekoGlobalObject.cpp create mode 100644 src/ui/NhekoGlobalObject.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 96948827..5155af40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -284,11 +284,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 +298,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 @@ -500,26 +501,27 @@ qt5_wrap_cpp(MOC_HEADERS # 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 diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 108bb768..84c22da1 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -20,7 +20,7 @@ Rectangle { width: 48 height: 48 radius: Settings.avatarCircles ? height / 2 : 3 - color: colors.alternateBase + color: Nheko.colors.alternateBase Component.onCompleted: { mouseArea.clicked.connect(clicked); } @@ -33,7 +33,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter visible: img.status != Image.Ready - color: colors.text + color: Nheko.colors.text } Image { @@ -55,7 +55,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/Completer.qml b/resources/qml/Completer.qml index d648553f..2609371b 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -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,7 @@ 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 } } @@ -235,12 +235,12 @@ 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 } Label { text: "(" + model.roomAlias + ")" - color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } } @@ -274,10 +274,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/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..1ec18540 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,7 +44,7 @@ Popup { text: qsTr("Forward Message") font.bold: true bottomPadding: 10 - color: colors.text + color: Nheko.colors.text } Reply { @@ -52,14 +52,14 @@ Popup { modelData: TimelineManager.timeline ? TimelineManager.timeline.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; } @@ -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..60954bfd 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 diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 7cfa6735..fa1cd98c 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -13,7 +13,7 @@ TextEdit { wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode enabled: selectByMouse - color: colors.text + color: Nheko.colors.text onLinkActivated: TimelineManager.openLink(link) ToolTip.visible: hoveredLink ToolTip.text: hoveredLink diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index 3bcc9675..42ea33be 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -9,14 +9,14 @@ import QtQuick.Layouts 1.12 TextField { id: input - palette: colors + palette: Nheko.colors 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 +27,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 +60,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..f4e253ad 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 @@ -69,7 +69,7 @@ Rectangle { Rectangle { anchors.fill: parent - color: colors.window + color: Nheko.colors.window visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading NhekoBusyIndicator { @@ -116,8 +116,8 @@ 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 @@ -357,7 +357,7 @@ Rectangle { anchors.centerIn: parent visible: TimelineManager.timeline ? (!TimelineManager.timeline.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..0da1dff3 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -13,7 +13,7 @@ import im.nheko 1.0 ScrollView { clip: false - palette: colors + palette: Nheko.colors padding: 8 ScrollBar.horizontal.visible: false @@ -51,8 +51,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 +74,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 +220,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 +228,7 @@ ScrollView { background: Rectangle { radius: parent.height / 2 - color: colors.window + color: Nheko.colors.window } } @@ -267,7 +267,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,7 +288,7 @@ ScrollView { } Label { - color: colors.buttonText + color: Nheko.colors.buttonText text: modelData ? TimelineManager.userStatus(modelData.userId) : "" textFormat: Text.PlainText elide: Text.ElideRight @@ -317,7 +317,7 @@ ScrollView { opacity: 0 visible: true anchors.fill: timelinerow - color: colors.highlight + color: Nheko.colors.highlight states: State { name: "revealed" 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..a6373b1c 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; } diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index f53c89ad..064df543 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 @@ -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..1d85acb0 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -17,7 +17,7 @@ Rectangle { 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 +31,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/RoomSettings.qml b/resources/qml/RoomSettings.qml index 58567916..ba577f33 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -18,8 +18,8 @@ ApplicationWindow { y: MainWindow.y + (MainWindow.height / 2) - (height / 2) minimumWidth: 420 minimumHeight: 650 - palette: colors - color: colors.window + palette: Nheko.colors + color: Nheko.colors.window modality: Qt.WindowModal flags: Qt.Dialog title: qsTr("Room Settings") @@ -126,7 +126,7 @@ ApplicationWindow { readOnly: true background: null selectByMouse: true - color: colors.text + color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter onLinkActivated: TimelineManager.openLink(link) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 09a55e60..bae3e5a3 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 } @@ -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 6750b427..52847db0 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -18,15 +18,13 @@ import im.nheko.EmojiModel 1.0 Page { id: timelineRoot - 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 real highlightHue: Nheko.colors.highlight.hslHue + property real highlightSat: Nheko.colors.highlight.hslSaturation + property real highlightLight: Nheko.colors.highlight.hslLightness - palette: colors + palette: Nheko.colors FontMetrics { id: fontMetrics @@ -219,7 +217,7 @@ Page { Rectangle { anchors.fill: parent - color: colors.window + color: Nheko.colors.window Component { id: deviceVerificationDialog @@ -270,7 +268,7 @@ Page { anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 - color: colors.text + color: Nheko.colors.text } BusyIndicator { @@ -296,7 +294,7 @@ Page { Layout.fillWidth: true height: 1 z: 3 - color: colors.mid + color: Nheko.colors.mid } Rectangle { @@ -304,7 +302,7 @@ Page { Layout.fillWidth: true Layout.fillHeight: true - color: colors.base + color: Nheko.colors.base ColumnLayout { anchors.fill: parent @@ -358,7 +356,7 @@ Page { Layout.fillWidth: true z: 3 height: 1 - color: colors.mid + color: Nheko.colors.mid } ReplyPopup { @@ -383,8 +381,4 @@ Page { timelineRoot: timelineLayout } - systemInactive: SystemPalette { - colorGroup: SystemPalette.Disabled - } - } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 0b943ed1..4de126b6 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -16,7 +16,7 @@ Rectangle { Layout.fillWidth: true implicitHeight: topLayout.height + 16 z: 3 - color: colors.window + color: Nheko.colors.window TapHandler { onSingleTapped: { @@ -68,7 +68,7 @@ Rectangle { 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 diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml index ffe88fb6..783a9ebc 100644 --- a/resources/qml/TypingIndicator.qml +++ b/resources/qml/TypingIndicator.qml @@ -17,7 +17,7 @@ Item { id: typingRect visible: (room && room.typingUsers.length > 0) - color: colors.base + color: Nheko.colors.base anchors.fill: parent z: 3 @@ -29,8 +29,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..4e5e64dc 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -19,8 +19,8 @@ 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 flags: Qt.Dialog @@ -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/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index e883ddbb..2e5f33c2 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 @@ -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..704af3fe 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -19,7 +19,7 @@ Item { 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..4e6a73fe 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) } } 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..fef226a7 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml @@ -6,14 +6,14 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 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..223c2a34 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 @@ -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 @@ -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..810ee3d4 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -9,7 +9,7 @@ MatrixText { property string formatted: model.data.formattedBody property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body - text: "" + formatted.replace("
", "
")
+    text: "" + formatted.replace("
", "
")
     width: parent ? parent.width : undefined
     height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
     clip: isReply
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..6d0be204 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -16,7 +16,7 @@ ApplicationWindow {
     title: stack.currentItem.title
     flags: Qt.Dialog
     modality: Qt.WindowModal
-    palette: colors
+    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..b858a1a1 100644
--- a/resources/qml/device-verification/Success.qml
+++ b/resources/qml/device-verification/Success.qml
@@ -20,7 +20,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/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/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..c20e4fff 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -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..d3306099 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
@@ -46,7 +46,7 @@ Popup {
 
             Label {
                 text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName)
-                color: colors.windowText
+                color: Nheko.colors.windowText
             }
 
             Item {
@@ -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..258ac9b0 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 {
@@ -28,7 +28,7 @@ Popup {
             Layout.rightMargin: 8
             Layout.alignment: Qt.AlignLeft
             text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
-            color: colors.windowText
+            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 {
@@ -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/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 628f3c31..94cef1a7 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -32,6 +32,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)
@@ -221,6 +222,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>();
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
new file mode 100644
index 00000000..5a2b9788
--- /dev/null
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "NhekoGlobalObject.h"
+
+#include "UserSettingsPage.h"
+
+Nheko::Nheko()
+{
+        connect(
+          UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged);
+}
+
+QPalette
+Nheko::colors() const
+{
+        return QPalette();
+}
+
+QPalette
+Nheko::inactiveColors() const
+{
+        QPalette p;
+        p.setCurrentColorGroup(QPalette::ColorGroup::Inactive);
+        return p;
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
new file mode 100644
index 00000000..76186828
--- /dev/null
+++ b/src/ui/NhekoGlobalObject.h
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+
+class Nheko : public QObject
+{
+        Q_OBJECT
+
+        Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
+        Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
+
+public:
+        Nheko();
+
+        QPalette colors() const;
+        QPalette inactiveColors() const;
+
+signals:
+        void colorsChanged();
+};

From 22afa122c4697d25fd2f1eb4c7931bcf4ea43f31 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 13 May 2021 08:52:02 +0200
Subject: [PATCH 02/38] Move openLink to Nheko globals

---
 resources/qml/MatrixText.qml         |  2 +-
 resources/qml/RoomSettings.qml       |  2 +-
 src/timeline/TimelineViewManager.cpp | 51 --------------------------
 src/timeline/TimelineViewManager.h   |  2 --
 src/ui/NhekoGlobalObject.cpp         | 54 ++++++++++++++++++++++++++++
 src/ui/NhekoGlobalObject.h           |  3 ++
 6 files changed, 59 insertions(+), 55 deletions(-)

diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index fa1cd98c..167899a5 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -14,7 +14,7 @@ TextEdit {
     selectByMouse: !Settings.mobileMode
     enabled: selectByMouse
     color: Nheko.colors.text
-    onLinkActivated: TimelineManager.openLink(link)
+    onLinkActivated: Nheko.openLink(link)
     ToolTip.visible: hoveredLink
     ToolTip.text: hoveredLink
 
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index ba577f33..14de0edf 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -128,7 +128,7 @@ ApplicationWindow {
                 selectByMouse: true
                 color: Nheko.colors.text
                 horizontalAlignment: TextEdit.AlignHCenter
-                onLinkActivated: TimelineManager.openLink(link)
+                onLinkActivated: Nheko.openLink(link)
 
                 CursorShape {
                     anchors.fill: parent
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 94cef1a7..b407a128 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -4,7 +4,6 @@
 
 #include "TimelineViewManager.h"
 
-#include 
 #include 
 #include 
 #include 
@@ -476,56 +475,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()
 {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b23a61db..0665b663 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -68,8 +68,6 @@ public:
         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;
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index 5a2b9788..e5e6825e 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -4,6 +4,10 @@
 
 #include "NhekoGlobalObject.h"
 
+#include 
+#include 
+
+#include "ChatPage.h"
 #include "UserSettingsPage.h"
 
 Nheko::Nheko()
@@ -25,3 +29,53 @@ Nheko::inactiveColors() const
         p.setCurrentColorGroup(QPalette::ColorGroup::Inactive);
         return p;
 }
+
+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);
+        }
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 76186828..05a0c050 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -20,6 +20,9 @@ public:
         QPalette colors() const;
         QPalette inactiveColors() const;
 
+        Q_INVOKABLE void openLink(QString link) const;
+
 signals:
         void colorsChanged();
 };
+

From 877f4daa1a36aa5d803b944cc8ce21ed4717c9ce Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 13 May 2021 10:57:04 +0200
Subject: [PATCH 03/38] Fix missing import for colors

---
 resources/qml/MatrixTextField.qml | 1 +
 resources/qml/delegates/Pill.qml  | 1 +
 2 files changed, 2 insertions(+)

diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index 42ea33be..2ba648b5 100644
--- a/resources/qml/MatrixTextField.qml
+++ b/resources/qml/MatrixTextField.qml
@@ -5,6 +5,7 @@
 import QtQuick 2.12
 import QtQuick.Controls 2.12
 import QtQuick.Layouts 1.12
+import im.nheko 1.0
 
 TextField {
     id: input
diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
index fef226a7..248d91da 100644
--- a/resources/qml/delegates/Pill.qml
+++ b/resources/qml/delegates/Pill.qml
@@ -4,6 +4,7 @@
 
 import QtQuick 2.5
 import QtQuick.Controls 2.1
+import im.nheko 1.0
 
 Label {
     color: Nheko.colors.brightText

From 5658be52154e6a03aece3149aed6324f2ae221bc Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Thu, 13 May 2021 11:32:20 +0200
Subject: [PATCH 04/38] Move global avatarSize property

---
 resources/qml/MessageView.qml        | 6 +++---
 resources/qml/TimelineRow.qml        | 2 +-
 resources/qml/TimelineView.qml       | 6 ------
 resources/qml/TopBar.qml             | 8 ++++----
 resources/qml/voip/ActiveCallBar.qml | 4 ++--
 resources/qml/voip/CallInviteBar.qml | 4 ++--
 resources/qml/voip/PlaceCall.qml     | 4 ++--
 src/ui/NhekoGlobalObject.h           | 3 +++
 8 files changed, 17 insertions(+), 20 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 0da1dff3..5af4e4de 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -240,8 +240,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 : ""
@@ -292,7 +292,7 @@ ScrollView {
                         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
                     }
 
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index bae3e5a3..3fa1ad8e 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -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
 
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 52847db0..a848cb49 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -18,12 +18,6 @@ import im.nheko.EmojiModel 1.0
 Page {
     id: timelineRoot
 
-    property var systemInactive
-    readonly property int avatarSize: 40
-    property real highlightHue: Nheko.colors.highlight.hslHue
-    property real highlightSat: Nheko.colors.highlight.hslSaturation
-    property real highlightLight: Nheko.colors.highlight.hslLightness
-
     palette: Nheko.colors
 
     FontMetrics {
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 4de126b6..d5bcb3a8 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -43,8 +43,8 @@ Rectangle {
             Layout.row: 0
             Layout.rowSpan: 2
             Layout.alignment: Qt.AlignVCenter
-            width: avatarSize
-            height: avatarSize
+            width: Nheko.avatarSize
+            height: Nheko.avatarSize
             visible: TimelineManager.isNarrowView
             image: ":/icons/icons/ui/angle-pointing-to-left.png"
             ToolTip.visible: hovered
@@ -57,8 +57,8 @@ 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()
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 68d3bc4a..5798433a 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -31,8 +31,8 @@ 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)
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index c20e4fff..a169aca9 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -38,8 +38,8 @@ 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)
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index d3306099..7e2146cb 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -75,8 +75,8 @@ Popup {
 
             Avatar {
                 Layout.rightMargin: cameraCombo.visible ? 16 : 64
-                width: avatarSize
-                height: avatarSize
+                width: Nheko.avatarSize
+                height: Nheko.avatarSize
                 url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
                 displayName: TimelineManager.timeline.roomName
                 onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 05a0c050..9875507e 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -13,6 +13,7 @@ class Nheko : public QObject
 
         Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
         Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
+        Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
 
 public:
         Nheko();
@@ -20,6 +21,8 @@ public:
         QPalette colors() const;
         QPalette inactiveColors() const;
 
+        int avatarSize() const { return 40; }
+
         Q_INVOKABLE void openLink(QString link) const;
 
 signals:

From 39a43ad4abc84733b6e8a5a244f1055048ce115c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 14 May 2021 15:23:32 +0200
Subject: [PATCH 05/38] Reorganize TimelineView to prepare porting the room
 list

---
 resources/qml/ChatPage.qml                    |  48 ++
 resources/qml/ForwardCompleter.qml            |   2 +-
 resources/qml/Root.qml                        | 260 +++++++++++
 resources/qml/TimelineView.qml                | 415 ++++--------------
 resources/qml/delegates/ImageMessage.qml      |   6 +-
 .../qml/delegates/PlayableMediaMessage.qml    |   6 +-
 resources/qml/delegates/TextMessage.qml       |   2 +-
 resources/res.qrc                             |   2 +
 src/timeline/TimelineViewManager.cpp          |   2 +-
 src/ui/NhekoGlobalObject.h                    |   7 +
 10 files changed, 410 insertions(+), 340 deletions(-)
 create mode 100644 resources/qml/ChatPage.qml
 create mode 100644 resources/qml/Root.qml

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
new file mode 100644
index 00000000..a02f0ca9
--- /dev/null
+++ b/resources/qml/ChatPage.qml
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.9
+import QtQuick.Controls 2.13
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+Rectangle {
+    id: chatPage
+
+    color: Nheko.colors.window
+
+    SplitView {
+        anchors.fill: parent
+
+        Rectangle {
+            SplitView.minimumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            SplitView.preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            SplitView.maximumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            color: "blue"
+        }
+
+        Rectangle {
+            SplitView.minimumWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2
+            SplitView.preferredWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2
+            SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
+            color: "red"
+        }
+
+        TimelineView {
+            id: timeline
+
+            SplitView.fillWidth: true
+            SplitView.minimumWidth: 400
+        }
+
+    }
+
+    PrivacyScreen {
+        anchors.fill: parent
+        visible: Settings.privacyScreen
+        screenTimeout: Settings.privacyScreenTimeout
+        timelineRoot: timeline
+    }
+
+}
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 1ec18540..59bfe94d 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -21,7 +21,7 @@ Popup {
     modal: true
     palette: Nheko.colors
     parent: Overlay.overlay
-    width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
+    width: implicitWidth >= (timelineView.width * 0.8) ? implicitWidth : (timelineView.width * 0.8)
     height: implicitHeight + completerPopup.height + padding * 2
     leftPadding: 10
     rightPadding: 10
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
new file mode 100644
index 00000000..35b81a1f
--- /dev/null
+++ b/resources/qml/Root.qml
@@ -0,0 +1,260 @@
+// 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.13
+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 {
+        }
+
+    }
+
+    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("React")
+            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("Reply")
+            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 receipts")
+            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("Remove 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 event")
+            onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
+        }
+
+    }
+
+    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: TimelineManager.timeline
+        onOpenRoomSettingsDialog: {
+            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
+                "roomSettings": settings
+            });
+            roomSettings.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/TimelineView.qml b/resources/qml/TimelineView.qml
index a848cb49..0d0e286d 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -9,370 +9,123 @@ 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.13
 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
 
-    palette: Nheko.colors
-
-    FontMetrics {
-        id: fontMetrics
+    Label {
+        visible: !TimelineManager.timeline && !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("React")
-            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("Reply")
-            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 receipts")
-            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("Remove 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 event")
-            onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
-        }
-
-    }
-
-    Rectangle {
+        visible: TimelineManager.timeline != null
         anchors.fill: parent
-        color: Nheko.colors.window
-
-        Component {
-            id: deviceVerificationDialog
-
-            DeviceVerification {
-            }
+        spacing: 0
 
+        TopBar {
         }
 
-        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.colors.mid
         }
 
-        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 onActiveTimelineChanged() {
+                            stackLayout.currentIndex = 0;
+                        }
+
+                        target: TimelineManager
+                    }
+
+                    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: Nheko.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: Nheko.colors.mid
-            }
-
-            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 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: Nheko.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.colors.mid
+        }
+
+        ReplyPopup {
+        }
+
+        MessageInput {
         }
 
     }
 
-    PrivacyScreen {
+    NhekoDropArea {
         anchors.fill: parent
-        visible: Settings.privacyScreen
-        screenTimeout: Settings.privacyScreenTimeout
-        timelineRoot: timelineLayout
+        roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
     }
 
 }
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index 704af3fe..ce8e779c 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -9,10 +9,10 @@ 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
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 223c2a34..0234495d 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -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
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index 810ee3d4..ae622480 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -11,7 +11,7 @@ MatrixText {
 
     text: "" + formatted.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/res.qrc b/resources/res.qrc
index 304493b6..8105e966 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -123,6 +123,8 @@
     
         qtquickcontrols2.conf
 
+        qml/Root.qml
+        qml/ChatPage.qml
         qml/TimelineView.qml
         qml/Avatar.qml
         qml/Completer.qml
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b407a128..e8e57fd8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -257,7 +257,7 @@ 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,
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 9875507e..d952c266 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -14,6 +14,9 @@ class Nheko : public QObject
         Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
         Q_PROPERTY(QPalette inactiveColors READ inactiveColors 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)
 
 public:
         Nheko();
@@ -23,6 +26,10 @@ public:
 
         int avatarSize() const { return 40; }
 
+        int paddingSmall() const { return 4; }
+        int paddingMedium() const { return 8; }
+        int paddingLarge() const { return 20; }
+
         Q_INVOKABLE void openLink(QString link) const;
 
 signals:

From 567fe81ad78e707a4b914976a92c855d4ac8fc45 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 14 May 2021 23:35:34 +0200
Subject: [PATCH 06/38] Basic header and footer of room list

---
 resources/qml/ChatPage.qml     |  17 +--
 resources/qml/RoomList.qml     | 183 +++++++++++++++++++++++++++++++++
 resources/qml/TimelineView.qml |   4 +-
 resources/qml/TopBar.qml       |   4 +-
 resources/res.qrc              |   1 +
 src/Cache.cpp                  |  29 ++++--
 src/Cache_p.h                  |   3 +
 src/CommunitiesList.h          |   1 -
 src/CommunitiesListItem.h      |   1 -
 src/MainWindow.cpp             |   1 +
 src/MainWindow.h               |   1 +
 src/UserSettingsPage.cpp       |  37 +------
 src/ui/NhekoGlobalObject.cpp   |  33 +++++-
 src/ui/NhekoGlobalObject.h     |  16 ++-
 src/ui/Theme.cpp               | 119 ++++++++++-----------
 src/ui/Theme.h                 |  38 ++-----
 src/ui/ThemeManager.cpp        |  39 +++++--
 src/ui/ThemeManager.h          |   5 -
 src/ui/UserProfile.cpp         |  28 +++--
 19 files changed, 385 insertions(+), 175 deletions(-)
 create mode 100644 resources/qml/RoomList.qml

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index a02f0ca9..fc6137a6 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -19,14 +19,14 @@ Rectangle {
             SplitView.minimumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
             SplitView.preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
             SplitView.maximumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            color: "blue"
+            color: Nheko.theme.sidebarBackground
         }
 
-        Rectangle {
-            SplitView.minimumWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2
-            SplitView.preferredWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2
-            SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
-            color: "red"
+        RoomList {
+            //SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
+
+            SplitView.minimumWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
+            SplitView.preferredWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
         }
 
         TimelineView {
@@ -36,6 +36,11 @@ Rectangle {
             SplitView.minimumWidth: 400
         }
 
+        handle: Rectangle {
+            implicitWidth: 2
+            color: SplitHandle.pressed ? Nheko.colors.highlight : (SplitHandle.hovered ? Nheko.colors.light : Nheko.theme.separator)
+        }
+
     }
 
     PrivacyScreen {
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
new file mode 100644
index 00000000..25abb4d1
--- /dev/null
+++ b/resources/qml/RoomList.qml
@@ -0,0 +1,183 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.9
+import QtQuick.Controls 2.13
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+Page {
+
+    background: Rectangle {
+        color: Nheko.theme.sidebarBackground
+    }
+
+    header: ColumnLayout {
+        spacing: 0
+
+        Rectangle {
+            color: Nheko.colors.window
+            Layout.fillWidth: true
+            Layout.alignment: Qt.AlignBottom
+            Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
+            Layout.minimumHeight: 40
+
+            RowLayout {
+                id: userInfoGrid
+
+                spacing: Nheko.paddingMedium
+                anchors.fill: parent
+                anchors.margins: Nheko.paddingMedium
+
+                Avatar {
+                    id: avatar
+
+                    Layout.alignment: Qt.AlignVCenter
+                    Layout.preferredWidth: Nheko.avatarSize
+                    Layout.preferredHeight: Nheko.avatarSize
+                    url: Nheko.currentUser.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    displayName: Nheko.currentUser.displayName
+                    userid: Nheko.currentUser.userid
+                }
+
+                ColumnLayout {
+                    id: col
+
+                    Layout.alignment: Qt.AlignLeft
+                    Layout.fillWidth: true
+                    Layout.minimumWidth: 100
+                    width: parent.width - avatar.width - logoutButton.width
+                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width
+                    spacing: 0
+
+                    Label {
+                        Layout.alignment: Qt.AlignBottom
+                        color: Nheko.colors.text
+                        font.pointSize: fontMetrics.font.pointSize * 1.1
+                        font.weight: Font.DemiBold
+                        text: userNameText.elidedText
+                        maximumLineCount: 1
+                        elide: Text.ElideRight
+                        textFormat: Text.PlainText
+
+                        TextMetrics {
+                            id: userNameText
+
+                            font.pointSize: fontMetrics.font.pointSize * 1.1
+                            elide: Text.ElideRight
+                            elideWidth: col.width
+                            text: Nheko.currentUser.displayName
+                        }
+
+                    }
+
+                    Label {
+                        Layout.alignment: Qt.AlignTop
+                        color: Nheko.colors.buttonText
+                        font.weight: Font.Thin
+                        text: userIdText.elidedText
+                        maximumLineCount: 1
+                        textFormat: Text.PlainText
+                        font.pointSize: fontMetrics.font.pointSize * 0.9
+
+                        TextMetrics {
+                            id: userIdText
+
+                            font.pointSize: fontMetrics.font.pointSize * 0.9
+                            elide: Text.ElideRight
+                            elideWidth: col.width
+                            text: Nheko.currentUser.userid
+                        }
+
+                    }
+
+                }
+
+                Item {
+                }
+
+                ImageButton {
+                    id: logoutButton
+
+                    Layout.alignment: Qt.AlignVCenter
+                    image: ":/icons/icons/ui/power-button-off.png"
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("Logout")
+                }
+
+            }
+
+        }
+
+        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.alignment: Qt.AlignBottom | Qt.AlignLeft
+                    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
+                }
+
+                ImageButton {
+                    Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
+                    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 {
+                    Layout.alignment: Qt.AlignBottom | Qt.AlignRight
+                    hoverEnabled: true
+                    width: 22
+                    height: 22
+                    image: ":/icons/icons/ui/settings.png"
+                    ToolTip.visible: hovered
+                    ToolTip.text: qsTr("User settings")
+                    Layout.margins: Nheko.paddingMedium
+                }
+
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 0d0e286d..257d670d 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -49,7 +49,7 @@ Item {
             Layout.fillWidth: true
             height: 1
             z: 3
-            color: Nheko.colors.mid
+            color: Nheko.theme.separator
         }
 
         Rectangle {
@@ -112,7 +112,7 @@ Item {
             Layout.fillWidth: true
             z: 3
             height: 1
-            color: Nheko.colors.mid
+            color: Nheko.theme.separator
         }
 
         ReplyPopup {
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index d5bcb3a8..bda5ce14 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -14,7 +14,7 @@ Rectangle {
     property var room: TimelineManager.timeline
 
     Layout.fillWidth: true
-    implicitHeight: topLayout.height + 16
+    implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
     color: Nheko.colors.window
 
@@ -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 {
diff --git a/resources/res.qrc b/resources/res.qrc
index 8105e966..c146f2d9 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -125,6 +125,7 @@
 
         qml/Root.qml
         qml/ChatPage.qml
+        qml/RoomList.qml
         qml/TimelineView.qml
         qml/Avatar.qml
         qml/Completer.qml
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 24b2bc24..d8c78381 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_);
@@ -2426,7 +2429,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 {
@@ -3551,8 +3554,8 @@ Cache::query_keys(const std::string &user_id,
 
         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 +3564,22 @@ Cache::query_keys(const std::string &user_id,
                           return;
                   }
 
-                  cache::updateUserKeys(last_changed, res);
+                  emit userKeysUpdate(last_changed, res);
 
-                  auto keys = cache::userKeys(user_id);
-                  cb(keys.value_or(UserKeyCache{}), err);
+                  // use context object so that we can disconnect again
+                  std::unique_ptr context{new QObject};
+                  QObject *pcontext = context.get();
+                  QObject::connect(
+                    this,
+                    &Cache::verificationStatusChanged,
+                    pcontext,
+                    [cb, user_id, context_ = std::move(context)](std::string updated_user) mutable {
+                            if (user_id == updated_user) {
+                                    context_.release();
+                                    auto keys = cache::userKeys(user_id);
+                                    cb(keys.value_or(UserKeyCache{}), {});
+                            }
+                    });
           });
 }
 
@@ -3999,6 +4014,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
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 356c6e42..c55fa601 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -100,6 +100,7 @@ public:
 
         void saveState(const mtx::responses::Sync &res);
         bool isInitialized();
+        bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
 
         std::string nextBatchToken();
 
@@ -620,6 +621,8 @@ private:
         QString cacheDirectory_;
 
         VerificationStorage verification_storage;
+
+        bool databaseReady_ = false;
 };
 
 namespace cache {
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
index 2586f6f5..12b275b0 100644
--- a/src/CommunitiesList.h
+++ b/src/CommunitiesList.h
@@ -10,7 +10,6 @@
 
 #include "CacheStructs.h"
 #include "CommunitiesListItem.h"
-#include "ui/Theme.h"
 
 namespace mtx::responses {
 struct GroupProfile;
diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
index 006511c8..e7468611 100644
--- a/src/CommunitiesListItem.h
+++ b/src/CommunitiesListItem.h
@@ -10,7 +10,6 @@
 #include 
 
 #include "Config.h"
-#include "ui/Theme.h"
 
 class RippleOverlay;
 class QMouseEvent;
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 92f43e03..e2b625b0 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -295,6 +295,7 @@ MainWindow::showChatPage()
                 &Cache::secretChanged,
                 userSettingsPage_,
                 &UserSettingsPage::updateSecretStatus);
+        emit reload();
 }
 
 void
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4122e4c1..69d07e62 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -109,6 +109,7 @@ private slots:
 
 signals:
         void focusChanged(const bool focused);
+        void reload();
 
 private:
         bool loadJdenticonPlugin();
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 0edc1288..99560678 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -545,49 +545,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());
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index e5e6825e..70abfbb8 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -7,29 +7,50 @@
 #include 
 #include 
 
+#include "Cache_p.h"
 #include "ChatPage.h"
+#include "Logging.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 QPalette();
+        return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString());
 }
 
 QPalette
 Nheko::inactiveColors() const
 {
-        QPalette p;
+        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
 {
@@ -79,3 +100,11 @@ Nheko::openLink(QString link) const
                 QDesktopServices::openUrl(url);
         }
 }
+
+UserProfile *
+Nheko::currentUser() const
+{
+        nhlog::ui()->debug("Profile requested");
+
+        return currentUser_.get();
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index d952c266..fe645a34 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -7,32 +7,46 @@
 #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;
 
+private 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..ca2a4ce0 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -2,76 +2,65 @@
 //
 // 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(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("#555459"));
+                return lightActive;
+        } else if (theme == "dark") {
+                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");
+                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");
+        } else if (theme == "dark") {
+                sidebarBackground_ = QColor("#2d3139");
+        } else {
+                sidebarBackground_ = p.window().color();
+        }
 }
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 3243c076..64bc8273 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,21 @@ 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 separator READ separator 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 separator() const { return separator_; }
 
 private:
-        QColor rgba(int r, int g, int b, qreal a) const;
-
-        QHash colors_;
+        QColor sidebarBackground_, separator_;
 };
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..cef8bd85 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())
+                return;
+
+        fetchDeviceList(this->userid_);
         connect(cache::client(),
                 &Cache::verificationStatusChanged,
                 this,
@@ -54,16 +67,6 @@ UserProfile::UserProfile(QString roomid,
                         }
                         deviceList_.reset(deviceList_.deviceList_);
                 });
-
-        connect(this,
-                &UserProfile::globalUsernameRetrieved,
-                this,
-                &UserProfile::setGlobalUsername,
-                Qt::QueuedConnection);
-
-        if (isGlobalUserProfile()) {
-                getGlobalProfileData();
-        }
 }
 
 QHash
@@ -157,6 +160,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,

From 10fd2752f9863c43bf7df6c39d7cec1397dfde1c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Wed, 19 May 2021 19:34:10 +0200
Subject: [PATCH 07/38] Some basic room list

---
 CMakeLists.txt                       |   2 +
 resources/qml/ElidedLabel.qml        |  28 +++++
 resources/qml/ForwardCompleter.qml   |   2 +-
 resources/qml/RoomList.qml           | 171 +++++++++++++++++++------
 resources/res.qrc                    |   1 +
 src/timeline/RoomlistModel.cpp       | 146 ++++++++++++++++++++++
 src/timeline/RoomlistModel.h         |  58 +++++++++
 src/timeline/TimelineViewManager.cpp | 178 +++++++++------------------
 src/timeline/TimelineViewManager.h   |  17 ++-
 9 files changed, 439 insertions(+), 164 deletions(-)
 create mode 100644 resources/qml/ElidedLabel.qml
 create mode 100644 src/timeline/RoomlistModel.cpp
 create mode 100644 src/timeline/RoomlistModel.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5155af40..8b43559f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -272,6 +272,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
@@ -497,6 +498,7 @@ 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
diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml
new file mode 100644
index 00000000..5ae99de7
--- /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.13
+import im.nheko 1.0
+
+Label {
+    id: root
+
+    property alias fullText: metrics.text
+    property alias elideWidth: metrics.elideWidth
+
+    color: Nheko.colors.text
+    text: 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/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 59bfe94d..1ec18540 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -21,7 +21,7 @@ Popup {
     modal: true
     palette: Nheko.colors
     parent: Overlay.overlay
-    width: implicitWidth >= (timelineView.width * 0.8) ? implicitWidth : (timelineView.width * 0.8)
+    width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
     height: implicitHeight + completerPopup.height + padding * 2
     leftPadding: 10
     rightPadding: 10
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 25abb4d1..87a27517 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -8,6 +8,132 @@ import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Page {
+    ListView {
+        anchors.left: parent.left
+        anchors.right: parent.right
+        height: parent.height
+        model: Rooms
+
+        ScrollHelper {
+            flickable: parent
+            anchors.fill: parent
+            enabled: !Settings.mobileMode
+        }
+
+        delegate: Rectangle {
+            color: Nheko.colors.window
+            height: fontMetrics.lineSpacing * 2.5 + Nheko.paddingMedium * 2
+            width: ListView.view.width
+
+            RowLayout {
+                //id: userInfoGrid
+
+                spacing: Nheko.paddingMedium
+                anchors.fill: parent
+                anchors.margins: Nheko.paddingMedium
+
+                Avatar {
+                    //userid: Nheko.currentUser.userid
+
+                    id: avatar
+
+                    Layout.alignment: Qt.AlignVCenter
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2.5
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2.5
+                    url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    displayName: model.roomName
+                }
+
+                ColumnLayout {
+                    id: textContent
+
+                    Layout.alignment: Qt.AlignLeft
+                    Layout.fillWidth: true
+                    Layout.minimumWidth: 100
+                    width: parent.width - avatar.width
+                    Layout.preferredWidth: parent.width - avatar.width
+                    spacing: 0
+
+                    RowLayout {
+                        Layout.fillWidth: true
+                        spacing: 0
+
+                        ElidedLabel {
+                            Layout.alignment: Qt.AlignBottom
+                            color: Nheko.colors.text
+                            elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium
+                            fullText: model.roomName + ": " + model.notificationCount
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                        }
+
+                        Label {
+                            id: timestamp
+
+                            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                            color: Nheko.colors.buttonText
+                            text: "14:32"
+                        }
+
+                    }
+
+                    RowLayout {
+                        Layout.fillWidth: true
+                        spacing: 0
+
+                        ElidedLabel {
+                            color: Nheko.colors.buttonText
+                            font.weight: Font.Thin
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                            elideWidth: textContent.width - notificationBubble.width
+                            fullText: model.lastMessage
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                        }
+
+                        Rectangle {
+                            id: notificationBubble
+
+                            Layout.alignment: Qt.AlignRight
+                            height: fontMetrics.font.pixelSize * 1.3
+                            width: height
+                            radius: height / 2
+                            color: Nheko.colors.highlight
+
+                            Label {
+                                anchors.fill: parent
+                                horizontalAlignment: Text.AlignHCenter
+                                verticalAlignment: Text.AlignVCenter
+                                fontSizeMode: Text.Fit
+                                color: Nheko.colors.highlightedText
+                                text: model.notificationCount
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+            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
@@ -34,8 +160,8 @@ Page {
                     id: avatar
 
                     Layout.alignment: Qt.AlignVCenter
-                    Layout.preferredWidth: Nheko.avatarSize
-                    Layout.preferredHeight: Nheko.avatarSize
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
                     url: Nheko.currentUser.avatarUrl.replace("mxc://", "image://MxcImage/")
                     displayName: Nheko.currentUser.displayName
                     userid: Nheko.currentUser.userid
@@ -46,50 +172,25 @@ Page {
 
                     Layout.alignment: Qt.AlignLeft
                     Layout.fillWidth: true
-                    Layout.minimumWidth: 100
-                    width: parent.width - avatar.width - logoutButton.width
-                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width
+                    width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
+                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
                     spacing: 0
 
-                    Label {
+                    ElidedLabel {
                         Layout.alignment: Qt.AlignBottom
-                        color: Nheko.colors.text
                         font.pointSize: fontMetrics.font.pointSize * 1.1
                         font.weight: Font.DemiBold
-                        text: userNameText.elidedText
-                        maximumLineCount: 1
-                        elide: Text.ElideRight
-                        textFormat: Text.PlainText
-
-                        TextMetrics {
-                            id: userNameText
-
-                            font.pointSize: fontMetrics.font.pointSize * 1.1
-                            elide: Text.ElideRight
-                            elideWidth: col.width
-                            text: Nheko.currentUser.displayName
-                        }
-
+                        fullText: Nheko.currentUser.displayName
+                        elideWidth: col.width
                     }
 
-                    Label {
+                    ElidedLabel {
                         Layout.alignment: Qt.AlignTop
                         color: Nheko.colors.buttonText
                         font.weight: Font.Thin
-                        text: userIdText.elidedText
-                        maximumLineCount: 1
-                        textFormat: Text.PlainText
                         font.pointSize: fontMetrics.font.pointSize * 0.9
-
-                        TextMetrics {
-                            id: userIdText
-
-                            font.pointSize: fontMetrics.font.pointSize * 0.9
-                            elide: Text.ElideRight
-                            elideWidth: col.width
-                            text: Nheko.currentUser.userid
-                        }
-
+                        elideWidth: col.width
+                        fullText: Nheko.currentUser.userid
                     }
 
                 }
diff --git a/resources/res.qrc b/resources/res.qrc
index c146f2d9..79e63810 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -131,6 +131,7 @@
         qml/Completer.qml
         qml/EncryptionIndicator.qml
         qml/ImageButton.qml
+        qml/ElidedLabel.qml
         qml/MatrixText.qml
         qml/MatrixTextField.qml
         qml/ToggleButton.qml
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
new file mode 100644
index 00000000..6a1fc3c5
--- /dev/null
+++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,146 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomlistModel.h"
+
+#include "ChatPage.h"
+#include "MatrixClient.h"
+#include "MxcImageProvider.h"
+#include "TimelineModel.h"
+#include "TimelineViewManager.h"
+#include "UserSettingsPage.h"
+
+RoomlistModel::RoomlistModel(TimelineViewManager *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();
+                        }
+                }
+        });
+}
+
+QHash
+RoomlistModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {RoomName, "roomName"},
+          {LastMessage, "lastMessage"},
+          {HasUnreadMessages, "hasUnreadMessages"},
+          {NotificationCount, "notificationCount"},
+        };
+}
+
+QVariant
+RoomlistModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) {
+                auto room = models.value(roomids.at(index.row()));
+                switch (role) {
+                case Roles::AvatarUrl:
+                        return room->roomAvatarUrl();
+                case Roles::RoomName:
+                        return room->roomName();
+                case Roles::LastMessage:
+                        return QString("Nico: Hahaha, this is funny!");
+                case Roles::HasUnreadMessages:
+                        return true;
+                case Roles::NotificationCount:
+                        return 5;
+                default:
+                        return {};
+                }
+        } else {
+                return {};
+        }
+}
+
+void
+RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
+{
+        if (!models.contains(room_id)) {
+                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);
+
+                if (!suppressInsertNotification)
+                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+                models.insert(room_id, std::move(newRoom));
+                roomids.push_back(room_id);
+                if (!suppressInsertNotification)
+                        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->syncState(room.state);
+                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);
+                                }
+                        }
+                }
+        }
+}
+
+void
+RoomlistModel::initializeRooms(const std::vector &roomIds_)
+{
+        beginResetModel();
+        models.clear();
+        roomids.clear();
+        roomids = roomIds_;
+        for (const auto &id : roomIds_)
+                addRoom(id, true);
+        endResetModel();
+}
+
+void
+RoomlistModel::clear()
+{
+        beginResetModel();
+        models.clear();
+        roomids.clear();
+        endResetModel();
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
new file mode 100644
index 00000000..44fcf032
--- /dev/null
+++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+class TimelineModel;
+class TimelineViewManager;
+
+class RoomlistModel : public QAbstractListModel
+{
+        Q_OBJECT
+public:
+        enum Roles
+        {
+                AvatarUrl = Qt::UserRole,
+                RoomName,
+                LastMessage,
+                HasUnreadMessages,
+                NotificationCount,
+        };
+
+        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(const std::vector &roomids);
+        void sync(const mtx::responses::Rooms &rooms);
+        void clear();
+
+private:
+        void addRoom(const QString &room_id, bool suppressInsertNotification = false);
+
+        TimelineViewManager *manager = nullptr;
+        std::vector roomids;
+        QHash> models;
+};
+
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e8e57fd8..b0c13b03 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -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()
 {
@@ -148,6 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
+  , rooms(new RoomlistModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -205,6 +191,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                   QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
                   return ptr;
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->rooms;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   auto ptr = ChatPage::instance()->userSettings().data();
@@ -260,10 +252,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         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,
@@ -334,64 +322,13 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
+TimelineViewManager::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));
-                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_);
 
-                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);
-                                }
-                        }
-                }
-        }
-
-        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));
+        if (isInitialSync_) {
+                this->isInitialSync_ = false;
+                emit initialSyncChanged(false);
         }
 }
 
@@ -400,9 +337,8 @@ 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();
+        if (auto room = rooms->getRoomById(room_id)) {
+                timeline_ = room.get();
                 emit activeTimelineChanged(timeline_);
                 container->setFocus();
                 nhlog::ui()->info("Activated room {}", room_id.toStdString());
@@ -418,10 +354,9 @@ TimelineViewManager::highlightRoom(const QString &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();
+        if (auto room = rooms->getRoomById(room_id)) {
+                if (timeline_ != room) {
+                        timeline_ = room.get();
                         emit activeTimelineChanged(timeline_);
                         container->setFocus();
                         nhlog::ui()->info("Activated room {}", room_id.toStdString());
@@ -505,17 +440,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;
+                                }
                         }
                 }
         }
@@ -548,26 +487,23 @@ 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)
 {
-        for (const auto &roomId : roomIds)
-                addRoom(roomId);
+        rooms->initializeRooms(roomIds);
 }
 
 void
@@ -575,10 +511,9 @@ 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);
         }
 }
 
@@ -620,29 +555,32 @@ 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
@@ -693,7 +631,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);
 
@@ -736,12 +674,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);
@@ -759,8 +700,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 0665b663..f4297243 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,7 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
 class BlurhashProvider;
@@ -48,13 +49,15 @@ public:
         QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
-        void addRoom(const QString &room_id);
+
+        MxcImageProvider *imageProvider() { return imgProvider; }
+        CallManager *callManager() { return callManager_; }
 
         void clearAll()
         {
                 timeline_ = nullptr;
                 emit activeTimelineChanged(nullptr);
-                models.clear();
+                rooms->clear();
         }
 
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
@@ -109,11 +112,7 @@ public slots:
         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;
+                return rooms->getRoomById(room_id).get();
         }
 
         void updateColorPalette();
@@ -126,7 +125,6 @@ public slots:
         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()
@@ -163,7 +161,6 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        QHash> models;
         TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
@@ -171,6 +168,8 @@ private:
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
+        RoomlistModel *rooms = nullptr;
+
         QHash userColors;
 
         QHash> dvList;

From cd67046f6011f0838b5ed4621fb3ee9b846e63a0 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 21 May 2021 21:19:03 +0200
Subject: [PATCH 08/38] Make roomlist look nice

---
 resources/qml/RoomList.qml     |  99 ++++++++++++++++++++++++------
 src/CacheStructs.h             |  13 ++++
 src/ChatPage.cpp               |   5 --
 src/RoomList.cpp               |   5 --
 src/timeline/RoomlistModel.cpp | 107 +++++++++++++++++++++++++++++++--
 src/timeline/RoomlistModel.h   |  20 +++++-
 src/timeline/TimelineModel.cpp |  30 +++++++--
 src/timeline/TimelineModel.h   |  14 +++++
 src/ui/Theme.cpp               |  13 ++--
 src/ui/Theme.h                 |   4 +-
 10 files changed, 265 insertions(+), 45 deletions(-)

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 87a27517..bb8deda6 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -2,13 +2,15 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import QtQuick 2.9
+import QtQuick 2.13
 import QtQuick.Controls 2.13
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Page {
     ListView {
+        id: roomlist
+
         anchors.left: parent.left
         anchors.right: parent.right
         height: parent.height
@@ -20,26 +22,80 @@ Page {
             enabled: !Settings.mobileMode
         }
 
+        Connections {
+            onActiveTimelineChanged: {
+                roomlist.positionViewAtIndex(Rooms.roomidToIndex(TimelineManager.timeline.roomId()), ListView.Contain);
+                console.log("Test" + TimelineManager.timeline.roomId() + " " + Rooms.roomidToIndex(TimelineManager.timeline.roomId));
+            }
+            target: TimelineManager
+        }
+
         delegate: Rectangle {
-            color: Nheko.colors.window
-            height: fontMetrics.lineSpacing * 2.5 + Nheko.paddingMedium * 2
+            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: Math.ceil(fontMetrics.lineSpacing * 2.3 + Nheko.paddingMedium * 2)
             width: ListView.view.width
+            state: "normal"
+            states: [
+                State {
+                    name: "highlight"
+                    when: hovered.hovered
+
+                    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: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId()
+
+                    PropertyChanges {
+                        target: roomItem
+                        background: Nheko.colors.highlight
+                        importantText: Nheko.colors.highlightedText
+                        unimportantText: Nheko.colors.highlightedText
+                        bubbleBackground: Nheko.colors.highlightedText
+                        bubbleText: Nheko.colors.highlight
+                    }
+
+                }
+            ]
+
+            HoverHandler {
+                id: hovered
+            }
+
+            TapHandler {
+                onSingleTapped: TimelineManager.setHistoryView(model.roomId)
+            }
 
             RowLayout {
-                //id: userInfoGrid
-
                 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
 
                     Layout.alignment: Qt.AlignVCenter
-                    Layout.preferredWidth: fontMetrics.lineSpacing * 2.5
-                    Layout.preferredHeight: fontMetrics.lineSpacing * 2.5
+                    height: Math.ceil(fontMetrics.lineSpacing * 2.3)
+                    width: Math.ceil(fontMetrics.lineSpacing * 2.3)
                     url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
                     displayName: model.roomName
                 }
@@ -52,7 +108,7 @@ Page {
                     Layout.minimumWidth: 100
                     width: parent.width - avatar.width
                     Layout.preferredWidth: parent.width - avatar.width
-                    spacing: 0
+                    spacing: Nheko.paddingSmall
 
                     RowLayout {
                         Layout.fillWidth: true
@@ -60,9 +116,9 @@ Page {
 
                         ElidedLabel {
                             Layout.alignment: Qt.AlignBottom
-                            color: Nheko.colors.text
+                            color: roomItem.importantText
                             elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium
-                            fullText: model.roomName + ": " + model.notificationCount
+                            fullText: model.roomName
                         }
 
                         Item {
@@ -74,8 +130,8 @@ Page {
 
                             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
-                            color: Nheko.colors.buttonText
-                            text: "14:32"
+                            color: roomItem.unimportantText
+                            text: model.timestamp
                         }
 
                     }
@@ -85,10 +141,10 @@ Page {
                         spacing: 0
 
                         ElidedLabel {
-                            color: Nheko.colors.buttonText
+                            color: roomItem.unimportantText
                             font.weight: Font.Thin
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
-                            elideWidth: textContent.width - notificationBubble.width
+                            elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall
                             fullText: model.lastMessage
                         }
 
@@ -99,19 +155,24 @@ Page {
                         Rectangle {
                             id: notificationBubble
 
+                            visible: model.notificationCount > 0
                             Layout.alignment: Qt.AlignRight
-                            height: fontMetrics.font.pixelSize * 1.3
+                            height: fontMetrics.averageCharacterWidth * 3
                             width: height
                             radius: height / 2
-                            color: Nheko.colors.highlight
+                            color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground
 
                             Label {
-                                anchors.fill: parent
+                                anchors.centerIn: parent
+                                width: parent.width * 0.8
+                                height: parent.height * 0.8
                                 horizontalAlignment: Text.AlignHCenter
                                 verticalAlignment: Text.AlignVCenter
                                 fontSizeMode: Text.Fit
-                                color: Nheko.colors.highlightedText
-                                text: model.notificationCount
+                                font.bold: true
+                                font.pixelSize: fontMetrics.font.pixelSize * 0.8
+                                color: model.hasLoudNotification ? "white" : roomItem.bubbleText
+                                text: model.notificationCount > 99 ? "99+" : model.notificationCount
                             }
 
                         }
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/ChatPage.cpp b/src/ChatPage.cpp
index c5199ff1..58b76174 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -233,11 +233,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 room_list_,
                 &RoomList::updateRoomDescription);
 
-        connect(room_list_,
-                SIGNAL(totalUnreadMessageCountUpdated(int)),
-                this,
-                SIGNAL(unreadMessages(int)));
-
         connect(
           this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
 
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 8a807e71..5c41a7a1 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -305,8 +305,6 @@ 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;
         }
 
@@ -320,9 +318,6 @@ 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;
         }
 
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 6a1fc3c5..5fc4dc65 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -4,6 +4,7 @@
 
 #include "RoomlistModel.h"
 
+#include "Cache_p.h"
 #include "ChatPage.h"
 #include "MatrixClient.h"
 #include "MxcImageProvider.h"
@@ -26,6 +27,11 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent)
                         }
                 }
         });
+
+        connect(this,
+                &RoomlistModel::totalUnreadMessageCountUpdated,
+                ChatPage::instance(),
+                &ChatPage::unreadMessages);
 }
 
 QHash
@@ -34,8 +40,11 @@ RoomlistModel::roleNames() const
         return {
           {AvatarUrl, "avatarUrl"},
           {RoomName, "roomName"},
+          {RoomId, "roomId"},
           {LastMessage, "lastMessage"},
+          {Timestamp, "timestamp"},
           {HasUnreadMessages, "hasUnreadMessages"},
+          {HasLoudNotification, "hasLoudNotification"},
           {NotificationCount, "notificationCount"},
         };
 }
@@ -44,18 +53,26 @@ QVariant
 RoomlistModel::data(const QModelIndex &index, int role) const
 {
         if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) {
-                auto room = models.value(roomids.at(index.row()));
+                auto roomid = roomids.at(index.row());
+                auto room   = models.value(roomid);
                 switch (role) {
                 case Roles::AvatarUrl:
                         return room->roomAvatarUrl();
                 case Roles::RoomName:
                         return room->roomName();
+                case Roles::RoomId:
+                        return room->roomId();
                 case Roles::LastMessage:
-                        return QString("Nico: Hahaha, this is funny!");
+                        return room->lastMessage().body;
+                case Roles::Timestamp:
+                        return room->lastMessage().descriptiveTime;
                 case Roles::HasUnreadMessages:
-                        return true;
+                        return this->roomReadStatus.count(roomid) &&
+                               this->roomReadStatus.at(roomid);
+                case Roles::HasLoudNotification:
+                        return room->hasMentions();
                 case Roles::NotificationCount:
-                        return 5;
+                        return room->notificationCount();
                 default:
                         return {};
                 }
@@ -64,10 +81,38 @@ RoomlistModel::data(const QModelIndex &index, int role) const
         }
 }
 
+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 = roomReadStatus_;
+
+        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());
@@ -80,6 +125,56 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                         &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,
+                                           });
+                  });
+                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,
+                                           });
+
+                          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();
 
                 if (!suppressInsertNotification)
                         beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
@@ -97,8 +192,8 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms)
                 // 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->syncState(room.state);
-                room_model->addEvents(room.timeline);
+                room_model->sync(room);
+                // room_model->addEvents(room.timeline);
                 connect(room_model.data(),
                         &TimelineModel::newCallEvent,
                         manager->callManager(),
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 44fcf032..c4c9d9ba 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -22,8 +22,11 @@ public:
         {
                 AvatarUrl = Qt::UserRole,
                 RoomName,
+                RoomId,
                 LastMessage,
+                Timestamp,
                 HasUnreadMessages,
+                HasLoudNotification,
                 NotificationCount,
         };
 
@@ -47,6 +50,21 @@ public slots:
         void initializeRooms(const std::vector &roomids);
         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;
+        }
+
+private slots:
+        void updateReadStatus(const std::map roomReadStatus_);
+
+signals:
+        void totalUnreadMessageCountUpdated(int unreadMessages);
 
 private:
         void addRoom(const QString &room_id, bool suppressInsertNotification = false);
@@ -54,5 +72,5 @@ private:
         TimelineViewManager *manager = nullptr;
         std::vector roomids;
         QHash> models;
+        std::map roomReadStatus;
 };
-
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8df17457..19c3fb30 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -723,6 +723,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 +880,18 @@ 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 manager_->updateRoomsLastMessage(room_id_, lastMessage_);
+                                emit lastMessageChanged();
+                        }
                         return;
                 }
                 if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
@@ -884,7 +902,11 @@ 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 manager_->updateRoomsLastMessage(room_id_, description);
+                        emit lastMessageChanged();
+                }
                 return;
         }
 }
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 92fccd2d..5c1065cb 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); }
@@ -309,6 +313,9 @@ public slots:
         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 +335,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 +382,11 @@ private:
         QString eventIdToShow;
         int showEventTimerCounter = 0;
 
+        DescInfo lastMessage_;
+
         friend struct SendMessageVisitor;
+
+        int notification_count = 0, highlight_count = 0;
 };
 
 template
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index ca2a4ce0..b6c9579a 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -16,14 +16,15 @@ Theme::paletteFromTheme(std::string_view theme)
                   /*windowText*/ QColor("#333"),
                   /*button*/ QColor("white"),
                   /*light*/ QColor(0xef, 0xef, 0xef),
-                  /*dark*/ QColor(110, 110, 110),
+                  /*dark*/ QColor(70, 77, 93),
                   /*mid*/ QColor(220, 220, 220),
                   /*text*/ QColor("#333"),
-                  /*bright_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"));
@@ -34,14 +35,15 @@ Theme::paletteFromTheme(std::string_view theme)
                   /*windowText*/ QColor("#caccd1"),
                   /*button*/ QColor(0xff, 0xff, 0xff),
                   /*light*/ QColor("#caccd1"),
-                  /*dark*/ QColor(110, 110, 110),
+                  /*dark*/ QColor(60, 70, 77),
                   /*mid*/ QColor("#202228"),
                   /*text*/ QColor("#caccd1"),
-                  /*bright_text*/ QColor(0xff, 0xff, 0xff),
+                  /*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"));
@@ -58,9 +60,12 @@ Theme::Theme(std::string_view theme)
         separator_ = p.mid().color();
         if (theme == "light") {
                 sidebarBackground_ = QColor("#233649");
+                red_               = QColor("#a82353");
         } else if (theme == "dark") {
                 sidebarBackground_ = QColor("#2d3139");
+                red_               = QColor("#a82353");
         } else {
                 sidebarBackground_ = p.window().color();
+                red_               = QColor("red");
         }
 }
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 64bc8273..834571c0 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -66,6 +66,7 @@ class Theme : public QPalette
         Q_GADGET
         Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT)
         Q_PROPERTY(QColor separator READ separator CONSTANT)
+        Q_PROPERTY(QColor red READ red CONSTANT)
 public:
         Theme() {}
         explicit Theme(std::string_view theme);
@@ -73,7 +74,8 @@ public:
 
         QColor sidebarBackground() const { return sidebarBackground_; }
         QColor separator() const { return separator_; }
+        QColor red() const { return red_; }
 
 private:
-        QColor sidebarBackground_, separator_;
+        QColor sidebarBackground_, separator_, red_;
 };

From beeb60e4a12b47ae619e52629040aff5a8f43db2 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 22 May 2021 00:57:14 +0200
Subject: [PATCH 09/38] Sort the room list

---
 resources/qml/RoomList.qml           |  2 +-
 src/timeline/RoomlistModel.cpp       | 93 ++++++++++++++++++++++++++--
 src/timeline/RoomlistModel.h         | 26 ++++++++
 src/timeline/TimelineModel.cpp       |  2 +
 src/timeline/TimelineModel.h         |  2 +-
 src/timeline/TimelineViewManager.cpp |  4 +-
 6 files changed, 120 insertions(+), 9 deletions(-)

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index bb8deda6..f2a957c9 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -131,7 +131,7 @@ Page {
                             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             color: roomItem.unimportantText
-                            text: model.timestamp
+                            text: model.time
                         }
 
                     }
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 5fc4dc65..afe9679a 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -42,10 +42,13 @@ RoomlistModel::roleNames() const
           {RoomName, "roomName"},
           {RoomId, "roomId"},
           {LastMessage, "lastMessage"},
+          {Time, "time"},
           {Timestamp, "timestamp"},
           {HasUnreadMessages, "hasUnreadMessages"},
           {HasLoudNotification, "hasLoudNotification"},
           {NotificationCount, "notificationCount"},
+          {IsInvite, "isInvite"},
+          {IsSpace, "isSpace"},
         };
 }
 
@@ -64,8 +67,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         return room->roomId();
                 case Roles::LastMessage:
                         return room->lastMessage().body;
-                case Roles::Timestamp:
+                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);
@@ -73,6 +78,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         return room->hasMentions();
                 case Roles::NotificationCount:
                         return room->notificationCount();
+                case Roles::IsInvite:
+                case Roles::IsSpace:
+                        return false;
                 default:
                         return {};
                 }
@@ -90,9 +98,9 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_)
                 if (roomUnread != roomReadStatus[roomid]) {
                         roomsToUpdate.push_back(this->roomidToIndex(roomid));
                 }
-        }
 
-        this->roomReadStatus = roomReadStatus_;
+                this->roomReadStatus[roomid] = roomUnread;
+        }
 
         for (auto idx : roomsToUpdate) {
                 emit dataChanged(index(idx),
@@ -135,6 +143,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                                              Roles::LastMessage,
                                              Roles::Timestamp,
                                              Roles::NotificationCount,
+                                             Qt::DisplayRole,
                                            });
                   });
                 connect(
@@ -162,6 +171,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                                            {
                                              Roles::HasLoudNotification,
                                              Roles::NotificationCount,
+                                             Qt::DisplayRole,
                                            });
 
                           int total_unread_msgs = 0;
@@ -225,7 +235,6 @@ RoomlistModel::initializeRooms(const std::vector &roomIds_)
         beginResetModel();
         models.clear();
         roomids.clear();
-        roomids = roomIds_;
         for (const auto &id : roomIds_)
                 addRoom(id, true);
         endResetModel();
@@ -239,3 +248,79 @@ RoomlistModel::clear()
         roomids.clear();
         endResetModel();
 }
+
+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();
+                         });
+
+        sort(0);
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index c4c9d9ba..c3374bd2 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -24,10 +25,13 @@ public:
                 RoomName,
                 RoomId,
                 LastMessage,
+                Time,
                 Timestamp,
                 HasUnreadMessages,
                 HasLoudNotification,
                 NotificationCount,
+                IsInvite,
+                IsSpace,
         };
 
         RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -73,4 +77,26 @@ private:
         std::vector roomids;
         QHash> models;
         std::map roomReadStatus;
+
+        friend class FilteredRoomlistModel;
+};
+
+class FilteredRoomlistModel : public QSortFilterProxyModel
+{
+        Q_OBJECT
+public:
+        FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
+        bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+public slots:
+        int roomidToIndex(QString roomid)
+        {
+                return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid)))
+                  .row();
+        }
+
+private:
+        short int calculateImportance(const QModelIndex &idx) const;
+        RoomlistModel *roomlistmodel;
+        bool sortByImportance = true;
 };
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 19c3fb30..2625127c 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,
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 5c1065cb..b3d3b663 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -382,7 +382,7 @@ private:
         QString eventIdToShow;
         int showEventTimerCounter = 0;
 
-        DescInfo lastMessage_;
+        DescInfo lastMessage_{};
 
         friend struct SendMessageVisitor;
 
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b0c13b03..c84e0df8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -193,9 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = self->rooms;
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
+                  return new FilteredRoomlistModel(self->rooms);
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {

From f3d956aebcd34ecf8c4e2c44acd39d5de380233f Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 22 May 2021 10:16:42 +0200
Subject: [PATCH 10/38] Fix emoji in new RoomList

---
 resources/qml/ElidedLabel.qml      |  2 +-
 resources/qml/RoomList.qml         |  2 ++
 src/timeline/RoomlistModel.cpp     |  2 +-
 src/timeline/TimelineModel.cpp     | 11 +++++++++++
 src/timeline/TimelineModel.h       |  1 +
 src/timeline/TimelineViewManager.h |  1 +
 6 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml
index 5ae99de7..1f4aeeea 100644
--- a/resources/qml/ElidedLabel.qml
+++ b/resources/qml/ElidedLabel.qml
@@ -13,7 +13,7 @@ Label {
     property alias elideWidth: metrics.elideWidth
 
     color: Nheko.colors.text
-    text: metrics.elidedText
+    text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(TimelineManager.htmlEscape(metrics.elidedText))
     maximumLineCount: 1
     elide: Text.ElideRight
     textFormat: Text.PlainText
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index f2a957c9..89af78a5 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -119,6 +119,7 @@ Page {
                             color: roomItem.importantText
                             elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium
                             fullText: model.roomName
+                            textFormat: Text.RichText
                         }
 
                         Item {
@@ -146,6 +147,7 @@ Page {
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall
                             fullText: model.lastMessage
+                            textFormat: Text.RichText
                         }
 
                         Item {
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index afe9679a..6d741322 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -62,7 +62,7 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                 case Roles::AvatarUrl:
                         return room->roomAvatarUrl();
                 case Roles::RoomName:
-                        return room->roomName();
+                        return room->plainRoomName();
                 case Roles::RoomId:
                         return room->roomId();
                 case Roles::LastMessage:
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 2625127c..8f4a8564 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -1890,6 +1890,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 b3d3b663..3ebbe120 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -307,6 +307,7 @@ public slots:
         }
 
         QString roomName() const;
+        QString plainRoomName() const;
         QString roomTopic() const;
         InputBar *input() { return &input_; }
         Permissions *permissions() { return &permissions_; }
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index f4297243..609f5a4a 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -67,6 +67,7 @@ public:
         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;

From d307f24adf346ec3fe41d40a599db98d7eb435cf Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 22 May 2021 11:23:16 +0200
Subject: [PATCH 11/38] Fix leaving rooms

---
 resources/qml/RoomList.qml     |  2 +-
 src/timeline/RoomlistModel.cpp | 11 +++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 89af78a5..979e727d 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -46,7 +46,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered
+                    when: hovered.hovered && !(TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId())
 
                     PropertyChanges {
                         target: roomItem
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 6d741322..28c3cf46 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -227,6 +227,17 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms)
                         }
                 }
         }
+
+        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);
+                        models.remove(QString::fromStdString(room_id));
+                        endRemoveRows();
+                }
+        }
 }
 
 void

From f2bc1845508cc2d90540404b6b9e11192cc56104 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 22 May 2021 14:31:38 +0200
Subject: [PATCH 12/38] Fix device list not showing up and UserProfile blocking
 the window

---
 resources/qml/RoomList.qml                    | 10 ++++++
 resources/qml/RoomSettings.qml                |  4 +--
 resources/qml/UserProfile.qml                 |  2 +-
 .../DeviceVerification.qml                    |  2 +-
 src/Cache.cpp                                 | 32 +++++++++----------
 src/ui/NhekoGlobalObject.h                    |  2 +-
 src/ui/UserProfile.cpp                        |  4 ++-
 src/ui/UserProfile.h                          |  3 +-
 8 files changed, 35 insertions(+), 24 deletions(-)

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 979e727d..cde744c5 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -212,6 +212,16 @@ Page {
             Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
             Layout.minimumHeight: 40
 
+            TapHandler {
+                onSingleTapped: {
+                    Nheko.updateUserProfile();
+                    var userProfile = userProfileComponent.createObject(timelineRoot, {
+                        "profile": Nheko.currentUser
+                    });
+                    userProfile.show();
+                }
+            }
+
             RowLayout {
                 id: userInfoGrid
 
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 14de0edf..1f7fe5de 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -20,7 +20,7 @@ ApplicationWindow {
     minimumHeight: 650
     palette: Nheko.colors
     color: Nheko.colors.window
-    modality: Qt.WindowModal
+    modality: Qt.NonModal
     flags: Qt.Dialog
     title: qsTr("Room Settings")
 
@@ -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/UserProfile.qml b/resources/qml/UserProfile.qml index 4e5e64dc..21f34f15 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -22,7 +22,7 @@ ApplicationWindow { 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 { diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml index 6d0be204..e2c66c5a 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml @@ -15,7 +15,7 @@ ApplicationWindow { onClosing: TimelineManager.removeVerificationFlow(flow) title: stack.currentItem.title flags: Qt.Dialog - modality: Qt.WindowModal + modality: Qt.NonModal palette: Nheko.colors height: stack.implicitHeight width: stack.implicitWidth diff --git a/src/Cache.cpp b/src/Cache.cpp index d8c78381..c41b66cc 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3469,6 +3469,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) { @@ -3476,9 +3477,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); } } @@ -3552,6 +3552,19 @@ 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, this](const mtx::responses::QueryKeys &res, @@ -3565,21 +3578,6 @@ Cache::query_keys(const std::string &user_id, } emit userKeysUpdate(last_changed, res); - - // use context object so that we can disconnect again - std::unique_ptr context{new QObject}; - QObject *pcontext = context.get(); - QObject::connect( - this, - &Cache::verificationStatusChanged, - pcontext, - [cb, user_id, context_ = std::move(context)](std::string updated_user) mutable { - if (user_id == updated_user) { - context_.release(); - auto keys = cache::userKeys(user_id); - cb(keys.value_or(UserKeyCache{}), {}); - } - }); }); } diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index fe645a34..fc35fe22 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -40,7 +40,7 @@ public: Q_INVOKABLE void openLink(QString link) const; -private slots: +public slots: void updateUserProfile(); signals: diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index cef8bd85..da130242 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -42,7 +42,6 @@ UserProfile::UserProfile(QString roomid, if (!cache::client() || !cache::client()->isDatabaseReady()) return; - fetchDeviceList(this->userid_); connect(cache::client(), &Cache::verificationStatusChanged, this, @@ -66,7 +65,9 @@ UserProfile::UserProfile(QString roomid, : verification::VERIFIED; } deviceList_.reset(deviceList_.deviceList_); + emit devicesChanged(); }); + fetchDeviceList(this->userid_); } QHash @@ -223,6 +224,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(); From 6112badb087e424d6a6f82301922ccd2c93e178c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 22 May 2021 15:19:44 +0200 Subject: [PATCH 13/38] Reenable userInfo settings menu --- resources/qml/MatrixTextField.qml | 1 + resources/qml/RoomList.qml | 82 ++++++++++++++++++++++++++++--- src/ui/NhekoGlobalObject.cpp | 5 ++ src/ui/NhekoGlobalObject.h | 1 + 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index 2ba648b5..3c660bac 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -11,6 +11,7 @@ TextField { id: input palette: Nheko.colors + color: Nheko.colors.text Rectangle { id: blueBar diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index cde744c5..40669eda 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import Qt.labs.platform 1.1 as Platform import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.3 @@ -206,20 +207,87 @@ Page { 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 - TapHandler { - onSingleTapped: { - Nheko.updateUserProfile(); - var userProfile = userProfileComponent.createObject(timelineRoot, { - "profile": Nheko.currentUser - }); - userProfile.show(); + ApplicationWindow { + id: statusDialog + + modality: Qt.NonModal + flags: Qt.Dialog + title: qsTr("Status Message") + width: 350 + height: fontMetrics.lineSpacing * 7 + + ColumnLayout { + anchors.margins: Nheko.paddingLarge + anchors.fill: parent + + Label { + color: Nheko.colors.text + text: qsTr("Enter your status message:") + } + + MatrixTextField { + id: statusInput + + Layout.fillWidth: true + } + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + onAccepted: { + Nheko.setStatusMessage(statusInput.text); + statusDialog.close(); + } + onRejected: { + statusDialog.close(); + } + } + + } + + Platform.Menu { + id: userInfoMenu + + Platform.MenuItem { + text: qsTr("Profile settings") + onTriggered: userInfoPanel.openUserProfile() + } + + Platform.MenuItem { + text: qsTr("Set status message") + onTriggered: statusDialog.show() + } + + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onSingleTapped: userInfoPanel.openUserProfile() + onLongPressed: userInfoMenu.open() + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: userInfoMenu.open() + gesturePolicy: TapHandler.ReleaseWithinBounds } RowLayout { diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index 70abfbb8..fd572b4b 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -100,6 +100,11 @@ Nheko::openLink(QString link) const QDesktopServices::openUrl(url); } } +void +Nheko::setStatusMessage(QString msg) const +{ + ChatPage::instance()->setStatus(msg); +} UserProfile * Nheko::currentUser() const diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index fc35fe22..593514fa 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -39,6 +39,7 @@ public: UserProfile *currentUser() const; Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void setStatusMessage(QString msg) const; public slots: void updateUserProfile(); From c290b0747f34a6f683365f93d64ce93dc4428ca8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 24 May 2021 14:04:07 +0200 Subject: [PATCH 14/38] Reenable invites --- resources/qml/RoomList.qml | 56 +++++++++ src/Cache.cpp | 50 ++++++-- src/Cache.h | 2 +- src/Cache_p.h | 3 +- src/ChatPage.cpp | 4 +- src/ChatPage.h | 2 +- src/RoomList.cpp | 2 +- src/RoomList.h | 2 +- src/timeline/RoomlistModel.cpp | 169 ++++++++++++++++++++++----- src/timeline/RoomlistModel.h | 8 +- src/timeline/TimelineViewManager.cpp | 4 +- src/timeline/TimelineViewManager.h | 2 +- src/ui/Theme.cpp | 3 + src/ui/Theme.h | 4 +- 14 files changed, 260 insertions(+), 51 deletions(-) diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 40669eda..e9bb351f 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -141,6 +141,8 @@ Page { RowLayout { Layout.fillWidth: true spacing: 0 + visible: !model.isInvite + height: visible ? 0 : undefined ElidedLabel { color: roomItem.unimportantText @@ -182,6 +184,60 @@ Page { } + 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 + } + + } + } } diff --git a/src/Cache.cpp b/src/Cache.cpp index c41b66cc..4a99dd59 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2045,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; } @@ -4064,7 +4100,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/Cache_p.h b/src/Cache_p.h index c55fa601..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); diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 58b76174..166c03ec 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -313,7 +313,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::initializeEmptyViews, view_manager_, - &TimelineViewManager::initWithMessages); + &TimelineViewManager::initializeRoomlist); connect(this, &ChatPage::initializeMentions, user_mentions_popup_, @@ -554,7 +554,7 @@ ChatPage::loadStateFromCache() try { olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - emit initializeEmptyViews(cache::client()->roomIds()); + emit initializeEmptyViews(); emit initializeRoomList(cache::roomInfo()); emit initializeMentions(cache::getTimelineMentions()); emit syncTags(cache::roomInfo().toStdMap()); diff --git a/src/ChatPage.h b/src/ChatPage.h index 84e7cdff..eb60047d 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -147,7 +147,7 @@ signals: 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); diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 5c41a7a1..5839c4a0 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -183,7 +183,7 @@ RoomList::initialize(const QMap &info) } void -RoomList::cleanupInvites(const std::map &invites) +RoomList::cleanupInvites(const QHash &invites) { if (invites.size() == 0) return; diff --git a/src/RoomList.h b/src/RoomList.h index 74152c55..af792fd7 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -48,7 +48,7 @@ public: //! 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); + void cleanupInvites(const QHash &invites); signals: void roomChanged(const QString &room_id); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 28c3cf46..f3d4dad7 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -57,31 +57,64 @@ RoomlistModel::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) { auto roomid = roomids.at(index.row()); - 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; - default: + + 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; + 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; + default: + return {}; + } + } else { return {}; } } else { @@ -109,7 +142,7 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_) Roles::HasUnreadMessages, }); } -}; +} void RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) { @@ -186,11 +219,21 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) newRoom->updateLastMessage(); - if (!suppressInsertNotification) + bool wasInvite = invites.contains(room_id); + if (!suppressInsertNotification && !wasInvite) beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + models.insert(room_id, std::move(newRoom)); - roomids.push_back(room_id); - if (!suppressInsertNotification) + + 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(); } } @@ -234,20 +277,50 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) if (idx != -1) { beginRemoveRows(QModelIndex(), idx, idx); roomids.erase(roomids.begin() + idx); - models.remove(QString::fromStdString(room_id)); + 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_id; + 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(const std::vector &roomIds_) +RoomlistModel::initializeRooms() { beginResetModel(); models.clear(); roomids.clear(); - for (const auto &id : roomIds_) + invites.clear(); + + 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(); } @@ -256,10 +329,42 @@ RoomlistModel::clear() { beginResetModel(); models.clear(); + invites.clear(); roomids.clear(); 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); + } + } +} + namespace { enum NotificationImportance : short { diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index c3374bd2..ff85614c 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -51,7 +52,7 @@ public: } public slots: - void initializeRooms(const std::vector &roomids); + void initializeRooms(); void sync(const mtx::responses::Rooms &rooms); void clear(); int roomidToIndex(QString roomid) @@ -63,6 +64,8 @@ public slots: return -1; } + void acceptInvite(QString roomid); + void declineInvite(QString roomid); private slots: void updateReadStatus(const std::map roomReadStatus_); @@ -75,6 +78,7 @@ private: TimelineViewManager *manager = nullptr; std::vector roomids; + QHash invites; QHash> models; std::map roomReadStatus; @@ -94,6 +98,8 @@ public slots: return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) .row(); } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } private: short int calculateImportance(const QModelIndex &idx) const; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index c84e0df8..9fa7f8b6 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -499,9 +499,9 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s } void -TimelineViewManager::initWithMessages(const std::vector &roomIds) +TimelineViewManager::initializeRoomlist() { - rooms->initializeRooms(roomIds); + rooms->initializeRooms(); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 609f5a4a..37e50804 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -100,7 +100,7 @@ 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; diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index b6c9579a..26119393 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -60,12 +60,15 @@ Theme::Theme(std::string_view 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 834571c0..b5bcd4dd 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -65,6 +65,7 @@ class Theme : public QPalette { 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: @@ -73,9 +74,10 @@ public: static QPalette paletteFromTheme(std::string_view theme); QColor sidebarBackground() const { return sidebarBackground_; } + QColor alternateButton() const { return alternateButton_; } QColor separator() const { return separator_; } QColor red() const { return red_; } private: - QColor sidebarBackground_, separator_, red_; + QColor sidebarBackground_, separator_, red_, alternateButton_; }; From e2765212fb229e8d025d2255314a04a376207749 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 28 May 2021 17:25:46 +0200 Subject: [PATCH 15/38] Reimplement room context menus --- resources/qml/RoomList.qml | 132 ++++++++++++++++++------ resources/qml/delegates/TextMessage.qml | 2 +- resources/qml/dialogs/InputDialog.qml | 53 ++++++++++ resources/res.qrc | 1 + src/timeline/RoomlistModel.cpp | 72 +++++++++++++ src/timeline/RoomlistModel.h | 6 ++ 6 files changed, 232 insertions(+), 34 deletions(-) create mode 100644 resources/qml/dialogs/InputDialog.qml diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index e9bb351f..b184aef0 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "./dialogs" import Qt.labs.platform 1.1 as Platform import QtQuick 2.13 import QtQuick.Controls 2.13 @@ -31,6 +32,93 @@ Page { target: TimelineManager } + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + roomContextMenu.clear(); + roomContextMenu.addItem(leaveOpt.createObject(roomContextMenu)); + roomContextMenu.addItem(separatorOpt.createObject(roomContextMenu)); + for (let tag of Rooms.tags()) { + roomContextMenu.addItem(tagDelegate.createObject(roomContextMenu, { + "t": tag + })); + } + roomContextMenu.addItem(newTagOpt.createObject(roomContextMenu)); + 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); + } + } + + Component { + id: leaveOpt + + Platform.MenuItem { + text: qsTr("Leave room") + onTriggered: Rooms.leave(roomContextMenu.roomid) + } + + } + + Component { + id: separatorOpt + + Platform.MenuSeparator { + text: qsTr("Tag room as:") + } + + } + + Component { + id: tagDelegate + + Platform.MenuItem { + property string t + + text: { + switch (t) { + case "m.favourite": + return qsTr("Favourite"); + case "m.lowpriority": + return qsTr("Low priority"); + case "m.server_notice": + return qsTr("Server notice"); + default: + return t.substring(2); + } + } + checkable: true + checked: roomContextMenu.tags.includes(t) + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + } + + Component { + id: newTagOpt + + Platform.MenuItem { + text: qsTr("Create new tag...") + onTriggered: newTag.show() + } + + } + + } + delegate: Rectangle { id: roomItem @@ -75,6 +163,12 @@ Page { } ] + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: roomContextMenu.show(model.roomId, model.tags) + gesturePolicy: TapHandler.ReleaseWithinBounds + } + HoverHandler { id: hovered } @@ -94,6 +188,7 @@ Page { id: avatar + enabled: false Layout.alignment: Qt.AlignVCenter height: Math.ceil(fontMetrics.lineSpacing * 2.3) width: Math.ceil(fontMetrics.lineSpacing * 2.3) @@ -279,43 +374,14 @@ Page { Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium Layout.minimumHeight: 40 - ApplicationWindow { + InputDialog { id: statusDialog - modality: Qt.NonModal - flags: Qt.Dialog title: qsTr("Status Message") - width: 350 - height: fontMetrics.lineSpacing * 7 - - ColumnLayout { - anchors.margins: Nheko.paddingLarge - anchors.fill: parent - - Label { - color: Nheko.colors.text - text: qsTr("Enter your status message:") - } - - MatrixTextField { - id: statusInput - - Layout.fillWidth: true - } - + prompt: qsTr("Enter your status message:") + onAccepted: function(text) { + Nheko.setStatusMessage(text); } - - footer: DialogButtonBox { - standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel - onAccepted: { - Nheko.setStatusMessage(statusInput.text); - statusDialog.close(); - } - onRejected: { - statusDialog.close(); - } - } - } Platform.Menu { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index ae622480..f65eda79 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -9,7 +9,7 @@ MatrixText { property string formatted: model.data.formattedBody property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body - text: "" + formatted.replace("
", "
")
+    text: "" + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
     width: parent ? parent.width : undefined
     height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
new file mode 100644
index 00000000..0cd6be1c
--- /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.13
+import QtQuick.Controls 2.13
+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/res.qrc b/resources/res.qrc
index 79e63810..183cf394 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -168,6 +168,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
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f3d4dad7..63054aa9 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -49,6 +49,7 @@ RoomlistModel::roleNames() const
           {NotificationCount, "notificationCount"},
           {IsInvite, "isInvite"},
           {IsSpace, "isSpace"},
+          {Tags, "tags"},
         };
 }
 
@@ -84,6 +85,13 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         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 {};
                         }
@@ -111,6 +119,8 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                                 return true;
                         case Roles::IsSpace:
                                 return false;
+                        case Roles::Tags:
+                                return QStringList();
                         default:
                                 return {};
                         }
@@ -364,6 +374,21 @@ RoomlistModel::declineInvite(QString 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);
+                }
+        }
+}
 
 namespace {
 enum NotificationImportance : short
@@ -440,3 +465,50 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
 
         sort(0);
 }
+
+QStringList
+FilteredRoomlistModel::tags()
+{
+        std::set ts;
+        for (const auto &e : cache::roomInfo()) {
+                for (const auto &t : e.tags) {
+                        if (t.find("u.") == 0) {
+                                ts.insert(t);
+                        }
+                }
+        }
+
+        QStringList ret{{
+          "m.favourite",
+          "m.lowpriority",
+        }};
+
+        for (const auto &t : ts)
+                ret.push_back(QString::fromStdString(t));
+
+        return ret;
+}
+
+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);
+                          }
+                  });
+        }
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index ff85614c..2d1e5264 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -33,6 +34,7 @@ public:
                 NotificationCount,
                 IsInvite,
                 IsSpace,
+                Tags,
         };
 
         RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -66,6 +68,7 @@ public slots:
         }
         void acceptInvite(QString roomid);
         void declineInvite(QString roomid);
+        void leave(QString roomid);
 
 private slots:
         void updateReadStatus(const std::map roomReadStatus_);
@@ -100,6 +103,9 @@ public slots:
         }
         void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
         void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
+        void leave(QString roomid) { roomlistmodel->leave(roomid); }
+        QStringList tags();
+        void toggleTag(QString roomid, QString tag, bool on);
 
 private:
         short int calculateImportance(const QModelIndex &idx) const;

From 298822baeaffdc83386e003099e34819bcd7d18c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 28 May 2021 22:14:59 +0200
Subject: [PATCH 16/38] Move currentRoom/timeline handling to roomlist

---
 resources/qml/ChatPage.qml                    |   1 +
 resources/qml/Completer.qml                   |   6 +-
 resources/qml/ForwardCompleter.qml            |   4 +-
 resources/qml/MessageInput.qml                |  62 ++++----
 resources/qml/MessageView.qml                 | 140 ++++++++++++++++-
 resources/qml/QuickSwitcher.qml               |   3 +-
 resources/qml/Reactions.qml                   |   2 +-
 resources/qml/ReplyPopup.qml                  |   2 -
 resources/qml/RoomList.qml                    |  21 ++-
 resources/qml/Root.qml                        | 147 ------------------
 resources/qml/StatusIndicator.qml             |   2 +-
 resources/qml/TimelineView.qml                |  22 ++-
 resources/qml/TopBar.qml                      |  14 +-
 resources/qml/TypingIndicator.qml             |   2 -
 resources/qml/delegates/FileMessage.qml       |   2 +-
 resources/qml/delegates/MessageDelegate.qml   |  10 +-
 .../qml/delegates/PlayableMediaMessage.qml    |   4 +-
 resources/qml/emoji/EmojiButton.qml           |   2 +-
 resources/qml/voip/ActiveCallBar.qml          |   2 +-
 resources/qml/voip/CallInviteBar.qml          |   2 +-
 resources/qml/voip/PlaceCall.qml              |  12 +-
 resources/qml/voip/ScreenShare.qml            |   4 +-
 src/ChatPage.cpp                              |   7 +-
 src/timeline/InputBar.cpp                     |  35 ++++-
 src/timeline/InputBar.h                       |   1 +
 src/timeline/RoomlistModel.cpp                |  18 +++
 src/timeline/RoomlistModel.h                  |  16 +-
 src/timeline/TimelineViewManager.cpp          | 123 +++++----------
 src/timeline/TimelineViewManager.h            |  26 +---
 src/ui/NhekoDropArea.cpp                      |   2 +-
 30 files changed, 349 insertions(+), 345 deletions(-)

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index fc6137a6..966f169b 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -31,6 +31,7 @@ Rectangle {
 
         TimelineView {
             id: timeline
+            room: Rooms.currentRoom
 
             SplitView.fillWidth: true
             SplitView.minimumWidth: 400
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 2609371b..0cdd789d 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 {
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 1ec18540..eee3879c 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -50,7 +50,7 @@ Popup {
         Reply {
             id: replyPreview
 
-            modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : {
+            modelData: room ? room.getDump(mid, "") : {
             }
             userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
         }
@@ -95,7 +95,7 @@ Popup {
 
     Connections {
         onCompletionSelected: {
-            TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id);
+            room.forwardMessage(messageContextMenu.eventId, id);
             forwardMessagePopup.close();
         }
         onCountChanged: {
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index f4e253ad..24f9b0e8 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -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: Nheko.colors.window
-                visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading
+                visible: room && room.input.uploading
 
                 NhekoBusyIndicator {
                     anchors.fill: parent
@@ -123,16 +123,16 @@ Rectangle {
                 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,7 +355,7 @@ 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: Nheko.colors.text
     }
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 5af4e4de..176905db 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
@@ -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
@@ -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("React")
+            onTriggered: emojiPopup.show(null, function(emoji) {
+                room.input.reaction(messageContextMenu.eventId, emoji);
+            })
+        }
+
+        Platform.MenuItem {
+            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+            text: qsTr("Reply")
+            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 receipts")
+            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("Remove 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 event")
+            onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
+        }
+
+    }
+
+    Component {
+        id: forwardCompleterComponent
+
+        ForwardCompleter {
+        }
+
+    }
+
 }
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index a6373b1c..8c4f47ca 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -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 064df543..def87f75 100644
--- a/resources/qml/Reactions.qml
+++ b/resources/qml/Reactions.qml
@@ -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 {
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 1d85acb0..0de68fe8 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -11,8 +11,6 @@ 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
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index b184aef0..c5e07032 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -149,7 +149,7 @@ Page {
                 },
                 State {
                     name: "selected"
-                    when: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId()
+                    when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()
 
                     PropertyChanges {
                         target: roomItem
@@ -165,18 +165,27 @@ Page {
 
             TapHandler {
                 acceptedButtons: Qt.RightButton
-                onSingleTapped: roomContextMenu.show(model.roomId, model.tags)
+                onSingleTapped: {
+                    if (!TimelineManager.isInvite) {
+                        roomContextMenu.show(model.roomId, model.tags);
+                    }
+                }
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
+            TapHandler {
+                onSingleTapped: Rooms.setCurrentRoom(model.roomId)
+                onLongPressed: {
+                    if (!TimelineManager.isInvite) {
+                        roomContextMenu.show(model.roomId, model.tags);
+                    }
+                }
+            }
+
             HoverHandler {
                 id: hovered
             }
 
-            TapHandler {
-                onSingleTapped: TimelineManager.setHistoryView(model.roomId)
-            }
-
             RowLayout {
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 35b81a1f..a8b6fa52 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -63,14 +63,6 @@ Page {
 
     }
 
-    Component {
-        id: forwardCompleterComponent
-
-        ForwardCompleter {
-        }
-
-    }
-
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -80,135 +72,6 @@ Page {
         }
     }
 
-    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("React")
-            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("Reply")
-            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 receipts")
-            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("Remove 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 event")
-            onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
-        }
-
-    }
-
     Component {
         id: deviceVerificationDialog
 
@@ -233,16 +96,6 @@ Page {
         }
     }
 
-    Connections {
-        target: TimelineManager.timeline
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
-    }
-
     Connections {
         target: CallManager
         onNewInviteState: {
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/TimelineView.qml b/resources/qml/TimelineView.qml
index 257d670d..747be61e 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -18,8 +18,10 @@ import im.nheko.EmojiModel 1.0
 Item {
     id: timelineView
 
+    property var room: null
+
     Label {
-        visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
+        visible: !room && !TimelineManager.isInitialSync
         anchors.centerIn: parent
         text: qsTr("No room open")
         font.pointSize: 24
@@ -38,7 +40,7 @@ Item {
     ColumnLayout {
         id: timelineLayout
 
-        visible: TimelineManager.timeline != null
+        visible: room != null
         anchors.fill: parent
         spacing: 0
 
@@ -69,11 +71,11 @@ Item {
                     currentIndex: 0
 
                     Connections {
-                        function onActiveTimelineChanged() {
+                        function onRoomChanged() {
                             stackLayout.currentIndex = 0;
                         }
 
-                        target: TimelineManager
+                        target: timelineView
                     }
 
                     MessageView {
@@ -125,7 +127,17 @@ Item {
 
     NhekoDropArea {
         anchors.fill: parent
-        roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
+        roomid: room ? room.roomId() : ""
+    }
+
+    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 bda5ce14..65e27939 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -11,8 +11,6 @@ import im.nheko 1.0
 Rectangle {
     id: topBar
 
-    property var room: TimelineManager.timeline
-
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
@@ -20,7 +18,7 @@ Rectangle {
 
     TapHandler {
         onSingleTapped: {
-            TimelineManager.timeline.openRoomSettings();
+            room.openRoomSettings();
             eventPoint.accepted = true;
         }
         gesturePolicy: TapHandler.ReleaseWithinBounds
@@ -61,7 +59,7 @@ Rectangle {
             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 {
@@ -101,24 +99,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 783a9ebc..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
 
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 2e5f33c2..0392c73a 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -34,7 +34,7 @@ Item {
             }
 
             TapHandler {
-                onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
+                onSingleTapped: room.saveMedia(model.data.id)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 4e6a73fe..9e076a7a 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -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/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 0234495d..83864db9 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -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;
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/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 5798433a..3106c382 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -35,7 +35,7 @@ Rectangle {
             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/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index a169aca9..2d8e3040 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -42,7 +42,7 @@ Rectangle {
             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/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 7e2146cb..97e39e02 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -45,7 +45,7 @@ Popup {
             Layout.leftMargin: 8
 
             Label {
-                text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName)
+                text: qsTr("Place a call to %1?").arg(room.roomName)
                 color: Nheko.colors.windowText
             }
 
@@ -77,9 +77,9 @@ Popup {
                 Layout.rightMargin: cameraCombo.visible ? 16 : 64
                 width: Nheko.avatarSize
                 height: Nheko.avatarSize
-                url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
-                displayName: TimelineManager.timeline.roomName
-                onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+                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();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index 258ac9b0..a10057b2 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -27,7 +27,7 @@ Popup {
             Layout.leftMargin: 8
             Layout.rightMargin: 8
             Layout.alignment: Qt.AlignLeft
-            text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
+            text: qsTr("Share desktop with %1?").arg(room.roomName)
             color: Nheko.colors.windowText
         }
 
@@ -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();
                     }
                 }
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 166c03ec..bee20d60 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -215,8 +215,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 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);
@@ -982,7 +980,7 @@ ChatPage::leaveRoom(const QString &room_id)
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->setHistoryView(room_id);
+        view_manager_->rooms()->setCurrentRoom(room_id);
         room_list_->highlightSelectedRoom(room_id);
 }
 
@@ -1397,7 +1395,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);
                 }
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..a283d24e 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -508,8 +508,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 +714,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
index 63054aa9..ad4177a4 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -341,6 +341,8 @@ RoomlistModel::clear()
         models.clear();
         invites.clear();
         roomids.clear();
+        currentRoom_ = nullptr;
+        emit currentRoomChanged();
         endResetModel();
 }
 
@@ -390,6 +392,17 @@ RoomlistModel::leave(QString 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
 {
@@ -463,6 +476,11 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
                                  invalidate();
                          });
 
+        connect(roomlistmodel,
+                &RoomlistModel::currentRoomChanged,
+                this,
+                &FilteredRoomlistModel::currentRoomChanged);
+
         sort(0);
 }
 
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 2d1e5264..1c6fa833 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -14,12 +14,14 @@
 
 #include 
 
-class TimelineModel;
+#include "TimelineModel.h"
+
 class TimelineViewManager;
 
 class RoomlistModel : public QAbstractListModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         enum Roles
         {
@@ -69,12 +71,15 @@ public slots:
         void acceptInvite(QString roomid);
         void declineInvite(QString roomid);
         void leave(QString roomid);
+        TimelineModel *currentRoom() const { return currentRoom_.get(); }
+        void setCurrentRoom(QString roomid);
 
 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);
@@ -85,12 +90,15 @@ private:
         QHash> models;
         std::map roomReadStatus;
 
+        QSharedPointer currentRoom_;
+
         friend class FilteredRoomlistModel;
 };
 
 class FilteredRoomlistModel : public QSortFilterProxyModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
         bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
@@ -107,6 +115,12 @@ public slots:
         QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
+        TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
+        void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+
+signals:
+        void currentRoomChanged();
+
 private:
         short int calculateImportance(const QModelIndex &idx) const;
         RoomlistModel *roomlistmodel;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 9fa7f8b6..3b3ea423 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -133,7 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
-  , rooms(new RoomlistModel(this))
+  , rooms_(new RoomlistModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -193,7 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms);
+                  return new FilteredRoomlistModel(self->rooms_);
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
@@ -320,9 +320,9 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
-        this->rooms->sync(rooms_);
+        this->rooms_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -330,37 +330,17 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
         }
 }
 
-void
-TimelineViewManager::setHistoryView(const QString &room_id)
-{
-        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
-
-        if (auto room = rooms->getRoomById(room_id)) {
-                timeline_ = room.get();
-                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)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
-                if (timeline_ != room) {
-                        timeline_ = room.get();
-                        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);
         }
 }
 
@@ -395,17 +375,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
 
         auto imgDialog = new dialogs::ImageOverlay(pixmap);
         imgDialog->showFullScreen();
-        connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
-                // hide the overlay while presenting the save dialog for better
-                // cross platform support.
-                imgDialog->hide();
 
-                if (!timeline_->saveMedia(eventId)) {
-                        imgDialog->show();
-                } else {
-                        imgDialog->close();
-                }
-        });
+        auto room = rooms_->currentRoom();
+        connect(
+          imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
+                  // hide the overlay while presenting the save dialog for better
+                  // cross platform support.
+                  imgDialog->hide();
+
+                  if (!room->saveMedia(eventId)) {
+                          imgDialog->show();
+                  } else {
+                          imgDialog->close();
+                  }
+          });
 }
 
 void
@@ -415,14 +398,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
@@ -439,7 +422,7 @@ TimelineViewManager::verifyUser(QString userid)
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
                                 if (auto model =
-                                      rooms->getRoomById(QString::fromStdString(room_id))) {
+                                      rooms_->getRoomById(QString::fromStdString(room_id))) {
                                         auto flow =
                                           DeviceVerificationFlow::InitiateUserVerification(
                                             this, model.data(), userid);
@@ -485,7 +468,7 @@ void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &event_ids)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
+        if (auto room = rooms_->getRoomById(room_id)) {
                 room->markEventsAsRead(event_ids);
         }
 }
@@ -493,7 +476,7 @@ TimelineViewManager::updateReadReceipts(const QString &room_id,
 void
 TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        if (auto room = rooms->getRoomById(QString::fromStdString(room_id))) {
+        if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
                 room->receivedSessionKey(session_id);
         }
 }
@@ -501,7 +484,7 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s
 void
 TimelineViewManager::initializeRoomlist()
 {
-        rooms->initializeRooms();
+        rooms_->initializeRooms();
 }
 
 void
@@ -509,51 +492,17 @@ TimelineViewManager::queueReply(const QString &roomid,
                                 const QString &repliedToEvent,
                                 const QString &replyBody)
 {
-        if (auto room = rooms->getRoomById(roomid)) {
+        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)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
 }
 
@@ -561,7 +510,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallCandidates &callCandidates)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
 }
 
@@ -569,7 +518,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallAnswer &callAnswer)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
 }
 
@@ -577,7 +526,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallHangUp &callHangUp)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
 
@@ -629,7 +578,7 @@ void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
 {
-        auto room                                                = rooms->getRoomById(roomId);
+        auto room                                                = rooms_->getRoomById(roomId);
         auto content                                             = mtx::accessors::url(*e);
         std::optional encryptionInfo = mtx::accessors::file(*e);
 
@@ -672,7 +621,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                                               ev.content.url = url;
                                                       }
 
-                                                      if (auto room = rooms->getRoomById(roomId)) {
+                                                      if (auto room = rooms_->getRoomById(roomId)) {
                                                               removeReplyFallback(ev);
                                                               ev.content.relations.relations
                                                                 .clear();
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 37e50804..c4707208 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -35,8 +35,6 @@ 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(
@@ -53,14 +51,8 @@ public:
         MxcImageProvider *imageProvider() { return imgProvider; }
         CallManager *callManager() { return callManager_; }
 
-        void clearAll()
-        {
-                timeline_ = nullptr;
-                emit activeTimelineChanged(nullptr);
-                rooms->clear();
-        }
+        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_; }
@@ -74,8 +66,8 @@ public:
 
         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);
@@ -107,20 +99,13 @@ public slots:
                 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)
-        {
-                return rooms->getRoomById(room_id).get();
-        }
 
         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 &);
@@ -147,6 +132,8 @@ public slots:
         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);
 
@@ -162,14 +149,13 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
         bool isInitialSync_   = true;
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms = nullptr;
+        RoomlistModel *rooms_ = nullptr;
 
         QHash userColors;
 
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());
                 }

From 03d30a2abc6a2c9c9e1eaecc5a611b70e3041066 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 28 May 2021 23:25:57 +0200
Subject: [PATCH 17/38] Delete old room list

---
 CMakeLists.txt                  |  20 --
 resources/qml/Root.qml          |   9 +
 src/ChatPage.cpp                | 309 ++----------------
 src/ChatPage.h                  |  63 +---
 src/CommunitiesList.cpp         | 345 --------------------
 src/CommunitiesList.h           |  65 ----
 src/CommunitiesListItem.cpp     | 201 ------------
 src/CommunitiesListItem.h       | 107 -------
 src/MainWindow.cpp              |  34 --
 src/MainWindow.h                |   4 -
 src/RoomInfoListItem.cpp        | 522 -------------------------------
 src/RoomInfoListItem.h          | 210 -------------
 src/RoomList.cpp                | 535 --------------------------------
 src/RoomList.h                  | 101 ------
 src/popups/PopupItem.cpp        |  89 ------
 src/popups/PopupItem.h          |  66 ----
 src/popups/SuggestionsPopup.cpp | 164 ----------
 src/popups/SuggestionsPopup.h   |  53 ----
 src/popups/UserMentions.cpp     | 178 -----------
 src/popups/UserMentions.h       |  49 ---
 src/timeline/InputBar.cpp       |   1 +
 src/timeline/RoomlistModel.cpp  |  30 ++
 src/timeline/RoomlistModel.h    |   3 +
 23 files changed, 76 insertions(+), 3082 deletions(-)
 delete mode 100644 src/CommunitiesList.cpp
 delete mode 100644 src/CommunitiesList.h
 delete mode 100644 src/CommunitiesListItem.cpp
 delete mode 100644 src/CommunitiesListItem.h
 delete mode 100644 src/RoomInfoListItem.cpp
 delete mode 100644 src/RoomInfoListItem.h
 delete mode 100644 src/RoomList.cpp
 delete mode 100644 src/RoomList.h
 delete mode 100644 src/popups/PopupItem.cpp
 delete mode 100644 src/popups/PopupItem.h
 delete mode 100644 src/popups/SuggestionsPopup.cpp
 delete mode 100644 src/popups/SuggestionsPopup.h
 delete mode 100644 src/popups/UserMentions.cpp
 delete mode 100644 src/popups/UserMentions.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8b43559f..5a5e3ba1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -311,8 +311,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
@@ -324,22 +322,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
@@ -535,8 +525,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
@@ -544,21 +532,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/qml/Root.qml b/resources/qml/Root.qml
index a8b6fa52..c23ab97d 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -72,6 +72,15 @@ Page {
         }
     }
 
+    Shortcut {
+        sequence: "Ctrl+Down"
+        onActivated: Rooms.nextRoom();
+    }
+    Shortcut {
+        sequence: "Ctrl+Up"
+        onActivated: Rooms.previousRoom();
+    }
+
     Component {
         id: deviceVerificationDialog
 
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index bee20d60..4ad7bd14 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,29 +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::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(
-          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);
@@ -248,60 +154,23 @@ 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(userSettings_.data(),
-                &UserSettings::groupViewStateChanged,
-                this,
-                &ChatPage::setGroupViewState);
-
-        connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize);
         connect(
           this,
           &ChatPage::initializeViews,
@@ -312,30 +181,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 &ChatPage::initializeEmptyViews,
                 view_manager_,
                 &TimelineViewManager::initializeRoomlist);
-        connect(this,
-                &ChatPage::initializeMentions,
-                user_mentions_popup_,
-                &popups::UserMentions::initializeMentions);
         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;
                 }
@@ -358,16 +210,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);
@@ -420,8 +262,6 @@ ChatPage::dropToLoginPage(const QString &msg)
 void
 ChatPage::resetUI()
 {
-        room_list_->clear();
-        user_info_widget_->reset();
         view_manager_->clearAll();
 
         emit unreadMessages(0);
@@ -474,9 +314,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,
@@ -553,9 +390,7 @@ ChatPage::loadStateFromCache()
                 olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
 
                 emit initializeEmptyViews();
-                emit initializeRoomList(cache::roomInfo());
                 emit initializeMentions(cache::getTimelineMentions());
-                emit syncTags(cache::roomInfo().toStdMap());
 
                 cache::calculateRoomReadStatus();
 
@@ -593,38 +428,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
@@ -672,18 +475,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()
 {
@@ -782,11 +573,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());
@@ -823,12 +612,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;
@@ -932,7 +717,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));
           });
 }
 
@@ -981,18 +766,17 @@ void
 ChatPage::changeRoom(const QString &room_id)
 {
         view_manager_->rooms()->setCurrentRoom(room_id);
-        room_list_->highlightSelectedRoom(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;
 
@@ -1014,12 +798,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;
 
@@ -1041,12 +825,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;
 
@@ -1068,12 +852,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;
 
@@ -1175,51 +959,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);
-          });
-}
-
-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
@@ -1305,7 +1044,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;
                         }
                 }
@@ -1406,7 +1146,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;
@@ -1424,7 +1164,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(
@@ -1446,8 +1186,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 eb60047d..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();
         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();
@@ -207,65 +186,31 @@ private:
         void getProfileInfo();
 
         //! Check if the given room is currently open.
-        bool isRoomActive(const QString &room_id)
-        {
-                return isActiveWindow() && currentRoom() == room_id;
-        }
+        bool isRoomActive(const QString &room_id);
 
         using UserID      = QString;
         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 12b275b0..00000000
--- a/src/CommunitiesList.h
+++ /dev/null
@@ -1,65 +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"
-
-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 e7468611..00000000
--- a/src/CommunitiesListItem.h
+++ /dev/null
@@ -1,107 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-
-#include "Config.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/MainWindow.cpp b/src/MainWindow.cpp
index e2b625b0..057ee4af 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -109,10 +109,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 +172,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 +185,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()
 {
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 69d07e62..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);
 
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 5839c4a0..00000000
--- a/src/RoomList.cpp
+++ /dev/null
@@ -1,535 +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 QHash &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)) {
-                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)) {
-                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 af792fd7..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 QHash &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/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/InputBar.cpp b/src/timeline/InputBar.cpp
index a283d24e..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"
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index ad4177a4..9f926d2b 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -530,3 +530,33 @@ FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
                   });
         }
 }
+
+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
index 1c6fa833..d3e1e1f9 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -118,6 +118,9 @@ public slots:
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
         void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
 
+        void nextRoom();
+        void previousRoom();
+
 signals:
         void currentRoomChanged();
 

From 2174f6507face6ec322a5115a40a0e722703b29e Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 28 May 2021 23:50:04 +0200
Subject: [PATCH 18/38] Fix warning

---
 src/timeline/RoomlistModel.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 9f926d2b..d2ba0dc3 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -296,7 +296,7 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms)
         }
 
         for (const auto &[room_id, room] : rooms.invite) {
-                (void)room_id;
+                (void)room;
                 auto qroomid = QString::fromStdString(room_id);
 
                 auto invite = cache::client()->invite(room_id);

From 18ff58edb3bc186e2114efad34de7ffca803be02 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 00:23:57 +0200
Subject: [PATCH 19/38] Fix use after free from Qml widget

---
 src/ChatPage.cpp                     | 8 ++++++++
 src/timeline/RoomlistModel.cpp       | 3 ++-
 src/timeline/TimelineViewManager.cpp | 5 +++--
 3 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 4ad7bd14..0f16f205 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -171,6 +171,14 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                         activateWindow();
                 });
 
+        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+                // ensure the qml context is shutdown before we destroy all other singletons
+                // Otherwise Qml will try to access the room list or settings, after they have been
+                // destroyed
+                topLayout_->removeWidget(view_manager_->getWidget());
+                delete view_manager_->getWidget();
+        });
+
         connect(
           this,
           &ChatPage::initializeViews,
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index d2ba0dc3..283224f1 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -13,7 +13,8 @@
 #include "UserSettingsPage.h"
 
 RoomlistModel::RoomlistModel(TimelineViewManager *parent)
-  : manager(parent)
+  : QAbstractListModel(parent)
+  , manager(parent)
 {
         connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
                 auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3b3ea423..dd623f2f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -129,7 +129,8 @@ 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)
@@ -230,7 +231,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);

From dfe2495d9a2d1633dd5e238bd31b12959c2e236d Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 00:25:45 +0200
Subject: [PATCH 20/38] Optimize completion model by only splitting on normal
 spaces

---
 src/CompletionProxyModel.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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);
                         }

From 567078d39f3c85f260efd68aeb5c99f8e4ffe348 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 01:09:16 +0200
Subject: [PATCH 21/38] Try to get rid of an allocation

---
 src/timeline/EventStore.cpp    | 14 ++++++--------
 src/timeline/EventStore.h      |  2 +-
 src/timeline/TimelineModel.cpp |  4 ++--
 3 files changed, 9 insertions(+), 11 deletions(-)

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/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8f4a8564..cd3febd5 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -574,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>(
@@ -583,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>(

From 53fcf7f428d8d7ef2390f5877fe8f682ba9971d3 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 12:41:44 +0200
Subject: [PATCH 22/38] Port remaining sidebar actions to qml

---
 resources/qml/RoomList.qml   |  19 +++
 src/MainWindow.cpp           |   1 -
 src/SideBarActions.cpp       | 120 -------------------
 src/SideBarActions.h         |  54 ---------
 src/Splitter.cpp             | 166 --------------------------
 src/Splitter.h               |  49 --------
 src/UserInfoWidget.cpp       | 219 -----------------------------------
 src/UserInfoWidget.h         |  68 -----------
 src/ui/NhekoGlobalObject.cpp |  27 +++++
 src/ui/NhekoGlobalObject.h   |   4 +
 10 files changed, 50 insertions(+), 677 deletions(-)
 delete mode 100644 src/SideBarActions.cpp
 delete mode 100644 src/SideBarActions.h
 delete mode 100644 src/Splitter.cpp
 delete mode 100644 src/Splitter.h
 delete mode 100644 src/UserInfoWidget.cpp
 delete mode 100644 src/UserInfoWidget.h

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index c5e07032..3109b75c 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -477,6 +477,7 @@ Page {
                     image: ":/icons/icons/ui/power-button-off.png"
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Logout")
+                    onClicked: Nheko.openLogoutDialog()
                 }
 
             }
@@ -523,6 +524,23 @@ Page {
                     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 {
@@ -545,6 +563,7 @@ Page {
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("User settings")
                     Layout.margins: Nheko.paddingMedium
+                    onClicked: Nheko.showUserSettingsPage()
                 }
 
             }
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 057ee4af..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"
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/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index fd572b4b..fea10839 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -10,6 +10,7 @@
 #include "Cache_p.h"
 #include "ChatPage.h"
 #include "Logging.h"
+#include "MainWindow.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
@@ -113,3 +114,29 @@ Nheko::currentUser() const
 
         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
index 593514fa..14135fd1 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -40,6 +40,10 @@ public:
 
         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();

From 7f4656d3c3e90f5e0c3e22315e9c69df2ecdb532 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 13:22:11 +0200
Subject: [PATCH 23/38] Refactor to use Instantiator instead of doing it
 manually

---
 resources/qml/ChatPage.qml |  2 +-
 resources/qml/RoomList.qml | 63 +++++++++++++-------------------------
 resources/qml/Root.qml     |  5 +--
 3 files changed, 26 insertions(+), 44 deletions(-)

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 966f169b..0fe65afc 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -31,8 +31,8 @@ Rectangle {
 
         TimelineView {
             id: timeline
-            room: Rooms.currentRoom
 
+            room: Rooms.currentRoom
             SplitView.fillWidth: true
             SplitView.minimumWidth: 400
         }
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 3109b75c..ecfb3af9 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -4,6 +4,7 @@
 
 import "./dialogs"
 import Qt.labs.platform 1.1 as Platform
+import QtQml 2.13
 import QtQuick 2.13
 import QtQuick.Controls 2.13
 import QtQuick.Layouts 1.3
@@ -37,19 +38,12 @@ Page {
 
             property string roomid
             property var tags
+            property var allTags
 
             function show(roomid_, tags_) {
                 roomid = roomid_;
                 tags = tags_;
-                roomContextMenu.clear();
-                roomContextMenu.addItem(leaveOpt.createObject(roomContextMenu));
-                roomContextMenu.addItem(separatorOpt.createObject(roomContextMenu));
-                for (let tag of Rooms.tags()) {
-                    roomContextMenu.addItem(tagDelegate.createObject(roomContextMenu, {
-                        "t": tag
-                    }));
-                }
-                roomContextMenu.addItem(newTagOpt.createObject(roomContextMenu));
+                allTags = Rooms.tags();
                 open();
             }
 
@@ -63,30 +57,22 @@ Page {
                 }
             }
 
-            Component {
-                id: leaveOpt
-
-                Platform.MenuItem {
-                    text: qsTr("Leave room")
-                    onTriggered: Rooms.leave(roomContextMenu.roomid)
-                }
-
+            Platform.MenuItem {
+                text: qsTr("Leave room")
+                onTriggered: Rooms.leave(roomContextMenu.roomid)
             }
 
-            Component {
-                id: separatorOpt
-
-                Platform.MenuSeparator {
-                    text: qsTr("Tag room as:")
-                }
-
+            Platform.MenuSeparator {
+                text: qsTr("Tag room as:")
             }
 
-            Component {
-                id: tagDelegate
+            Instantiator {
+                model: roomContextMenu.allTags
+                onObjectAdded: roomContextMenu.insertItem(index + 2, object)
+                onObjectRemoved: roomContextMenu.removeItem(object)
 
-                Platform.MenuItem {
-                    property string t
+                delegate: Platform.MenuItem {
+                    property string t: modelData
 
                     text: {
                         switch (t) {
@@ -107,14 +93,9 @@ Page {
 
             }
 
-            Component {
-                id: newTagOpt
-
-                Platform.MenuItem {
-                    text: qsTr("Create new tag...")
-                    onTriggered: newTag.show()
-                }
-
+            Platform.MenuItem {
+                text: qsTr("Create new tag...")
+                onTriggered: newTag.show()
             }
 
         }
@@ -166,9 +147,9 @@ Page {
             TapHandler {
                 acceptedButtons: Qt.RightButton
                 onSingleTapped: {
-                    if (!TimelineManager.isInvite) {
+                    if (!TimelineManager.isInvite)
                         roomContextMenu.show(model.roomId, model.tags);
-                    }
+
                 }
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
@@ -176,9 +157,9 @@ Page {
             TapHandler {
                 onSingleTapped: Rooms.setCurrentRoom(model.roomId)
                 onLongPressed: {
-                    if (!TimelineManager.isInvite) {
+                    if (!TimelineManager.isInvite)
                         roomContextMenu.show(model.roomId, model.tags);
-                    }
+
                 }
             }
 
@@ -524,7 +505,6 @@ Page {
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Start a new chat")
                     Layout.margins: Nheko.paddingMedium
-
                     onClicked: roomJoinCreateMenu.open(parent)
 
                     Platform.Menu {
@@ -541,6 +521,7 @@ Page {
                         }
 
                     }
+
                 }
 
                 ImageButton {
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index c23ab97d..078281d4 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -74,11 +74,12 @@ Page {
 
     Shortcut {
         sequence: "Ctrl+Down"
-        onActivated: Rooms.nextRoom();
+        onActivated: Rooms.nextRoom()
     }
+
     Shortcut {
         sequence: "Ctrl+Up"
-        onActivated: Rooms.previousRoom();
+        onActivated: Rooms.previousRoom()
     }
 
     Component {

From 1da3f2e1da67923adae2decb5a0891056d23a256 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 5 Jun 2021 01:22:59 +0200
Subject: [PATCH 24/38] Add ugly borders to tables

---
 resources/qml/delegates/TextMessage.qml | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index f65eda79..cd46f8ca 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -9,7 +9,24 @@ MatrixText {
     property string formatted: model.data.formattedBody
     property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
 
-    text: "" + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").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(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply

From 27070386e7319c30472dffafeae027ecc26f41d8 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 5 Jun 2021 23:20:23 +0200
Subject: [PATCH 25/38] Fix html in completer

Fixes #578
---
 resources/qml/Completer.qml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 0cdd789d..bbfa7b94 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -210,6 +210,7 @@ Popup {
                             text: model.roomName
                             font.pixelSize: popup.avatarHeight * 0.5
                             color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
+                            textFormat: Text.RichText
                         }
 
                     }
@@ -236,11 +237,13 @@ Popup {
                         Label {
                             text: model.roomName
                             color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
+                            textFormat: Text.RichText
                         }
 
                         Label {
                             text: "(" + model.roomAlias + ")"
                             color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText
+                            textFormat: Text.RichText
                         }
 
                     }

From 686298e02397b4165fcbee4008e2cdcf137c1020 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 5 Jun 2021 23:36:08 +0200
Subject: [PATCH 26/38] Somewhat reenable the adaptive layout

---
 resources/qml/ChatPage.qml                    |  53 ++++---
 resources/qml/RoomList.qml                    |   8 ++
 resources/qml/components/AdaptiveLayout.qml   | 130 ++++++++++++++++++
 .../qml/components/AdaptiveLayoutElement.qml  |  26 ++++
 resources/res.qrc                             |   2 +
 5 files changed, 200 insertions(+), 19 deletions(-)
 create mode 100644 resources/qml/components/AdaptiveLayout.qml
 create mode 100644 resources/qml/components/AdaptiveLayoutElement.qml

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 0fe65afc..e5b53738 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -5,6 +5,7 @@
 import QtQuick 2.9
 import QtQuick.Controls 2.13
 import QtQuick.Layouts 1.3
+import "components"
 import im.nheko 1.0
 
 Rectangle {
@@ -12,34 +13,48 @@ Rectangle {
 
     color: Nheko.colors.window
 
-    SplitView {
+    AdaptiveLayout {
         anchors.fill: parent
+        singlePageMode: width < communityListC.maximumWidth + roomListC.maximumWidth + timlineViewC.minimumWidth
+
+        AdaptiveLayoutElement {
+            id: communityListC
+
+            minimumWidth: Nheko.avatarSize * 2 + Nheko.paddingSmall * 2
+            collapsedWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
+
+            Rectangle {
+                color: Nheko.theme.sidebarBackground
+            }
 
-        Rectangle {
-            SplitView.minimumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            SplitView.preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            SplitView.maximumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            color: Nheko.theme.sidebarBackground
         }
 
-        RoomList {
-            //SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
+        AdaptiveLayoutElement {
+            id: roomListC
+
+            minimumWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
+            preferredWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
+            maximumWidth: Nheko.avatarSize * 10 + Nheko.paddingSmall * 2
+            collapsedWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+
+            RoomList {
+            }
 
-            SplitView.minimumWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
-            SplitView.preferredWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
         }
 
-        TimelineView {
-            id: timeline
+        AdaptiveLayoutElement {
+            id: timlineViewC
 
-            room: Rooms.currentRoom
-            SplitView.fillWidth: true
-            SplitView.minimumWidth: 400
-        }
+            minimumWidth: 400
+
+            TimelineView {
+                id: timeline
+
+                room: Rooms.currentRoom
+            }
 
-        handle: Rectangle {
-            implicitWidth: 2
-            color: SplitHandle.pressed ? Nheko.colors.highlight : (SplitHandle.hovered ? Nheko.colors.light : Nheko.theme.separator)
         }
 
     }
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index ecfb3af9..ce991dec 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -11,6 +11,9 @@ import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Page {
+    //leftPadding: Nheko.paddingSmall
+    //rightPadding: Nheko.paddingSmall
+
     ListView {
         id: roomlist
 
@@ -145,6 +148,7 @@ Page {
             ]
 
             TapHandler {
+                margin: -2
                 acceptedButtons: Qt.RightButton
                 onSingleTapped: {
                     if (!TimelineManager.isInvite)
@@ -155,6 +159,7 @@ Page {
             }
 
             TapHandler {
+                margin: -2
                 onSingleTapped: Rooms.setCurrentRoom(model.roomId)
                 onLongPressed: {
                     if (!TimelineManager.isInvite)
@@ -164,6 +169,7 @@ Page {
             }
 
             HoverHandler {
+                margin: -2
                 id: hovered
             }
 
@@ -390,6 +396,7 @@ Page {
             }
 
             TapHandler {
+                margin: -2
                 acceptedButtons: Qt.LeftButton
                 onSingleTapped: userInfoPanel.openUserProfile()
                 onLongPressed: userInfoMenu.open()
@@ -397,6 +404,7 @@ Page {
             }
 
             TapHandler {
+                margin: -2
                 acceptedButtons: Qt.RightButton
                 onSingleTapped: userInfoMenu.open()
                 gesturePolicy: TapHandler.ReleaseWithinBounds
diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml
new file mode 100644
index 00000000..e6416414
--- /dev/null
+++ b/resources/qml/components/AdaptiveLayout.qml
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+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
+
+    handle: Rectangle {
+        z: 3
+        color: Nheko.theme.separator
+        height: container.height
+        width: visible ? 1 : 0
+        anchors.right: parent.right
+    }
+
+    property Component handleToucharea
+
+    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
+
+        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 {
+            enabled: !container.singlePageMode
+            margin: container.splitterGrabMargin
+            cursorShape: Qt.SizeHorCursor
+        }
+
+    }
+
+    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;
+            });
+        }
+    }
+
+    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..411f6c64
--- /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.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+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/res.qrc b/resources/res.qrc
index 183cf394..531e9be2 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -178,6 +178,8 @@
         qml/voip/PlaceCall.qml
         qml/voip/ScreenShare.qml
         qml/voip/VideoCall.qml
+        qml/components/AdaptiveLayout.qml
+        qml/components/AdaptiveLayoutElement.qml
     
     
         media/ring.ogg

From d364c29c43dca128f516c5f7d4e925b27347f558 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Tue, 8 Jun 2021 22:18:51 +0200
Subject: [PATCH 27/38] Implement switching in narrow mode

---
 resources/qml/ChatPage.qml                  |  9 ++-
 resources/qml/RoomList.qml                  | 56 ++++++++++++---
 resources/qml/TimelineView.qml              |  2 +
 resources/qml/TopBar.qml                    |  6 +-
 resources/qml/components/AdaptiveLayout.qml | 75 ++++++++++-----------
 src/timeline/RoomlistModel.h                | 12 +++-
 src/timeline/TimelineModel.cpp              |  2 -
 src/timeline/TimelineViewManager.h          | 24 -------
 8 files changed, 109 insertions(+), 77 deletions(-)

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index e5b53738..0f884d75 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -14,8 +14,11 @@ Rectangle {
     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
@@ -37,9 +40,12 @@ Rectangle {
             minimumWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
             preferredWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
             maximumWidth: Nheko.avatarSize * 10 + Nheko.paddingSmall * 2
-            collapsedWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
+            collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium
 
             RoomList {
+                id: roomlist
+
+                collapsed: parent.collapsed
             }
 
         }
@@ -52,6 +58,7 @@ Rectangle {
             TimelineView {
                 id: timeline
 
+                showBackButton: adaptiveView.singlePageMode
                 room: Rooms.currentRoom
             }
 
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index ce991dec..21973b77 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -13,6 +13,8 @@ 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
@@ -113,9 +115,11 @@ Page {
             property color bubbleText: Nheko.colors.highlightedText
 
             color: background
-            height: Math.ceil(fontMetrics.lineSpacing * 2.3 + Nheko.paddingMedium * 2)
+            height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
             state: "normal"
+            ToolTip.visible: hovered.hovered && collapsed
+            ToolTip.text: model.roomName
             states: [
                 State {
                     name: "highlight"
@@ -148,7 +152,7 @@ Page {
             ]
 
             TapHandler {
-                margin: -2
+                margin: -Nheko.paddingSmall
                 acceptedButtons: Qt.RightButton
                 onSingleTapped: {
                     if (!TimelineManager.isInvite)
@@ -159,7 +163,7 @@ Page {
             }
 
             TapHandler {
-                margin: -2
+                margin: -Nheko.paddingSmall
                 onSingleTapped: Rooms.setCurrentRoom(model.roomId)
                 onLongPressed: {
                     if (!TimelineManager.isInvite)
@@ -169,8 +173,9 @@ Page {
             }
 
             HoverHandler {
-                margin: -2
                 id: hovered
+
+                margin: -Nheko.paddingSmall
             }
 
             RowLayout {
@@ -186,15 +191,46 @@ Page {
 
                     enabled: false
                     Layout.alignment: Qt.AlignVCenter
-                    height: Math.ceil(fontMetrics.lineSpacing * 2.3)
-                    width: Math.ceil(fontMetrics.lineSpacing * 2.3)
+                    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
@@ -396,7 +432,7 @@ Page {
             }
 
             TapHandler {
-                margin: -2
+                margin: -Nheko.paddingSmall
                 acceptedButtons: Qt.LeftButton
                 onSingleTapped: userInfoPanel.openUserProfile()
                 onLongPressed: userInfoMenu.open()
@@ -404,7 +440,7 @@ Page {
             }
 
             TapHandler {
-                margin: -2
+                margin: -Nheko.paddingSmall
                 acceptedButtons: Qt.RightButton
                 onSingleTapped: userInfoMenu.open()
                 gesturePolicy: TapHandler.ReleaseWithinBounds
@@ -431,6 +467,7 @@ Page {
                 ColumnLayout {
                     id: col
 
+                    visible: !collapsed
                     Layout.alignment: Qt.AlignLeft
                     Layout.fillWidth: true
                     width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
@@ -462,6 +499,7 @@ Page {
                 ImageButton {
                     id: logoutButton
 
+                    visible: !collapsed
                     Layout.alignment: Qt.AlignVCenter
                     image: ":/icons/icons/ui/power-button-off.png"
                     ToolTip.visible: hovered
@@ -533,6 +571,7 @@ Page {
                 }
 
                 ImageButton {
+                    visible: !collapsed
                     Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
                     hoverEnabled: true
                     width: 22
@@ -544,6 +583,7 @@ Page {
                 }
 
                 ImageButton {
+                    visible: !collapsed
                     Layout.alignment: Qt.AlignBottom | Qt.AlignRight
                     hoverEnabled: true
                     width: 22
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 747be61e..095103fa 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -19,6 +19,7 @@ Item {
     id: timelineView
 
     property var room: null
+    property bool showBackButton: false
 
     Label {
         visible: !room && !TimelineManager.isInitialSync
@@ -45,6 +46,7 @@ Item {
         spacing: 0
 
         TopBar {
+            showBackButton: timelineView.showBackButton
         }
 
         Rectangle {
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 65e27939..30ab2e7c 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -11,6 +11,8 @@ import im.nheko 1.0
 Rectangle {
     id: topBar
 
+    property bool showBackButton: false
+
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
@@ -43,11 +45,11 @@ Rectangle {
             Layout.alignment: Qt.AlignVCenter
             width: Nheko.avatarSize
             height: Nheko.avatarSize
-            visible: TimelineManager.isNarrowView
+            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 {
diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml
index e6416414..eea85e38 100644
--- a/resources/qml/components/AdaptiveLayout.qml
+++ b/resources/qml/components/AdaptiveLayout.qml
@@ -18,6 +18,43 @@ Container {
     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
@@ -27,8 +64,6 @@ Container {
         anchors.right: parent.right
     }
 
-    property Component handleToucharea
-
     handleToucharea: Item {
         id: splitter
 
@@ -79,42 +114,6 @@ Container {
 
     }
 
-    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;
-            });
-        }
-    }
-
     contentItem: ListView {
         id: view
 
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index d3e1e1f9..fa991f6b 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -21,7 +21,8 @@ class TimelineViewManager;
 class RoomlistModel : public QAbstractListModel
 {
         Q_OBJECT
-        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET
+                     resetCurrentRoom)
 public:
         enum Roles
         {
@@ -73,6 +74,11 @@ public slots:
         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_);
@@ -98,7 +104,8 @@ private:
 class FilteredRoomlistModel : public QSortFilterProxyModel
 {
         Q_OBJECT
-        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
+        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;
@@ -117,6 +124,7 @@ public slots:
 
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
         void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+        void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); }
 
         void nextRoom();
         void previousRoom();
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index cd3febd5..f29f929e 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -891,7 +891,6 @@ TimelineModel::updateLastMessage()
                                    time};
                         if (description != lastMessage_) {
                                 lastMessage_ = description;
-                                emit manager_->updateRoomsLastMessage(room_id_, lastMessage_);
                                 emit lastMessageChanged();
                         }
                         return;
@@ -906,7 +905,6 @@ TimelineModel::updateLastMessage()
                                      QString::fromStdString(mtx::accessors::sender(*event))));
                 if (description != lastMessage_) {
                         lastMessage_ = description;
-                        emit manager_->updateRoomsLastMessage(room_id_, description);
                         emit lastMessageChanged();
                 }
                 return;
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index c4707208..68d9cd1b 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -37,8 +37,6 @@ class TimelineViewManager : public QObject
 
         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)
 
@@ -54,7 +52,6 @@ public:
         void clearAll() { rooms_->clear(); }
 
         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);
@@ -74,16 +71,12 @@ public:
         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);
@@ -113,22 +106,6 @@ public slots:
 
         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);
 
@@ -152,7 +129,6 @@ private:
         CallManager *callManager_ = nullptr;
 
         bool isInitialSync_   = true;
-        bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
         RoomlistModel *rooms_ = nullptr;

From 2cd1a931c28d0fd8e8755e9622a7d8f56d1a24a0 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Wed, 9 Jun 2021 23:52:28 +0200
Subject: [PATCH 28/38] Basic community list model

---
 CMakeLists.txt                       |   2 +
 resources/qml/RoomList.qml           |   6 +-
 src/timeline/CommunitiesModel.cpp    | 158 +++++++++++++++++++++++++++
 src/timeline/CommunitiesModel.h      |  60 ++++++++++
 src/timeline/RoomlistModel.cpp       |  23 ----
 src/timeline/RoomlistModel.h         |   1 -
 src/timeline/TimelineViewManager.cpp |   9 ++
 src/timeline/TimelineViewManager.h   |   4 +-
 8 files changed, 234 insertions(+), 29 deletions(-)
 create mode 100644 src/timeline/CommunitiesModel.cpp
 create mode 100644 src/timeline/CommunitiesModel.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5a5e3ba1..3d9d793c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -265,6 +265,7 @@ set(SRC_FILES
 
 
 	# Timeline
+	src/timeline/CommunitiesModel.cpp
 	src/timeline/EventStore.cpp
 	src/timeline/InputBar.cpp
 	src/timeline/Reaction.cpp
@@ -481,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
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 21973b77..a6637467 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -43,12 +43,10 @@ Page {
 
             property string roomid
             property var tags
-            property var allTags
 
             function show(roomid_, tags_) {
                 roomid = roomid_;
                 tags = tags_;
-                allTags = Rooms.tags();
                 open();
             }
 
@@ -72,7 +70,7 @@ Page {
             }
 
             Instantiator {
-                model: roomContextMenu.allTags
+                model: Communities.tags
                 onObjectAdded: roomContextMenu.insertItem(index + 2, object)
                 onObjectRemoved: roomContextMenu.removeItem(object)
 
@@ -92,7 +90,7 @@ Page {
                         }
                     }
                     checkable: true
-                    checked: roomContextMenu.tags.includes(t)
+                    checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
                     onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
                 }
 
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
new file mode 100644
index 00000000..cedaacce
--- /dev/null
+++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,158 @@
+// 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"},
+        };
+}
+
+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::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.right(2);
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tag.right(2);
+                        }
+                }
+
+                switch (role) {
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return UserSettings::instance()->hiddenTags().contains("tag:" + tag);
+                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));
+        endResetModel();
+
+        emit tagsChanged();
+}
+
+void
+CommunitiesModel::clear()
+{
+        beginResetModel();
+        tags_.clear();
+        endResetModel();
+
+        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.remove(0, 4);
+                for (const auto &t : tags_) {
+                        if (t == tag) {
+                                this->currentTagId_ = tagId;
+                                emit currentTagIdChanged();
+                                return;
+                        }
+                }
+        }
+
+        this->currentTagId_ = "";
+        emit currentTagIdChanged();
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
new file mode 100644
index 00000000..3f6a2a4c
--- /dev/null
+++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,60 @@
+// 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,
+                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();
+        }
+        QStringList tags() const { return tags_; }
+
+signals:
+        void currentTagIdChanged();
+        void tagsChanged();
+
+private:
+        QStringList tags_;
+        QString currentTagId_;
+};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 283224f1..4dd44b30 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -485,29 +485,6 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
         sort(0);
 }
 
-QStringList
-FilteredRoomlistModel::tags()
-{
-        std::set ts;
-        for (const auto &e : cache::roomInfo()) {
-                for (const auto &t : e.tags) {
-                        if (t.find("u.") == 0) {
-                                ts.insert(t);
-                        }
-                }
-        }
-
-        QStringList ret{{
-          "m.favourite",
-          "m.lowpriority",
-        }};
-
-        for (const auto &t : ts)
-                ret.push_back(QString::fromStdString(t));
-
-        return ret;
-}
-
 void
 FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index fa991f6b..7ee0419f 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -119,7 +119,6 @@ public slots:
         void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
         void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
         void leave(QString roomid) { roomlistmodel->leave(roomid); }
-        QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index dd623f2f..faf56b85 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -135,6 +135,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
   , rooms_(new RoomlistModel(this))
+  , communities_(new CommunitiesModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -196,6 +197,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   return new FilteredRoomlistModel(self->rooms_);
           });
+        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();
@@ -324,6 +331,7 @@ void
 TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
         this->rooms_->sync(rooms_res);
+        this->communities_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -486,6 +494,7 @@ void
 TimelineViewManager::initializeRoomlist()
 {
         rooms_->initializeRooms();
+        communities_->initializeSidebar();
 }
 
 void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 68d9cd1b..556bcf4c 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,7 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/CommunitiesModel.h"
 #include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
@@ -131,7 +132,8 @@ private:
         bool isInitialSync_   = true;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms_ = nullptr;
+        RoomlistModel *rooms_          = nullptr;
+        CommunitiesModel *communities_ = nullptr;
 
         QHash userColors;
 

From d8c0d4874bb1864a677ae451d93727ab75484f84 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 13:12:43 +0200
Subject: [PATCH 29/38] Render community items

---
 resources/qml/Avatar.qml                      |   2 +
 resources/qml/ChatPage.qml                    |  13 +-
 resources/qml/CommunitiesList.qml             | 151 ++++++++++++++++++
 resources/qml/RoomList.qml                    |   6 +-
 resources/qml/device-verification/Success.qml |   1 +
 resources/res.qrc                             |   1 +
 src/Cache.cpp                                 |   4 +
 src/Olm.cpp                                   |   9 +-
 src/timeline/CommunitiesModel.cpp             |   7 +-
 9 files changed, 180 insertions(+), 14 deletions(-)
 create mode 100644 resources/qml/CommunitiesList.qml

diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 84c22da1..9eb3380e 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -14,6 +14,7 @@ Rectangle {
     property alias url: img.source
     property string userid
     property string displayName
+    property alias textColor: label.color
 
     signal clicked(var mouse)
 
@@ -26,6 +27,7 @@ Rectangle {
     }
 
     Label {
+        id: label
         anchors.fill: parent
         text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
         textFormat: Text.RichText
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 0f884d75..5ccdd9f1 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -23,13 +23,14 @@ Rectangle {
         AdaptiveLayoutElement {
             id: communityListC
 
-            minimumWidth: Nheko.avatarSize * 2 + Nheko.paddingSmall * 2
-            collapsedWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2
-            maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2
+            minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
+            collapsedWidth: communitiesList.avatarSize + 2* Nheko.paddingMedium
+            preferredWidth: collapsedWidth
+            maximumWidth: communitiesList.avatarSize * 10 + 2* Nheko.paddingMedium
 
-            Rectangle {
-                color: Nheko.theme.sidebarBackground
+            CommunitiesList {
+                id: communitiesList
+                collapsed: parent.collapsed
             }
 
         }
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
new file mode 100644
index 00000000..6ca619c4
--- /dev/null
+++ b/resources/qml/CommunitiesList.qml
@@ -0,0 +1,151 @@
+// 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.13
+import QtQuick 2.13
+import QtQuick.Controls 2.13
+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 id
+
+            function show(id_, tags_) {
+                id = id_;
+                open();
+            }
+
+            Platform.MenuItem {
+                text: qsTr("Leave room")
+                onTriggered: Rooms.leave(roomContextMenu.roomid)
+            }
+
+        }
+
+        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 && !(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
+
+                }
+
+            }
+
+        }
+
+    }
+
+    background: Rectangle {
+        color: Nheko.theme.sidebarBackground
+    }
+
+}
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index a6637467..09fb3701 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -32,8 +32,8 @@ Page {
 
         Connections {
             onActiveTimelineChanged: {
-                roomlist.positionViewAtIndex(Rooms.roomidToIndex(TimelineManager.timeline.roomId()), ListView.Contain);
-                console.log("Test" + TimelineManager.timeline.roomId() + " " + Rooms.roomidToIndex(TimelineManager.timeline.roomId));
+                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain);
+                console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId()));
             }
             target: TimelineManager
         }
@@ -121,7 +121,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !(TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId())
+                    when: hovered.hovered && !(Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId())
 
                     PropertyChanges {
                         target: roomItem
diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml
index b858a1a1..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")
diff --git a/resources/res.qrc b/resources/res.qrc
index 531e9be2..53c74ae3 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -125,6 +125,7 @@
 
         qml/Root.qml
         qml/ChatPage.qml
+        qml/CommunitiesList.qml
         qml/RoomList.qml
         qml/TimelineView.qml
         qml/Avatar.qml
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 4a99dd59..5684de37 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -3451,6 +3451,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;
                         }
 
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/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index cedaacce..c8ebaa96 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -21,6 +21,7 @@ CommunitiesModel::roleNames() const
           {DisplayName, "displayName"},
           {Tooltip, "tooltip"},
           {ChildrenHidden, "childrenHidden"},
+          {Id, "id"},
         };
 }
 
@@ -74,9 +75,9 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                         case CommunitiesModel::Roles::AvatarUrl:
                                 return QString(":/icons/icons/ui/tag.png");
                         case CommunitiesModel::Roles::DisplayName:
-                                return tag.right(2);
+                                return tag.mid(2);
                         case CommunitiesModel::Roles::Tooltip:
-                                return tag.right(2);
+                                return tag.mid(2);
                         }
                 }
 
@@ -143,7 +144,7 @@ void
 CommunitiesModel::setCurrentTagId(QString tagId)
 {
         if (tagId.startsWith("tag:")) {
-                auto tag = tagId.remove(0, 4);
+                auto tag = tagId.mid(4);
                 for (const auto &t : tags_) {
                         if (t == tag) {
                                 this->currentTagId_ = tagId;

From 8d2d8dc26727a5b46613d83522490f568aef7cad Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 14:51:29 +0200
Subject: [PATCH 30/38] Enable toggling tags

---
 resources/qml/Avatar.qml             |  1 +
 resources/qml/ChatPage.qml           |  5 +++--
 resources/qml/CommunitiesList.qml    | 25 +++++++++++++++++--------
 src/timeline/CommunitiesModel.cpp    |  5 +++--
 src/timeline/CommunitiesModel.h      |  4 ++--
 src/timeline/RoomlistModel.cpp       | 17 +++++++++++++++++
 src/timeline/RoomlistModel.h         | 23 +++++++++++++++++++++++
 src/timeline/TimelineViewManager.cpp |  8 +++++++-
 8 files changed, 73 insertions(+), 15 deletions(-)

diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 9eb3380e..6c12952a 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -28,6 +28,7 @@ Rectangle {
 
     Label {
         id: label
+
         anchors.fill: parent
         text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
         textFormat: Text.RichText
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 5ccdd9f1..6cd48788 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -24,12 +24,13 @@ Rectangle {
             id: communityListC
 
             minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
-            collapsedWidth: communitiesList.avatarSize + 2* Nheko.paddingMedium
+            collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium
             preferredWidth: collapsedWidth
-            maximumWidth: communitiesList.avatarSize * 10 + 2* Nheko.paddingMedium
+            maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium
 
             CommunitiesList {
                 id: communitiesList
+
                 collapsed: parent.collapsed
             }
 
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 6ca619c4..0ccd7e82 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -10,7 +10,6 @@ import QtQuick.Controls 2.13
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
-
 Page {
     //leftPadding: Nheko.paddingSmall
     //rightPadding: Nheko.paddingSmall
@@ -97,8 +96,7 @@ Page {
             TapHandler {
                 margin: -Nheko.paddingSmall
                 acceptedButtons: Qt.RightButton
-                onSingleTapped: communityContextMenu.show(model.id);
-
+                onSingleTapped: communityContextMenu.show(model.id)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
@@ -127,15 +125,26 @@ Page {
                     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
-                        }
+                        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
                 }
 
             }
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index c8ebaa96..9b758e97 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -118,6 +118,7 @@ CommunitiesModel::clear()
         beginResetModel();
         tags_.clear();
         endResetModel();
+        resetCurrentTagId();
 
         emit tagsChanged();
 }
@@ -148,12 +149,12 @@ CommunitiesModel::setCurrentTagId(QString tagId)
                 for (const auto &t : tags_) {
                         if (t == tag) {
                                 this->currentTagId_ = tagId;
-                                emit currentTagIdChanged();
+                                emit currentTagIdChanged(currentTagId_);
                                 return;
                         }
                 }
         }
 
         this->currentTagId_ = "";
-        emit currentTagIdChanged();
+        emit currentTagIdChanged(currentTagId_);
 }
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 3f6a2a4c..038c253b 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -46,12 +46,12 @@ public slots:
         void resetCurrentTagId()
         {
                 currentTagId_.clear();
-                emit currentTagIdChanged();
+                emit currentTagIdChanged(currentTagId_);
         }
         QStringList tags() const { return tags_; }
 
 signals:
-        void currentTagIdChanged();
+        void currentTagIdChanged(QString tagId);
         void tagsChanged();
 
 private:
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 4dd44b30..c0fb74a4 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -324,6 +324,7 @@ RoomlistModel::initializeRooms()
         models.clear();
         roomids.clear();
         invites.clear();
+        currentRoom_ = nullptr;
 
         invites = cache::client()->invites();
         for (const auto &id : invites.keys())
@@ -461,6 +462,22 @@ FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &righ
                 return left.row() < right.row();
 }
 
+bool
+FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
+{
+        if (filterType == FilterBy::Nothing)
+                return true;
+        else if (filterType == FilterBy::Tag) {
+                auto tags = sourceModel()
+                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                              .toStringList();
+
+                return tags.contains(filterStr);
+        } else {
+                return true;
+        }
+}
+
 FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 7ee0419f..b89c9a54 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -109,6 +109,7 @@ class FilteredRoomlistModel : public QSortFilterProxyModel
 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)
@@ -128,6 +129,19 @@ public slots:
         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();
+        }
+
 signals:
         void currentRoomChanged();
 
@@ -135,4 +149,13 @@ 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;
 };
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index faf56b85..2ee79d4f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -195,7 +195,13 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms_);
+                  auto ptr = new FilteredRoomlistModel(self->rooms_);
+
+                  connect(self->communities_,
+                          &CommunitiesModel::currentTagIdChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateFilterTag);
+                  return ptr;
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {

From a5291605a9912a411100edf8ee88e59857d8b9aa Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 17:54:05 +0200
Subject: [PATCH 31/38] Reenable tag hiding

---
 resources/qml/CommunitiesList.qml    | 10 ++---
 src/timeline/CommunitiesModel.cpp    | 30 ++++++++++++-
 src/timeline/CommunitiesModel.h      |  4 ++
 src/timeline/RoomlistModel.cpp       | 65 +++++++++++++++++++++-------
 src/timeline/RoomlistModel.h         |  3 ++
 src/timeline/TimelineViewManager.cpp |  4 ++
 6 files changed, 94 insertions(+), 22 deletions(-)

diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 0ccd7e82..6aab949c 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -33,16 +33,16 @@ Page {
         Platform.Menu {
             id: communityContextMenu
 
-            property string id
+            property string tagId
 
             function show(id_, tags_) {
-                id = id_;
+                tagId = id_;
                 open();
             }
 
             Platform.MenuItem {
-                text: qsTr("Leave room")
-                onTriggered: Rooms.leave(roomContextMenu.roomid)
+                text: qsTr("Hide rooms with this tag or from this space by default.")
+                onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
             }
 
         }
@@ -65,7 +65,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !(Communities.currentTagId == model.id)
+                    when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id)
 
                     PropertyChanges {
                         target: communityItem
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 9b758e97..96a450ea 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -21,6 +21,7 @@ CommunitiesModel::roleNames() const
           {DisplayName, "displayName"},
           {Tooltip, "tooltip"},
           {ChildrenHidden, "childrenHidden"},
+          {Hidden, "hidden"},
           {Id, "id"},
         };
 }
@@ -38,6 +39,8 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                         return tr("Shows all rooms without filtering.");
                 case CommunitiesModel::Roles::ChildrenHidden:
                         return false;
+                case CommunitiesModel::Roles::Hidden:
+                        return false;
                 case CommunitiesModel::Roles::Id:
                         return "";
                 }
@@ -82,8 +85,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                 }
 
                 switch (role) {
+                case CommunitiesModel::Roles::Hidden:
+                        return hiddentTagIds_.contains("tag:" + tag);
                 case CommunitiesModel::Roles::ChildrenHidden:
-                        return UserSettings::instance()->hiddenTags().contains("tag:" + tag);
+                        return true;
                 case CommunitiesModel::Roles::Id:
                         return "tag:" + tag;
                 }
@@ -107,9 +112,12 @@ CommunitiesModel::initializeSidebar()
         tags_.clear();
         for (const auto &t : ts)
                 tags_.push_back(QString::fromStdString(t));
+
+        hiddentTagIds_ = UserSettings::instance()->hiddenTags();
         endResetModel();
 
         emit tagsChanged();
+        emit hiddenTagsChanged();
 }
 
 void
@@ -158,3 +166,23 @@ CommunitiesModel::setCurrentTagId(QString tagId)
         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
index 038c253b..c98b5955 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -25,6 +25,7 @@ public:
                 DisplayName,
                 Tooltip,
                 ChildrenHidden,
+                Hidden,
                 Id,
         };
 
@@ -49,12 +50,15 @@ public slots:
                 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/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index c0fb74a4..0f980c6c 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -462,22 +462,6 @@ FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &righ
                 return left.row() < right.row();
 }
 
-bool
-FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
-{
-        if (filterType == FilterBy::Nothing)
-                return true;
-        else if (filterType == FilterBy::Tag) {
-                auto tags = sourceModel()
-                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
-                              .toStringList();
-
-                return tags.contains(filterStr);
-        } else {
-                return true;
-        }
-}
-
 FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
@@ -502,6 +486,55 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
         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)
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index b89c9a54..b0244886 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -142,6 +142,8 @@ public slots:
                 invalidateFilter();
         }
 
+        void updateHiddenTagsAndSpaces();
+
 signals:
         void currentRoomChanged();
 
@@ -158,4 +160,5 @@ private:
         };
         QString filterStr   = "";
         FilterBy filterType = FilterBy::Nothing;
+        QStringList hiddenTags, hiddenSpaces;
 };
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 2ee79d4f..c109d38e 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -201,6 +201,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                           &CommunitiesModel::currentTagIdChanged,
                           ptr,
                           &FilteredRoomlistModel::updateFilterTag);
+                  connect(self->communities_,
+                          &CommunitiesModel::hiddenTagsChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
                   return ptr;
           });
         qmlRegisterSingletonType(

From 4a6e62d1ee07371870eeadb4eea6eebf0ca3fc6a Mon Sep 17 00:00:00 2001
From: Joseph Donofry 
Date: Fri, 11 Jun 2021 15:13:16 -0400
Subject: [PATCH 32/38] Fix PrivacyScreen for qml-roomlist

---
 resources/qml/ChatPage.qml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 6cd48788..85ae81e8 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -72,7 +72,7 @@ Rectangle {
         anchors.fill: parent
         visible: Settings.privacyScreen
         screenTimeout: Settings.privacyScreenTimeout
-        timelineRoot: timeline
+        timelineRoot: adaptiveView
     }
 
 }

From 1d80f5d0b4f353d135a5d7b348416db503197a16 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 21:25:06 +0200
Subject: [PATCH 33/38] Remove useless capture

---
 src/timeline/TimelineViewManager.cpp | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index c109d38e..a6947f99 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -396,18 +396,17 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
         imgDialog->showFullScreen();
 
         auto room = rooms_->currentRoom();
-        connect(
-          imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
-                  // hide the overlay while presenting the save dialog for better
-                  // cross platform support.
-                  imgDialog->hide();
+        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 (!room->saveMedia(eventId)) {
-                          imgDialog->show();
-                  } else {
-                          imgDialog->close();
-                  }
-          });
+                if (!room->saveMedia(eventId)) {
+                        imgDialog->show();
+                } else {
+                        imgDialog->close();
+                }
+        });
 }
 
 void

From 4985e1f536be5ee6ac827cc7d014b36e581c1c50 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 12 Jun 2021 14:07:32 +0200
Subject: [PATCH 34/38] Persist sidebar sizes

---
 resources/icons/ui/user-friends-solid.svg |  1 +
 resources/qml/ChatPage.qml                | 24 +++++++++++++++---
 src/UserSettingsPage.cpp                  | 31 +++++++++++++++++++++--
 src/UserSettingsPage.h                    | 12 +++++++++
 4 files changed, 62 insertions(+), 6 deletions(-)
 create mode 100644 resources/icons/ui/user-friends-solid.svg

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/ChatPage.qml b/resources/qml/ChatPage.qml
index 85ae81e8..1e03ef11 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -25,7 +25,7 @@ Rectangle {
 
             minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
             collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium
-            preferredWidth: collapsedWidth
+            preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth
             maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium
 
             CommunitiesList {
@@ -34,14 +34,22 @@ Rectangle {
                 collapsed: parent.collapsed
             }
 
+            Binding {
+                target: Settings
+                property: 'communityListWidth'
+                value: communityListC.preferredWidth
+                when: !adaptiveView.singlePageMode
+                delayed: true
+            }
+
         }
 
         AdaptiveLayoutElement {
             id: roomListC
 
-            minimumWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
-            preferredWidth: Nheko.avatarSize * 5 + Nheko.paddingSmall * 2
-            maximumWidth: Nheko.avatarSize * 10 + Nheko.paddingSmall * 2
+            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 {
@@ -50,6 +58,14 @@ Rectangle {
                 collapsed: parent.collapsed
             }
 
+            Binding {
+                target: Settings
+                property: 'roomListWidth'
+                value: roomListC.preferredWidth
+                when: !adaptiveView.singlePageMode
+                delayed: true
+            }
+
         }
 
         AdaptiveLayoutElement {
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 99560678..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)
@@ -571,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_;

From e6878ee298525ac7808595418c4b84b93788ff2e Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 12 Jun 2021 16:05:45 +0200
Subject: [PATCH 35/38] Don't read avatarUrl from local profile, if no global
 avatar is set

---
 resources/qml/RoomList.qml | 12 +++++++-----
 src/Cache.cpp              |  3 ++-
 src/ui/UserProfile.cpp     |  8 +++-----
 3 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 09fb3701..f31fce60 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -447,6 +447,8 @@ Page {
             RowLayout {
                 id: userInfoGrid
 
+                property var profile: Nheko.currentUser
+
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
                 anchors.margins: Nheko.paddingMedium
@@ -457,9 +459,9 @@ Page {
                     Layout.alignment: Qt.AlignVCenter
                     Layout.preferredWidth: fontMetrics.lineSpacing * 2
                     Layout.preferredHeight: fontMetrics.lineSpacing * 2
-                    url: Nheko.currentUser.avatarUrl.replace("mxc://", "image://MxcImage/")
-                    displayName: Nheko.currentUser.displayName
-                    userid: Nheko.currentUser.userid
+                    url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
+                    displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
+                    userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
                 }
 
                 ColumnLayout {
@@ -476,7 +478,7 @@ Page {
                         Layout.alignment: Qt.AlignBottom
                         font.pointSize: fontMetrics.font.pointSize * 1.1
                         font.weight: Font.DemiBold
-                        fullText: Nheko.currentUser.displayName
+                        fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
                         elideWidth: col.width
                     }
 
@@ -486,7 +488,7 @@ Page {
                         font.weight: Font.Thin
                         font.pointSize: fontMetrics.font.pointSize * 0.9
                         elideWidth: col.width
-                        fullText: Nheko.currentUser.userid
+                        fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
                     }
 
                 }
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 5684de37..0d75ac51 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2479,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;
 }
diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index da130242..3d9c4b6a 100644
--- a/src/ui/UserProfile.cpp
+++ b/src/ui/UserProfile.cpp
@@ -39,7 +39,8 @@ UserProfile::UserProfile(QString roomid,
                 getGlobalProfileData();
         }
 
-        if (!cache::client() || !cache::client()->isDatabaseReady())
+        if (!cache::client() || !cache::client()->isDatabaseReady() ||
+            !ChatPage::instance()->timelineManager())
                 return;
 
         connect(cache::client(),
@@ -127,10 +128,7 @@ UserProfile::displayName()
 QString
 UserProfile::avatarUrl()
 {
-        return (isGlobalUserProfile() && globalAvatarUrl != "")
-                 ? globalAvatarUrl
-                 : cache::avatarUrl(roomid_, userid_);
-        ;
+        return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_);
 }
 
 bool

From 88039083213a37ad132461429f7122e651874717 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 13 Jun 2021 01:48:11 +0200
Subject: [PATCH 36/38] Try to be compatible with Qt 5.12

---
 CMakeLists.txt                                 |  2 +-
 resources/qml/ChatPage.qml                     |  2 +-
 resources/qml/CommunitiesList.qml              |  6 +++---
 resources/qml/ElidedLabel.qml                  |  2 +-
 resources/qml/RoomList.qml                     |  6 +++---
 resources/qml/Root.qml                         |  2 +-
 resources/qml/TimelineView.qml                 |  2 +-
 resources/qml/components/AdaptiveLayout.qml    | 18 +++++++++++++-----
 .../qml/components/AdaptiveLayoutElement.qml   |  4 ++--
 resources/qml/dialogs/InputDialog.qml          |  4 ++--
 10 files changed, 28 insertions(+), 20 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d9d793c..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)
 
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 1e03ef11..7a428019 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import QtQuick 2.9
-import QtQuick.Controls 2.13
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import "components"
 import im.nheko 1.0
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 6aab949c..491913be 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -4,9 +4,9 @@
 
 import "./dialogs"
 import Qt.labs.platform 1.1 as Platform
-import QtQml 2.13
-import QtQuick 2.13
-import QtQuick.Controls 2.13
+import QtQml 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml
index 1f4aeeea..bc90e479 100644
--- a/resources/qml/ElidedLabel.qml
+++ b/resources/qml/ElidedLabel.qml
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import QtQuick 2.9
-import QtQuick.Controls 2.13
+import QtQuick.Controls 2.5
 import im.nheko 1.0
 
 Label {
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index f31fce60..92073a37 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -4,9 +4,9 @@
 
 import "./dialogs"
 import Qt.labs.platform 1.1 as Platform
-import QtQml 2.13
-import QtQuick 2.13
-import QtQuick.Controls 2.13
+import QtQml 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 078281d4..5316e20d 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -9,7 +9,7 @@ import "./voip"
 import Qt.labs.platform 1.1 as Platform
 import QtGraphicalEffects 1.0
 import QtQuick 2.9
-import QtQuick.Controls 2.13
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import QtQuick.Window 2.2
 import im.nheko 1.0
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 095103fa..90e28166 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -9,7 +9,7 @@ import "./voip"
 import Qt.labs.platform 1.1 as Platform
 import QtGraphicalEffects 1.0
 import QtQuick 2.9
-import QtQuick.Controls 2.13
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import QtQuick.Window 2.2
 import im.nheko 1.0
diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml
index eea85e38..1d44ba90 100644
--- a/resources/qml/components/AdaptiveLayout.qml
+++ b/resources/qml/components/AdaptiveLayout.qml
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import QtQuick 2.15
-import QtQuick.Controls 2.15
+import QtQuick 2.12
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.15
 import im.nheko 1.0
 
@@ -87,6 +87,13 @@ Container {
         x: parent.preferredWidth
         z: 3
 
+        CursorShape {
+            height: parent.height
+            width: container.splitterGrabMargin * 2
+            x: -container.splitterGrabMargin
+            cursorShape: Qt.SizeHorCursor
+        }
+
         DragHandler {
             id: dragHandler
 
@@ -96,9 +103,9 @@ Container {
             xAxis.minimum: splitter.minimumWidth - 1
             xAxis.maximum: splitter.maximumWidth
             margin: container.splitterGrabMargin
-            dragThreshold: 0
+            //dragThreshold: 0
             grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.ApprovesTakeOverByHandlersOfSameType
-            cursorShape: Qt.SizeHorCursor
+            //cursorShape: Qt.SizeHorCursor
             onActiveChanged: {
                 if (!active)
                     splitter.parent.preferredWidth = splitter.x;
@@ -107,9 +114,10 @@ Container {
         }
 
         HoverHandler {
+            //cursorShape: Qt.SizeHorCursor
+
             enabled: !container.singlePageMode
             margin: container.splitterGrabMargin
-            cursorShape: Qt.SizeHorCursor
         }
 
     }
diff --git a/resources/qml/components/AdaptiveLayoutElement.qml b/resources/qml/components/AdaptiveLayoutElement.qml
index 411f6c64..3922e27d 100644
--- a/resources/qml/components/AdaptiveLayoutElement.qml
+++ b/resources/qml/components/AdaptiveLayoutElement.qml
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import QtQuick 2.15
-import QtQuick.Controls 2.15
+import QtQuick 2.12
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.15
 
 Item {
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index 0cd6be1c..134b78a3 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -3,8 +3,8 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import ".."
-import QtQuick 2.13
-import QtQuick.Controls 2.13
+import QtQuick 2.12
+import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 

From 9fd70c34f90a9ef010091024ae13f9af8d131c61 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 13 Jun 2021 02:48:22 +0200
Subject: [PATCH 37/38] Layout 1.15 -> 1.12

---
 resources/qml/components/AdaptiveLayout.qml        | 2 +-
 resources/qml/components/AdaptiveLayoutElement.qml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml
index 1d44ba90..357a7831 100644
--- a/resources/qml/components/AdaptiveLayout.qml
+++ b/resources/qml/components/AdaptiveLayout.qml
@@ -4,7 +4,7 @@
 
 import QtQuick 2.12
 import QtQuick.Controls 2.5
-import QtQuick.Layouts 1.15
+import QtQuick.Layouts 1.12
 import im.nheko 1.0
 
 Container {
diff --git a/resources/qml/components/AdaptiveLayoutElement.qml b/resources/qml/components/AdaptiveLayoutElement.qml
index 3922e27d..a4aec72e 100644
--- a/resources/qml/components/AdaptiveLayoutElement.qml
+++ b/resources/qml/components/AdaptiveLayoutElement.qml
@@ -4,7 +4,7 @@
 
 import QtQuick 2.12
 import QtQuick.Controls 2.5
-import QtQuick.Layouts 1.15
+import QtQuick.Layouts 1.12
 
 Item {
     property int minimumWidth: 100

From e95659811d6ce9bb08cd11b0275d5d31f4b4a1da Mon Sep 17 00:00:00 2001
From: Loren Burkholder 
Date: Sat, 12 Jun 2021 21:18:31 -0400
Subject: [PATCH 38/38] Fix button spacing

---
 resources/qml/ImageButton.qml | 1 +
 resources/qml/RoomList.qml    | 6 +++---
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index 60954bfd..b5d9c152 100644
--- a/resources/qml/ImageButton.qml
+++ b/resources/qml/ImageButton.qml
@@ -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/RoomList.qml b/resources/qml/RoomList.qml
index 92073a37..76680b37 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -543,7 +543,7 @@ Page {
                 anchors.margins: Nheko.paddingMedium
 
                 ImageButton {
-                    Layout.alignment: Qt.AlignBottom | Qt.AlignLeft
+                    Layout.fillWidth: true
                     hoverEnabled: true
                     width: 22
                     height: 22
@@ -572,7 +572,7 @@ Page {
 
                 ImageButton {
                     visible: !collapsed
-                    Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
+                    Layout.fillWidth: true
                     hoverEnabled: true
                     width: 22
                     height: 22
@@ -584,7 +584,7 @@ Page {
 
                 ImageButton {
                     visible: !collapsed
-                    Layout.alignment: Qt.AlignBottom | Qt.AlignRight
+                    Layout.fillWidth: true
                     hoverEnabled: true
                     width: 22
                     height: 22