summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/qml/ChatPage.qml1
-rw-r--r--resources/qml/Completer.qml6
-rw-r--r--resources/qml/ForwardCompleter.qml4
-rw-r--r--resources/qml/MessageInput.qml62
-rw-r--r--resources/qml/MessageView.qml140
-rw-r--r--resources/qml/QuickSwitcher.qml3
-rw-r--r--resources/qml/Reactions.qml2
-rw-r--r--resources/qml/ReplyPopup.qml2
-rw-r--r--resources/qml/RoomList.qml21
-rw-r--r--resources/qml/Root.qml147
-rw-r--r--resources/qml/StatusIndicator.qml2
-rw-r--r--resources/qml/TimelineView.qml22
-rw-r--r--resources/qml/TopBar.qml14
-rw-r--r--resources/qml/TypingIndicator.qml2
-rw-r--r--resources/qml/delegates/FileMessage.qml2
-rw-r--r--resources/qml/delegates/MessageDelegate.qml10
-rw-r--r--resources/qml/delegates/PlayableMediaMessage.qml4
-rw-r--r--resources/qml/emoji/EmojiButton.qml2
-rw-r--r--resources/qml/voip/ActiveCallBar.qml2
-rw-r--r--resources/qml/voip/CallInviteBar.qml2
-rw-r--r--resources/qml/voip/PlaceCall.qml12
-rw-r--r--resources/qml/voip/ScreenShare.qml4
-rw-r--r--src/ChatPage.cpp7
-rw-r--r--src/timeline/InputBar.cpp35
-rw-r--r--src/timeline/InputBar.h1
-rw-r--r--src/timeline/RoomlistModel.cpp18
-rw-r--r--src/timeline/RoomlistModel.h16
-rw-r--r--src/timeline/TimelineViewManager.cpp125
-rw-r--r--src/timeline/TimelineViewManager.h26
-rw-r--r--src/ui/NhekoDropArea.cpp2
30 files changed, 350 insertions, 346 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,16 +165,25 @@ 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
             }
 
-            HoverHandler {
-                id: hovered
+            TapHandler {
+                onSingleTapped: Rooms.setCurrentRoom(model.roomId)
+                onLongPressed: {
+                    if (!TimelineManager.isInvite) {
+                        roomContextMenu.show(model.roomId, model.tags);
+                    }
+                }
             }
 
-            TapHandler {
-                onSingleTapped: TimelineManager.setHistoryView(model.roomId)
+            HoverHandler {
+                id: hovered
             }
 
             RowLayout {
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
 
@@ -234,16 +97,6 @@ Page {
     }
 
     Connections {
-        target: TimelineManager.timeline
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
-    }
-
-    Connections {
         target: CallManager
         onNewInviteState: {
             if (CallManager.haveCallInvite && Settings.mobileMode) {
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> 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 <mtx/responses/sync.hpp>
 
-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<QString, bool> roomReadStatus_);
 
 signals:
         void totalUnreadMessageCountUpdated(int unreadMessages);
+        void currentRoomChanged();
 
 private:
         void addRoom(const QString &room_id, bool suppressInsertNotification = false);
@@ -85,12 +90,15 @@ private:
         QHash<QString, QSharedPointer<TimelineModel>> models;
         std::map<QString, bool> roomReadStatus;
 
+        QSharedPointer<TimelineModel> 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<mtx::events::msg::KeyVerificationAccept>();
         qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
@@ -193,7 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType<RoomlistModel>(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms);
+                  return new FilteredRoomlistModel(self->rooms_);
           });
         qmlRegisterSingletonType<UserSettings>(
           "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;
@@ -331,36 +331,16 @@ 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<QString> &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<mtx::crypto::EncryptedFile> 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
@@ -36,8 +36,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(
           bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged)
@@ -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<QString, QColor> 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());
                 }