summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2021-05-19 19:34:10 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2021-05-19 19:34:10 +0200
commit10fd2752f9863c43bf7df6c39d7cec1397dfde1c (patch)
treef7aa56102da5ab19ccbd4072c50c662ec7dc301c
parentBasic header and footer of room list (diff)
downloadnheko-10fd2752f9863c43bf7df6c39d7cec1397dfde1c.tar.xz
Some basic room list
-rw-r--r--CMakeLists.txt2
-rw-r--r--resources/qml/ElidedLabel.qml28
-rw-r--r--resources/qml/ForwardCompleter.qml2
-rw-r--r--resources/qml/RoomList.qml171
-rw-r--r--resources/res.qrc1
-rw-r--r--src/timeline/RoomlistModel.cpp146
-rw-r--r--src/timeline/RoomlistModel.h58
-rw-r--r--src/timeline/TimelineViewManager.cpp180
-rw-r--r--src/timeline/TimelineViewManager.h17
9 files changed, 440 insertions, 165 deletions
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 @@
         <file>qml/Completer.qml</file>
         <file>qml/EncryptionIndicator.qml</file>
         <file>qml/ImageButton.qml</file>
+        <file>qml/ElidedLabel.qml</file>
         <file>qml/MatrixText.qml</file>
         <file>qml/MatrixTextField.qml</file>
         <file>qml/ToggleButton.qml</file>
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<QString, QSharedPointer<TimelineModel>>::iterator i;
+                for (i = models.begin(); i != models.end(); ++i) {
+                        auto ptr = i.value();
+
+                        if (!ptr.isNull()) {
+                                ptr->setDecryptDescription(decrypt);
+                                ptr->updateLastMessage();
+                        }
+                }
+        });
+}
+
+QHash<int, QByteArray>
+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<size_t>(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<TimelineModel> 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<mtx::events::ephemeral::Typing>>(
+                                      &ev)) {
+                                        std::vector<QString> 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<QString> &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 <QAbstractListModel>
+#include <QHash>
+#include <QSharedPointer>
+#include <QString>
+
+#include <mtx/responses/sync.hpp>
+
+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<int, QByteArray> 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<TimelineModel> getRoomById(QString id) const
+        {
+                if (models.contains(id))
+                        return models.value(id);
+                else
+                        return {};
+        }
+
+public slots:
+        void initializeRooms(const std::vector<QString> &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<QString> roomids;
+        QHash<QString, QSharedPointer<TimelineModel>> 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
@@ -87,21 +87,6 @@ removeReplyFallback(mtx::events::Event<T> &e)
 }
 
 void
-TimelineViewManager::updateEncryptedDescriptions()
-{
-        auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
-        QHash<QString, QSharedPointer<TimelineModel>>::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()
 {
         userColors.clear();
@@ -148,6 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
+  , rooms(new RoomlistModel(this))
 {
         qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
         qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
@@ -205,6 +191,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                   QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
                   return ptr;
           });
+        qmlRegisterSingletonType<RoomlistModel>(
+          "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->rooms;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType<UserSettings>(
           "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<ChatPage *>(parent),
           &ChatPage::receivedRoomDeviceVerificationRequest,
@@ -334,64 +322,13 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-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);
-
-                if (ChatPage::instance()->userSettings()->typingNotifications()) {
-                        for (const auto &ev : room.ephemeral.events) {
-                                if (auto t = std::get_if<
-                                      mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
-                                      &ev)) {
-                                        std::vector<QString> 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);
-}
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
+{
+        this->rooms->sync(rooms_);
 
-void
-TimelineViewManager::addRoom(const QString &room_id)
-{
-        if (!models.contains(room_id)) {
-                QSharedPointer<TimelineModel> 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<QString> &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<QString> &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<mtx::crypto::EncryptedFile> 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<QString, QSharedPointer<TimelineModel>> models;
         TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
@@ -171,6 +168,8 @@ private:
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
+        RoomlistModel *rooms = nullptr;
+
         QHash<QString, QColor> userColors;
 
         QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;