From 10fd2752f9863c43bf7df6c39d7cec1397dfde1c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 19 May 2021 19:34:10 +0200 Subject: Some basic room list --- src/timeline/RoomlistModel.cpp | 146 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/timeline/RoomlistModel.cpp (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp new file mode 100644 index 00000000..6a1fc3c5 --- /dev/null +++ b/src/timeline/RoomlistModel.cpp @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomlistModel.h" + +#include "ChatPage.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "TimelineModel.h" +#include "TimelineViewManager.h" +#include "UserSettingsPage.h" + +RoomlistModel::RoomlistModel(TimelineViewManager *parent) + : manager(parent) +{ + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); + QHash>::iterator i; + for (i = models.begin(); i != models.end(); ++i) { + auto ptr = i.value(); + + if (!ptr.isNull()) { + ptr->setDecryptDescription(decrypt); + ptr->updateLastMessage(); + } + } + }); +} + +QHash +RoomlistModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {RoomName, "roomName"}, + {LastMessage, "lastMessage"}, + {HasUnreadMessages, "hasUnreadMessages"}, + {NotificationCount, "notificationCount"}, + }; +} + +QVariant +RoomlistModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) { + auto room = models.value(roomids.at(index.row())); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->roomName(); + case Roles::LastMessage: + return QString("Nico: Hahaha, this is funny!"); + case Roles::HasUnreadMessages: + return true; + case Roles::NotificationCount: + return 5; + default: + return {}; + } + } else { + return {}; + } +} + +void +RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) +{ + if (!models.contains(room_id)) { + QSharedPointer newRoom(new TimelineModel(manager, room_id)); + newRoom->setDecryptDescription( + ChatPage::instance()->userSettings()->decryptSidebar()); + + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + manager->imageProvider(), + &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + manager, + &TimelineViewManager::forwardMessageToRoom); + + if (!suppressInsertNotification) + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + models.insert(room_id, std::move(newRoom)); + roomids.push_back(room_id); + if (!suppressInsertNotification) + endInsertRows(); + } +} + +void +RoomlistModel::sync(const mtx::responses::Rooms &rooms) +{ + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->syncState(room.state); + room_model->addEvents(room.timeline); + connect(room_model.data(), + &TimelineModel::newCallEvent, + manager->callManager(), + &CallManager::syncEvent, + Qt::UniqueConnection); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + for (const auto &ev : room.ephemeral.events) { + if (auto t = std::get_if< + mtx::events::EphemeralEvent>( + &ev)) { + std::vector typing; + typing.reserve(t->content.user_ids.size()); + for (const auto &user : t->content.user_ids) { + if (user != http::client()->user_id().to_string()) + typing.push_back( + QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } + } + } +} + +void +RoomlistModel::initializeRooms(const std::vector &roomIds_) +{ + beginResetModel(); + models.clear(); + roomids.clear(); + roomids = roomIds_; + for (const auto &id : roomIds_) + addRoom(id, true); + endResetModel(); +} + +void +RoomlistModel::clear() +{ + beginResetModel(); + models.clear(); + roomids.clear(); + endResetModel(); +} -- cgit 1.5.1 From cd67046f6011f0838b5ed4621fb3ee9b846e63a0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 21 May 2021 21:19:03 +0200 Subject: Make roomlist look nice --- resources/qml/RoomList.qml | 99 ++++++++++++++++++++++++++++++-------- src/CacheStructs.h | 13 +++++ src/ChatPage.cpp | 5 -- src/RoomList.cpp | 5 -- src/timeline/RoomlistModel.cpp | 107 ++++++++++++++++++++++++++++++++++++++--- src/timeline/RoomlistModel.h | 20 +++++++- src/timeline/TimelineModel.cpp | 30 ++++++++++-- src/timeline/TimelineModel.h | 14 ++++++ src/ui/Theme.cpp | 13 +++-- src/ui/Theme.h | 4 +- 10 files changed, 265 insertions(+), 45 deletions(-) (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 87a27517..bb8deda6 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -2,13 +2,15 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.9 +import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.3 import im.nheko 1.0 Page { ListView { + id: roomlist + anchors.left: parent.left anchors.right: parent.right height: parent.height @@ -20,26 +22,80 @@ Page { enabled: !Settings.mobileMode } + Connections { + onActiveTimelineChanged: { + roomlist.positionViewAtIndex(Rooms.roomidToIndex(TimelineManager.timeline.roomId()), ListView.Contain); + console.log("Test" + TimelineManager.timeline.roomId() + " " + Rooms.roomidToIndex(TimelineManager.timeline.roomId)); + } + target: TimelineManager + } + delegate: Rectangle { - color: Nheko.colors.window - height: fontMetrics.lineSpacing * 2.5 + Nheko.paddingMedium * 2 + id: roomItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: Math.ceil(fontMetrics.lineSpacing * 2.3 + Nheko.paddingMedium * 2) width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered + + PropertyChanges { + target: roomItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } - RowLayout { - //id: userInfoGrid + }, + State { + name: "selected" + when: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId() + + PropertyChanges { + target: roomItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + HoverHandler { + id: hovered + } + TapHandler { + onSingleTapped: TimelineManager.setHistoryView(model.roomId) + } + + RowLayout { spacing: Nheko.paddingMedium anchors.fill: parent anchors.margins: Nheko.paddingMedium Avatar { + // In the future we could show an online indicator by setting the userid for the avatar //userid: Nheko.currentUser.userid id: avatar Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2.5 - Layout.preferredHeight: fontMetrics.lineSpacing * 2.5 + height: Math.ceil(fontMetrics.lineSpacing * 2.3) + width: Math.ceil(fontMetrics.lineSpacing * 2.3) url: model.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: model.roomName } @@ -52,7 +108,7 @@ Page { Layout.minimumWidth: 100 width: parent.width - avatar.width Layout.preferredWidth: parent.width - avatar.width - spacing: 0 + spacing: Nheko.paddingSmall RowLayout { Layout.fillWidth: true @@ -60,9 +116,9 @@ Page { ElidedLabel { Layout.alignment: Qt.AlignBottom - color: Nheko.colors.text + color: roomItem.importantText elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium - fullText: model.roomName + ": " + model.notificationCount + fullText: model.roomName } Item { @@ -74,8 +130,8 @@ Page { Layout.alignment: Qt.AlignRight | Qt.AlignBottom font.pixelSize: fontMetrics.font.pixelSize * 0.9 - color: Nheko.colors.buttonText - text: "14:32" + color: roomItem.unimportantText + text: model.timestamp } } @@ -85,10 +141,10 @@ Page { spacing: 0 ElidedLabel { - color: Nheko.colors.buttonText + color: roomItem.unimportantText font.weight: Font.Thin font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: textContent.width - notificationBubble.width + elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall fullText: model.lastMessage } @@ -99,19 +155,24 @@ Page { Rectangle { id: notificationBubble + visible: model.notificationCount > 0 Layout.alignment: Qt.AlignRight - height: fontMetrics.font.pixelSize * 1.3 + height: fontMetrics.averageCharacterWidth * 3 width: height radius: height / 2 - color: Nheko.colors.highlight + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground Label { - anchors.fill: parent + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter fontSizeMode: Text.Fit - color: Nheko.colors.highlightedText - text: model.notificationCount + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.notificationCount } } diff --git a/src/CacheStructs.h b/src/CacheStructs.h index c449f013..f7d6f0e2 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -50,6 +50,19 @@ struct DescInfo QDateTime datetime; }; +inline bool +operator==(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} +inline bool +operator!=(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} + //! UI info associated with a room. struct RoomInfo { diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c5199ff1..58b76174 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -233,11 +233,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) room_list_, &RoomList::updateRoomDescription); - connect(room_list_, - SIGNAL(totalUnreadMessageCountUpdated(int)), - this, - SIGNAL(unreadMessages(int))); - connect( this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 8a807e71..5c41a7a1 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -305,8 +305,6 @@ void RoomList::updateRoomAvatar(const QString &roomid, const QString &img) { if (!roomExists(roomid)) { - nhlog::ui()->warn("avatar update on non-existent room_id: {}", - roomid.toStdString()); return; } @@ -320,9 +318,6 @@ void RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) { if (!roomExists(roomid)) { - nhlog::ui()->warn("description update on non-existent room_id: {}, {}", - roomid.toStdString(), - info.body.toStdString()); return; } diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 6a1fc3c5..5fc4dc65 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -4,6 +4,7 @@ #include "RoomlistModel.h" +#include "Cache_p.h" #include "ChatPage.h" #include "MatrixClient.h" #include "MxcImageProvider.h" @@ -26,6 +27,11 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) } } }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); } QHash @@ -34,8 +40,11 @@ RoomlistModel::roleNames() const return { {AvatarUrl, "avatarUrl"}, {RoomName, "roomName"}, + {RoomId, "roomId"}, {LastMessage, "lastMessage"}, + {Timestamp, "timestamp"}, {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, {NotificationCount, "notificationCount"}, }; } @@ -44,18 +53,26 @@ QVariant RoomlistModel::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) { - auto room = models.value(roomids.at(index.row())); + auto roomid = roomids.at(index.row()); + auto room = models.value(roomid); switch (role) { case Roles::AvatarUrl: return room->roomAvatarUrl(); case Roles::RoomName: return room->roomName(); + case Roles::RoomId: + return room->roomId(); case Roles::LastMessage: - return QString("Nico: Hahaha, this is funny!"); + return room->lastMessage().body; + case Roles::Timestamp: + return room->lastMessage().descriptiveTime; case Roles::HasUnreadMessages: - return true; + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); case Roles::NotificationCount: - return 5; + return room->notificationCount(); default: return {}; } @@ -64,10 +81,38 @@ RoomlistModel::data(const QModelIndex &index, int role) const } } +void +RoomlistModel::updateReadStatus(const std::map roomReadStatus_) +{ + std::vector roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); + } + } + + this->roomReadStatus = roomReadStatus_; + + for (auto idx : roomsToUpdate) { + emit dataChanged(index(idx), + index(idx), + { + Roles::HasUnreadMessages, + }); + } +}; void RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) { if (!models.contains(room_id)) { + // ensure we get read status updates and are only connected once + connect(cache::client(), + &Cache::roomReadStatus, + this, + &RoomlistModel::updateReadStatus, + Qt::UniqueConnection); + QSharedPointer newRoom(new TimelineModel(manager, room_id)); newRoom->setDecryptDescription( ChatPage::instance()->userSettings()->decryptSidebar()); @@ -80,6 +125,56 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) &TimelineModel::forwardToRoom, manager, &TimelineViewManager::forwardMessageToRoom); + connect( + newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::LastMessage, + Roles::Timestamp, + Roles::NotificationCount, + }); + }); + connect( + newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::AvatarUrl, + }); + }); + connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + }); + }); + connect( + newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::NotificationCount, + }); + + int total_unread_msgs = 0; + + for (const auto &room : models) { + if (!room.isNull()) + total_unread_msgs += room->notificationCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); + }); + + newRoom->updateLastMessage(); if (!suppressInsertNotification) beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); @@ -97,8 +192,8 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) // addRoom will only add the room, if it doesn't exist addRoom(QString::fromStdString(room_id)); const auto &room_model = models.value(QString::fromStdString(room_id)); - room_model->syncState(room.state); - room_model->addEvents(room.timeline); + room_model->sync(room); + // room_model->addEvents(room.timeline); connect(room_model.data(), &TimelineModel::newCallEvent, manager->callManager(), diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 44fcf032..c4c9d9ba 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -22,8 +22,11 @@ public: { AvatarUrl = Qt::UserRole, RoomName, + RoomId, LastMessage, + Timestamp, HasUnreadMessages, + HasLoudNotification, NotificationCount, }; @@ -47,6 +50,21 @@ public slots: void initializeRooms(const std::vector &roomids); void sync(const mtx::responses::Rooms &rooms); void clear(); + int roomidToIndex(QString roomid) + { + for (int i = 0; i < (int)roomids.size(); i++) { + if (roomids[i] == roomid) + return i; + } + + return -1; + } + +private slots: + void updateReadStatus(const std::map roomReadStatus_); + +signals: + void totalUnreadMessageCountUpdated(int unreadMessages); private: void addRoom(const QString &room_id, bool suppressInsertNotification = false); @@ -54,5 +72,5 @@ private: TimelineViewManager *manager = nullptr; std::vector roomids; QHash> models; + std::map roomReadStatus; }; - diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 8df17457..19c3fb30 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -723,6 +723,20 @@ TimelineModel::fetchMore(const QModelIndex &) events.fetchMore(); } +void +TimelineModel::sync(const mtx::responses::JoinedRoom &room) +{ + this->syncState(room.state); + this->addEvents(room.timeline); + + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } +} + void TimelineModel::syncState(const mtx::responses::State &s) { @@ -866,14 +880,18 @@ TimelineModel::updateLastMessage() if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { auto time = mtx::accessors::origin_server_ts(*event); uint64_t ts = time.toMSecsSinceEpoch(); - emit manager_->updateRoomsLastMessage( - room_id_, + auto description = DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), QString::fromStdString(http::client()->user_id().to_string()), tr("You joined this room."), utils::descriptiveTime(time), ts, - time}); + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit manager_->updateRoomsLastMessage(room_id_, lastMessage_); + emit lastMessageChanged(); + } return; } if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) @@ -884,7 +902,11 @@ TimelineModel::updateLastMessage() QString::fromStdString(http::client()->user_id().to_string()), cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (description != lastMessage_) { + lastMessage_ = description; + emit manager_->updateRoomsLastMessage(room_id_, description); + emit lastMessageChanged(); + } return; } } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 92fccd2d..5c1065cb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -14,6 +14,7 @@ #include #include "CacheCryptoStructs.h" +#include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" #include "Permissions.h" @@ -253,12 +254,15 @@ public: } void updateLastMessage(); + void sync(const mtx::responses::JoinedRoom &room); void addEvents(const mtx::responses::Timeline &events); void syncState(const mtx::responses::State &state); template void sendMessageEvent(const T &content, mtx::events::EventType eventType); RelatedInfo relatedInfo(QString id); + DescInfo lastMessage() const { return lastMessage_; } + public slots: void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } @@ -309,6 +313,9 @@ public slots: QString roomAvatarUrl() const; QString roomId() const { return room_id_; } + bool hasMentions() { return highlight_count > 0; } + int notificationCount() { return notification_count; } + QString scrollTarget() const; private slots: @@ -328,6 +335,9 @@ signals: void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); + void lastMessageChanged(); + void notificationsChanged(); + void openRoomSettingsDialog(RoomSettings *settings); void newMessageToSend(mtx::events::collections::TimelineEvents event); @@ -372,7 +382,11 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; + DescInfo lastMessage_; + friend struct SendMessageVisitor; + + int notification_count = 0, highlight_count = 0; }; template diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index ca2a4ce0..b6c9579a 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -16,14 +16,15 @@ Theme::paletteFromTheme(std::string_view theme) /*windowText*/ QColor("#333"), /*button*/ QColor("white"), /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(110, 110, 110), + /*dark*/ QColor(70, 77, 93), /*mid*/ QColor(220, 220, 220), /*text*/ QColor("#333"), - /*bright_text*/ QColor("#333"), + /*bright_text*/ QColor("#f2f5f8"), /*base*/ QColor("#fff"), /*window*/ QColor("white")); lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5")); lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); lightActive.setColor(QPalette::Link, QColor("#0077b5")); @@ -34,14 +35,15 @@ Theme::paletteFromTheme(std::string_view theme) /*windowText*/ QColor("#caccd1"), /*button*/ QColor(0xff, 0xff, 0xff), /*light*/ QColor("#caccd1"), - /*dark*/ QColor(110, 110, 110), + /*dark*/ QColor(60, 70, 77), /*mid*/ QColor("#202228"), /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(0xff, 0xff, 0xff), + /*bright_text*/ QColor("#f4f5f8"), /*base*/ QColor("#202228"), /*window*/ QColor("#2d3139")); darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8")); darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); darkActive.setColor(QPalette::Link, QColor("#38a3d8")); @@ -58,9 +60,12 @@ Theme::Theme(std::string_view theme) separator_ = p.mid().color(); if (theme == "light") { sidebarBackground_ = QColor("#233649"); + red_ = QColor("#a82353"); } else if (theme == "dark") { sidebarBackground_ = QColor("#2d3139"); + red_ = QColor("#a82353"); } else { sidebarBackground_ = p.window().color(); + red_ = QColor("red"); } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h index 64bc8273..834571c0 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -66,6 +66,7 @@ class Theme : public QPalette Q_GADGET Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) Q_PROPERTY(QColor separator READ separator CONSTANT) + Q_PROPERTY(QColor red READ red CONSTANT) public: Theme() {} explicit Theme(std::string_view theme); @@ -73,7 +74,8 @@ public: QColor sidebarBackground() const { return sidebarBackground_; } QColor separator() const { return separator_; } + QColor red() const { return red_; } private: - QColor sidebarBackground_, separator_; + QColor sidebarBackground_, separator_, red_; }; -- cgit 1.5.1 From beeb60e4a12b47ae619e52629040aff5a8f43db2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 22 May 2021 00:57:14 +0200 Subject: Sort the room list --- resources/qml/RoomList.qml | 2 +- src/timeline/RoomlistModel.cpp | 93 ++++++++++++++++++++++++++++++++++-- src/timeline/RoomlistModel.h | 26 ++++++++++ src/timeline/TimelineModel.cpp | 2 + src/timeline/TimelineModel.h | 2 +- src/timeline/TimelineViewManager.cpp | 4 +- 6 files changed, 120 insertions(+), 9 deletions(-) (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index bb8deda6..f2a957c9 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -131,7 +131,7 @@ Page { Layout.alignment: Qt.AlignRight | Qt.AlignBottom font.pixelSize: fontMetrics.font.pixelSize * 0.9 color: roomItem.unimportantText - text: model.timestamp + text: model.time } } diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 5fc4dc65..afe9679a 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -42,10 +42,13 @@ RoomlistModel::roleNames() const {RoomName, "roomName"}, {RoomId, "roomId"}, {LastMessage, "lastMessage"}, + {Time, "time"}, {Timestamp, "timestamp"}, {HasUnreadMessages, "hasUnreadMessages"}, {HasLoudNotification, "hasLoudNotification"}, {NotificationCount, "notificationCount"}, + {IsInvite, "isInvite"}, + {IsSpace, "isSpace"}, }; } @@ -64,8 +67,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const return room->roomId(); case Roles::LastMessage: return room->lastMessage().body; - case Roles::Timestamp: + case Roles::Time: return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast(room->lastMessage().timestamp)); case Roles::HasUnreadMessages: return this->roomReadStatus.count(roomid) && this->roomReadStatus.at(roomid); @@ -73,6 +78,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const return room->hasMentions(); case Roles::NotificationCount: return room->notificationCount(); + case Roles::IsInvite: + case Roles::IsSpace: + return false; default: return {}; } @@ -90,9 +98,9 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_) if (roomUnread != roomReadStatus[roomid]) { roomsToUpdate.push_back(this->roomidToIndex(roomid)); } - } - this->roomReadStatus = roomReadStatus_; + this->roomReadStatus[roomid] = roomUnread; + } for (auto idx : roomsToUpdate) { emit dataChanged(index(idx), @@ -135,6 +143,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) Roles::LastMessage, Roles::Timestamp, Roles::NotificationCount, + Qt::DisplayRole, }); }); connect( @@ -162,6 +171,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) { Roles::HasLoudNotification, Roles::NotificationCount, + Qt::DisplayRole, }); int total_unread_msgs = 0; @@ -225,7 +235,6 @@ RoomlistModel::initializeRooms(const std::vector &roomIds_) beginResetModel(); models.clear(); roomids.clear(); - roomids = roomIds_; for (const auto &id : roomIds_) addRoom(id, true); endResetModel(); @@ -239,3 +248,79 @@ RoomlistModel::clear() roomids.clear(); endResetModel(); } + +namespace { +enum NotificationImportance : short +{ + ImportanceDisabled = -1, + AllEventsRead = 0, + NewMessage = 1, + NewMentions = 2, + Invite = 3 +}; +} + +short int +FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const +{ + // Returns the degree of importance of the unread messages in the room. + // If sorting by importance is disabled in settings, this only ever + // returns ImportanceDisabled or Invite + if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + return Invite; + } else if (!this->sortByImportance) { + return ImportanceDisabled; + } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { + return NewMentions; + } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { + return NewMessage; + } else { + return AllEventsRead; + } +} +bool +FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); + QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); + + // Sort by "importance" (i.e. invites before mentions before + // notifs before new events before old events), then secondly + // by recency. + + // Checking importance first + const auto a_importance = calculateImportance(left_idx); + const auto b_importance = calculateImportance(right_idx); + if (a_importance != b_importance) { + return a_importance > b_importance; + } + + // Now sort by recency + // Zero if empty, otherwise the time that the event occured + uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); + uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); + + if (a_recency != b_recency) + return a_recency > b_recency; + else + return left.row() < right.row(); +} + +FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent) + : QSortFilterProxyModel(parent) + , roomlistmodel(model) +{ + this->sortByImportance = UserSettings::instance()->sortByImportance(); + setSourceModel(model); + setDynamicSortFilter(true); + + QObject::connect(UserSettings::instance().get(), + &UserSettings::roomSortingChanged, + this, + [this](bool sortByImportance_) { + this->sortByImportance = sortByImportance_; + invalidate(); + }); + + sort(0); +} diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index c4c9d9ba..c3374bd2 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -24,10 +25,13 @@ public: RoomName, RoomId, LastMessage, + Time, Timestamp, HasUnreadMessages, HasLoudNotification, NotificationCount, + IsInvite, + IsSpace, }; RoomlistModel(TimelineViewManager *parent = nullptr); @@ -73,4 +77,26 @@ private: std::vector roomids; QHash> models; std::map roomReadStatus; + + friend class FilteredRoomlistModel; +}; + +class FilteredRoomlistModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +public slots: + int roomidToIndex(QString roomid) + { + return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) + .row(); + } + +private: + short int calculateImportance(const QModelIndex &idx) const; + RoomlistModel *roomlistmodel; + bool sortByImportance = true; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 19c3fb30..2625127c 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , room_id_(room_id) , manager_(manager) { + lastMessage_.timestamp = 0; + connect( this, &TimelineModel::redactionFailed, diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 5c1065cb..b3d3b663 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -382,7 +382,7 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; - DescInfo lastMessage_; + DescInfo lastMessage_{}; friend struct SendMessageVisitor; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index b0c13b03..c84e0df8 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -193,9 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par }); qmlRegisterSingletonType( "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = self->rooms; - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; + return new FilteredRoomlistModel(self->rooms); }); qmlRegisterSingletonType( "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { -- cgit 1.5.1 From f3d956aebcd34ecf8c4e2c44acd39d5de380233f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 22 May 2021 10:16:42 +0200 Subject: Fix emoji in new RoomList --- resources/qml/ElidedLabel.qml | 2 +- resources/qml/RoomList.qml | 2 ++ src/timeline/RoomlistModel.cpp | 2 +- src/timeline/TimelineModel.cpp | 11 +++++++++++ src/timeline/TimelineModel.h | 1 + src/timeline/TimelineViewManager.h | 1 + 6 files changed, 17 insertions(+), 2 deletions(-) (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml index 5ae99de7..1f4aeeea 100644 --- a/resources/qml/ElidedLabel.qml +++ b/resources/qml/ElidedLabel.qml @@ -13,7 +13,7 @@ Label { property alias elideWidth: metrics.elideWidth color: Nheko.colors.text - text: metrics.elidedText + text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(TimelineManager.htmlEscape(metrics.elidedText)) maximumLineCount: 1 elide: Text.ElideRight textFormat: Text.PlainText diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index f2a957c9..89af78a5 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -119,6 +119,7 @@ Page { color: roomItem.importantText elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium fullText: model.roomName + textFormat: Text.RichText } Item { @@ -146,6 +147,7 @@ Page { font.pixelSize: fontMetrics.font.pixelSize * 0.9 elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall fullText: model.lastMessage + textFormat: Text.RichText } Item { diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index afe9679a..6d741322 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -62,7 +62,7 @@ RoomlistModel::data(const QModelIndex &index, int role) const case Roles::AvatarUrl: return room->roomAvatarUrl(); case Roles::RoomName: - return room->roomName(); + return room->plainRoomName(); case Roles::RoomId: return room->roomId(); case Roles::LastMessage: diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 2625127c..8f4a8564 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1890,6 +1890,17 @@ TimelineModel::roomName() const QString::fromStdString(info[room_id_].name).toHtmlEscaped()); } +QString +TimelineModel::plainRoomName() const +{ + auto info = cache::getRoomInfo({room_id_.toStdString()}); + + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); +} + QString TimelineModel::roomAvatarUrl() const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b3d3b663..3ebbe120 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -307,6 +307,7 @@ public slots: } QString roomName() const; + QString plainRoomName() const; QString roomTopic() const; InputBar *input() { return &input_; } Permissions *permissions() { return &permissions_; } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index f4297243..609f5a4a 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -67,6 +67,7 @@ public: Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; -- cgit 1.5.1 From d307f24adf346ec3fe41d40a599db98d7eb435cf Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 22 May 2021 11:23:16 +0200 Subject: Fix leaving rooms --- resources/qml/RoomList.qml | 2 +- src/timeline/RoomlistModel.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 89af78a5..979e727d 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -46,7 +46,7 @@ Page { states: [ State { name: "highlight" - when: hovered.hovered + when: hovered.hovered && !(TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId()) PropertyChanges { target: roomItem diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 6d741322..28c3cf46 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -227,6 +227,17 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) } } } + + for (const auto &[room_id, room] : rooms.leave) { + (void)room; + auto idx = this->roomidToIndex(QString::fromStdString(room_id)); + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + models.remove(QString::fromStdString(room_id)); + endRemoveRows(); + } + } } void -- cgit 1.5.1 From c290b0747f34a6f683365f93d64ce93dc4428ca8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 24 May 2021 14:04:07 +0200 Subject: Reenable invites --- resources/qml/RoomList.qml | 56 ++++++++++++ src/Cache.cpp | 50 +++++++++-- src/Cache.h | 2 +- src/Cache_p.h | 3 +- src/ChatPage.cpp | 4 +- src/ChatPage.h | 2 +- src/RoomList.cpp | 2 +- src/RoomList.h | 2 +- src/timeline/RoomlistModel.cpp | 169 ++++++++++++++++++++++++++++------- src/timeline/RoomlistModel.h | 8 +- src/timeline/TimelineViewManager.cpp | 4 +- src/timeline/TimelineViewManager.h | 2 +- src/ui/Theme.cpp | 3 + src/ui/Theme.h | 4 +- 14 files changed, 260 insertions(+), 51 deletions(-) (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 40669eda..e9bb351f 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -141,6 +141,8 @@ Page { RowLayout { Layout.fillWidth: true spacing: 0 + visible: !model.isInvite + height: visible ? 0 : undefined ElidedLabel { color: roomItem.unimportantText @@ -182,6 +184,60 @@ Page { } + RowLayout { + Layout.fillWidth: true + spacing: Nheko.paddingMedium + visible: model.isInvite + enabled: visible + height: visible ? 0 : undefined + + ElidedLabel { + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Accept") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.acceptInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + ElidedLabel { + Layout.alignment: Qt.AlignRight + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Decline") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.declineInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + Item { + Layout.fillWidth: true + } + + } + } } diff --git a/src/Cache.cpp b/src/Cache.cpp index c41b66cc..4a99dd59 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2045,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) return fallbackDesc; } -std::map +QHash Cache::invites() { - std::map result; + QHash result; auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); auto cursor = lmdb::cursor::open(txn, invitesDb_); - std::string_view room_id, unused; + std::string_view room_id, room_data; - while (cursor.get(room_id, unused, MDB_NEXT)) - result.emplace(QString::fromStdString(std::string(room_id)), true); + while (cursor.get(room_id, room_data, MDB_NEXT)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + room_id, + std::string(room_data), + e.what()); + } + } cursor.close(); - txn.commit(); + + return result; +} + +std::optional +Cache::invite(std::string_view roomid) +{ + std::optional result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::string_view room_data; + + if (invitesDb_.get(txn, roomid, room_data)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); + result = std::move(tmp); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + roomid, + std::string(room_data), + e.what()); + } + } return result; } @@ -4064,7 +4100,7 @@ roomInfo(bool withInvites) { return instance_->roomInfo(withInvites); } -std::map +QHash invites() { return instance_->invites(); diff --git a/src/Cache.h b/src/Cache.h index 427dbafc..74ec9695 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -62,7 +62,7 @@ joinedRooms(); QMap roomInfo(bool withInvites = true); -std::map +QHash invites(); //! Calculate & return the name of the room. diff --git a/src/Cache_p.h b/src/Cache_p.h index c55fa601..f2911622 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -70,7 +70,8 @@ public: QMap roomInfo(bool withInvites = true); std::optional getRoomAliases(const std::string &roomid); - std::map invites(); + QHash invites(); + std::optional invite(std::string_view roomid); //! Calculate & return the name of the room. QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 58b76174..166c03ec 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -313,7 +313,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::initializeEmptyViews, view_manager_, - &TimelineViewManager::initWithMessages); + &TimelineViewManager::initializeRoomlist); connect(this, &ChatPage::initializeMentions, user_mentions_popup_, @@ -554,7 +554,7 @@ ChatPage::loadStateFromCache() try { olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - emit initializeEmptyViews(cache::client()->roomIds()); + emit initializeEmptyViews(); emit initializeRoomList(cache::roomInfo()); emit initializeMentions(cache::getTimelineMentions()); emit syncTags(cache::roomInfo().toStdMap()); diff --git a/src/ChatPage.h b/src/ChatPage.h index 84e7cdff..eb60047d 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -147,7 +147,7 @@ signals: void initializeRoomList(QMap); void initializeViews(const mtx::responses::Rooms &rooms); - void initializeEmptyViews(const std::vector &roomIds); + void initializeEmptyViews(); void initializeMentions(const QMap ¬ifs); void syncUI(const mtx::responses::Rooms &rooms); void syncRoomlist(const std::map &updates); diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 5c41a7a1..5839c4a0 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -183,7 +183,7 @@ RoomList::initialize(const QMap &info) } void -RoomList::cleanupInvites(const std::map &invites) +RoomList::cleanupInvites(const QHash &invites) { if (invites.size() == 0) return; diff --git a/src/RoomList.h b/src/RoomList.h index 74152c55..af792fd7 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -48,7 +48,7 @@ public: //! Show all the available rooms. void removeFilter(const std::set &roomsToHide); void updateRoom(const QString &room_id, const RoomInfo &info); - void cleanupInvites(const std::map &invites); + void cleanupInvites(const QHash &invites); signals: void roomChanged(const QString &room_id); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 28c3cf46..f3d4dad7 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -57,31 +57,64 @@ RoomlistModel::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) { auto roomid = roomids.at(index.row()); - auto room = models.value(roomid); - switch (role) { - case Roles::AvatarUrl: - return room->roomAvatarUrl(); - case Roles::RoomName: - return room->plainRoomName(); - case Roles::RoomId: - return room->roomId(); - case Roles::LastMessage: - return room->lastMessage().body; - case Roles::Time: - return room->lastMessage().descriptiveTime; - case Roles::Timestamp: - return QVariant(static_cast(room->lastMessage().timestamp)); - case Roles::HasUnreadMessages: - return this->roomReadStatus.count(roomid) && - this->roomReadStatus.at(roomid); - case Roles::HasLoudNotification: - return room->hasMentions(); - case Roles::NotificationCount: - return room->notificationCount(); - case Roles::IsInvite: - case Roles::IsSpace: - return false; - default: + + if (models.contains(roomid)) { + auto room = models.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->plainRoomName(); + case Roles::RoomId: + return room->roomId(); + case Roles::LastMessage: + return room->lastMessage().body; + case Roles::Time: + return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant( + static_cast(room->lastMessage().timestamp)); + case Roles::HasUnreadMessages: + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); + case Roles::NotificationCount: + return room->notificationCount(); + case Roles::IsInvite: + case Roles::IsSpace: + return false; + default: + return {}; + } + } else if (invites.contains(roomid)) { + auto room = invites.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::RoomId: + return roomid; + case Roles::LastMessage: + return room.msgInfo.body; + case Roles::Time: + return room.msgInfo.descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast(room.msgInfo.timestamp)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return true; + case Roles::IsSpace: + return false; + default: + return {}; + } + } else { return {}; } } else { @@ -109,7 +142,7 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_) Roles::HasUnreadMessages, }); } -}; +} void RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) { @@ -186,11 +219,21 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) newRoom->updateLastMessage(); - if (!suppressInsertNotification) + bool wasInvite = invites.contains(room_id); + if (!suppressInsertNotification && !wasInvite) beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + models.insert(room_id, std::move(newRoom)); - roomids.push_back(room_id); - if (!suppressInsertNotification) + + if (wasInvite) { + auto idx = roomidToIndex(room_id); + invites.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else { + roomids.push_back(room_id); + } + + if (!suppressInsertNotification && !wasInvite) endInsertRows(); } } @@ -234,20 +277,50 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) if (idx != -1) { beginRemoveRows(QModelIndex(), idx, idx); roomids.erase(roomids.begin() + idx); - models.remove(QString::fromStdString(room_id)); + if (models.contains(QString::fromStdString(room_id))) + models.remove(QString::fromStdString(room_id)); + else if (invites.contains(QString::fromStdString(room_id))) + invites.remove(QString::fromStdString(room_id)); endRemoveRows(); } } + + for (const auto &[room_id, room] : rooms.invite) { + (void)room_id; + auto qroomid = QString::fromStdString(room_id); + + auto invite = cache::client()->invite(room_id); + if (!invite) + continue; + + if (invites.contains(qroomid)) { + invites[qroomid] = *invite; + auto idx = roomidToIndex(qroomid); + emit dataChanged(index(idx), index(idx)); + } else { + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + invites.insert(qroomid, *invite); + roomids.push_back(std::move(qroomid)); + endInsertRows(); + } + } } void -RoomlistModel::initializeRooms(const std::vector &roomIds_) +RoomlistModel::initializeRooms() { beginResetModel(); models.clear(); roomids.clear(); - for (const auto &id : roomIds_) + invites.clear(); + + invites = cache::client()->invites(); + for (const auto &id : invites.keys()) + roomids.push_back(id); + + for (const auto &id : cache::client()->roomIds()) addRoom(id, true); + endResetModel(); } @@ -256,10 +329,42 @@ RoomlistModel::clear() { beginResetModel(); models.clear(); + invites.clear(); roomids.clear(); endResetModel(); } +void +RoomlistModel::acceptInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->joinRoom(roomid); + } + } +} +void +RoomlistModel::declineInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} + namespace { enum NotificationImportance : short { diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index c3374bd2..ff85614c 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -51,7 +52,7 @@ public: } public slots: - void initializeRooms(const std::vector &roomids); + void initializeRooms(); void sync(const mtx::responses::Rooms &rooms); void clear(); int roomidToIndex(QString roomid) @@ -63,6 +64,8 @@ public slots: return -1; } + void acceptInvite(QString roomid); + void declineInvite(QString roomid); private slots: void updateReadStatus(const std::map roomReadStatus_); @@ -75,6 +78,7 @@ private: TimelineViewManager *manager = nullptr; std::vector roomids; + QHash invites; QHash> models; std::map roomReadStatus; @@ -94,6 +98,8 @@ public slots: return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) .row(); } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } private: short int calculateImportance(const QModelIndex &idx) const; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index c84e0df8..9fa7f8b6 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -499,9 +499,9 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s } void -TimelineViewManager::initWithMessages(const std::vector &roomIds) +TimelineViewManager::initializeRoomlist() { - rooms->initializeRooms(roomIds); + rooms->initializeRooms(); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 609f5a4a..37e50804 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -100,7 +100,7 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void initWithMessages(const std::vector &roomIds); + void initializeRoomlist(); void chatFocusChanged(bool focused) { isWindowFocused_ = focused; diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index b6c9579a..26119393 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -60,12 +60,15 @@ Theme::Theme(std::string_view theme) separator_ = p.mid().color(); if (theme == "light") { sidebarBackground_ = QColor("#233649"); + alternateButton_ = QColor("#ccc"); red_ = QColor("#a82353"); } else if (theme == "dark") { sidebarBackground_ = QColor("#2d3139"); + alternateButton_ = QColor("#414A59"); red_ = QColor("#a82353"); } else { sidebarBackground_ = p.window().color(); + alternateButton_ = p.dark().color(); red_ = QColor("red"); } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h index 834571c0..b5bcd4dd 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -65,6 +65,7 @@ class Theme : public QPalette { Q_GADGET Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) + Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) Q_PROPERTY(QColor separator READ separator CONSTANT) Q_PROPERTY(QColor red READ red CONSTANT) public: @@ -73,9 +74,10 @@ public: static QPalette paletteFromTheme(std::string_view theme); QColor sidebarBackground() const { return sidebarBackground_; } + QColor alternateButton() const { return alternateButton_; } QColor separator() const { return separator_; } QColor red() const { return red_; } private: - QColor sidebarBackground_, separator_, red_; + QColor sidebarBackground_, separator_, red_, alternateButton_; }; -- cgit 1.5.1 From e2765212fb229e8d025d2255314a04a376207749 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 28 May 2021 17:25:46 +0200 Subject: Reimplement room context menus --- resources/qml/RoomList.qml | 132 ++++++++++++++++++++++++-------- resources/qml/delegates/TextMessage.qml | 2 +- resources/qml/dialogs/InputDialog.qml | 53 +++++++++++++ resources/res.qrc | 1 + src/timeline/RoomlistModel.cpp | 72 +++++++++++++++++ src/timeline/RoomlistModel.h | 6 ++ 6 files changed, 232 insertions(+), 34 deletions(-) create mode 100644 resources/qml/dialogs/InputDialog.qml (limited to 'src/timeline/RoomlistModel.cpp') diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index e9bb351f..b184aef0 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "./dialogs" import Qt.labs.platform 1.1 as Platform import QtQuick 2.13 import QtQuick.Controls 2.13 @@ -31,6 +32,93 @@ Page { target: TimelineManager } + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + roomContextMenu.clear(); + roomContextMenu.addItem(leaveOpt.createObject(roomContextMenu)); + roomContextMenu.addItem(separatorOpt.createObject(roomContextMenu)); + for (let tag of Rooms.tags()) { + roomContextMenu.addItem(tagDelegate.createObject(roomContextMenu, { + "t": tag + })); + } + roomContextMenu.addItem(newTagOpt.createObject(roomContextMenu)); + open(); + } + + InputDialog { + id: newTag + + title: qsTr("New tag") + prompt: qsTr("Enter the tag you want to use:") + onAccepted: function(text) { + Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); + } + } + + Component { + id: leaveOpt + + Platform.MenuItem { + text: qsTr("Leave room") + onTriggered: Rooms.leave(roomContextMenu.roomid) + } + + } + + Component { + id: separatorOpt + + Platform.MenuSeparator { + text: qsTr("Tag room as:") + } + + } + + Component { + id: tagDelegate + + Platform.MenuItem { + property string t + + text: { + switch (t) { + case "m.favourite": + return qsTr("Favourite"); + case "m.lowpriority": + return qsTr("Low priority"); + case "m.server_notice": + return qsTr("Server notice"); + default: + return t.substring(2); + } + } + checkable: true + checked: roomContextMenu.tags.includes(t) + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + } + + Component { + id: newTagOpt + + Platform.MenuItem { + text: qsTr("Create new tag...") + onTriggered: newTag.show() + } + + } + + } + delegate: Rectangle { id: roomItem @@ -75,6 +163,12 @@ Page { } ] + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: roomContextMenu.show(model.roomId, model.tags) + gesturePolicy: TapHandler.ReleaseWithinBounds + } + HoverHandler { id: hovered } @@ -94,6 +188,7 @@ Page { id: avatar + enabled: false Layout.alignment: Qt.AlignVCenter height: Math.ceil(fontMetrics.lineSpacing * 2.3) width: Math.ceil(fontMetrics.lineSpacing * 2.3) @@ -279,43 +374,14 @@ Page { Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium Layout.minimumHeight: 40 - ApplicationWindow { + InputDialog { id: statusDialog - modality: Qt.NonModal - flags: Qt.Dialog title: qsTr("Status Message") - width: 350 - height: fontMetrics.lineSpacing * 7 - - ColumnLayout { - anchors.margins: Nheko.paddingLarge - anchors.fill: parent - - Label { - color: Nheko.colors.text - text: qsTr("Enter your status message:") - } - - MatrixTextField { - id: statusInput - - Layout.fillWidth: true - } - - } - - footer: DialogButtonBox { - standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel - onAccepted: { - Nheko.setStatusMessage(statusInput.text); - statusDialog.close(); - } - onRejected: { - statusDialog.close(); - } + prompt: qsTr("Enter your status message:") + onAccepted: function(text) { + Nheko.setStatusMessage(text); } - } Platform.Menu { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index ae622480..f65eda79 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -9,7 +9,7 @@ MatrixText { property string formatted: model.data.formattedBody property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body - text: "" + formatted.replace("
", "
")
+    text: "" + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
     width: parent ? parent.width : undefined
     height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
new file mode 100644
index 00000000..0cd6be1c
--- /dev/null
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.13
+import QtQuick.Controls 2.13
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: inputDialog
+
+    property alias prompt: promptLabel.text
+    property var onAccepted: undefined
+
+    modality: Qt.NonModal
+    flags: Qt.Dialog
+    width: 350
+    height: fontMetrics.lineSpacing * 7
+
+    ColumnLayout {
+        anchors.margins: Nheko.paddingLarge
+        anchors.fill: parent
+
+        Label {
+            id: promptLabel
+
+            color: Nheko.colors.text
+        }
+
+        MatrixTextField {
+            id: statusInput
+
+            Layout.fillWidth: true
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+        onAccepted: {
+            if (inputDialog.onAccepted)
+                inputDialog.onAccepted(statusInput.text);
+
+            inputDialog.close();
+        }
+        onRejected: {
+            inputDialog.close();
+        }
+    }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 79e63810..183cf394 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -168,6 +168,7 @@
         qml/device-verification/NewVerificationRequest.qml
         qml/device-verification/Failed.qml
         qml/device-verification/Success.qml
+        qml/dialogs/InputDialog.qml
         qml/ui/Ripple.qml
         qml/voip/ActiveCallBar.qml
         qml/voip/CallDevices.qml
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f3d4dad7..63054aa9 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -49,6 +49,7 @@ RoomlistModel::roleNames() const
           {NotificationCount, "notificationCount"},
           {IsInvite, "isInvite"},
           {IsSpace, "isSpace"},
+          {Tags, "tags"},
         };
 }
 
@@ -84,6 +85,13 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         case Roles::IsInvite:
                         case Roles::IsSpace:
                                 return false;
+                        case Roles::Tags: {
+                                auto info = cache::singleRoomInfo(roomid.toStdString());
+                                QStringList list;
+                                for (const auto &t : info.tags)
+                                        list.push_back(QString::fromStdString(t));
+                                return list;
+                        }
                         default:
                                 return {};
                         }
@@ -111,6 +119,8 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                                 return true;
                         case Roles::IsSpace:
                                 return false;
+                        case Roles::Tags:
+                                return QStringList();
                         default:
                                 return {};
                         }
@@ -364,6 +374,21 @@ RoomlistModel::declineInvite(QString roomid)
                 }
         }
 }
+void
+RoomlistModel::leave(QString roomid)
+{
+        if (models.contains(roomid)) {
+                auto idx = roomidToIndex(roomid);
+
+                if (idx != -1) {
+                        beginRemoveRows(QModelIndex(), idx, idx);
+                        roomids.erase(roomids.begin() + idx);
+                        models.remove(roomid);
+                        endRemoveRows();
+                        ChatPage::instance()->leaveRoom(roomid);
+                }
+        }
+}
 
 namespace {
 enum NotificationImportance : short
@@ -440,3 +465,50 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
 
         sort(0);
 }
+
+QStringList
+FilteredRoomlistModel::tags()
+{
+        std::set ts;
+        for (const auto &e : cache::roomInfo()) {
+                for (const auto &t : e.tags) {
+                        if (t.find("u.") == 0) {
+                                ts.insert(t);
+                        }
+                }
+        }
+
+        QStringList ret{{
+          "m.favourite",
+          "m.lowpriority",
+        }};
+
+        for (const auto &t : ts)
+                ret.push_back(QString::fromStdString(t));
+
+        return ret;
+}
+
+void
+FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
+{
+        if (on) {
+                http::client()->put_tag(
+                  roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::ui()->error("Failed to add tag: {}, {}",
+                                                     tag.toStdString(),
+                                                     err->matrix_error.error);
+                          }
+                  });
+        } else {
+                http::client()->delete_tag(
+                  roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
+                          if (err) {
+                                  nhlog::ui()->error("Failed to delete tag: {}, {}",
+                                                     tag.toStdString(),
+                                                     err->matrix_error.error);
+                          }
+                  });
+        }
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index ff85614c..2d1e5264 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -33,6 +34,7 @@ public:
                 NotificationCount,
                 IsInvite,
                 IsSpace,
+                Tags,
         };
 
         RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -66,6 +68,7 @@ public slots:
         }
         void acceptInvite(QString roomid);
         void declineInvite(QString roomid);
+        void leave(QString roomid);
 
 private slots:
         void updateReadStatus(const std::map roomReadStatus_);
@@ -100,6 +103,9 @@ public slots:
         }
         void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
         void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
+        void leave(QString roomid) { roomlistmodel->leave(roomid); }
+        QStringList tags();
+        void toggleTag(QString roomid, QString tag, bool on);
 
 private:
         short int calculateImportance(const QModelIndex &idx) const;
-- 
cgit 1.5.1


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

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
 
@@ -233,16 +96,6 @@ Page {
         }
     }
 
-    Connections {
-        target: TimelineManager.timeline
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
-    }
-
     Connections {
         target: CallManager
         onNewInviteState: {
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 3d2d8278..739cc007 100644
--- a/resources/qml/StatusIndicator.qml
+++ b/resources/qml/StatusIndicator.qml
@@ -31,7 +31,7 @@ ImageButton {
     }
     onClicked: {
         if (model.state == MtxEvent.Read)
-            TimelineManager.timeline.readReceiptsAction(model.id);
+            room.readReceiptsAction(model.id);
 
     }
     image: {
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 257d670d..747be61e 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -18,8 +18,10 @@ import im.nheko.EmojiModel 1.0
 Item {
     id: timelineView
 
+    property var room: null
+
     Label {
-        visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
+        visible: !room && !TimelineManager.isInitialSync
         anchors.centerIn: parent
         text: qsTr("No room open")
         font.pointSize: 24
@@ -38,7 +40,7 @@ Item {
     ColumnLayout {
         id: timelineLayout
 
-        visible: TimelineManager.timeline != null
+        visible: room != null
         anchors.fill: parent
         spacing: 0
 
@@ -69,11 +71,11 @@ Item {
                     currentIndex: 0
 
                     Connections {
-                        function onActiveTimelineChanged() {
+                        function onRoomChanged() {
                             stackLayout.currentIndex = 0;
                         }
 
-                        target: TimelineManager
+                        target: timelineView
                     }
 
                     MessageView {
@@ -125,7 +127,17 @@ Item {
 
     NhekoDropArea {
         anchors.fill: parent
-        roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
+        roomid: room ? room.roomId() : ""
+    }
+
+    Connections {
+        target: room
+        onOpenRoomSettingsDialog: {
+            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
+                "roomSettings": settings
+            });
+            roomSettings.show();
+        }
     }
 
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index bda5ce14..65e27939 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -11,8 +11,6 @@ import im.nheko 1.0
 Rectangle {
     id: topBar
 
-    property var room: TimelineManager.timeline
-
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
@@ -20,7 +18,7 @@ Rectangle {
 
     TapHandler {
         onSingleTapped: {
-            TimelineManager.timeline.openRoomSettings();
+            room.openRoomSettings();
             eventPoint.accepted = true;
         }
         gesturePolicy: TapHandler.ReleaseWithinBounds
@@ -61,7 +59,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
             displayName: room ? room.roomName : qsTr("No room selected")
-            onClicked: TimelineManager.timeline.openRoomSettings()
+            onClicked: room.openRoomSettings()
         }
 
         Label {
@@ -101,24 +99,24 @@ Rectangle {
                 id: roomOptionsMenu
 
                 Platform.MenuItem {
-                    visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false
+                    visible: room ? room.permissions.canInvite() : false
                     text: qsTr("Invite users")
                     onTriggered: TimelineManager.openInviteUsersDialog()
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Members")
-                    onTriggered: TimelineManager.openMemberListDialog()
+                    onTriggered: TimelineManager.openMemberListDialog(room.roomId())
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog()
+                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Settings")
-                    onTriggered: TimelineManager.timeline.openRoomSettings()
+                    onTriggered: room.openRoomSettings()
                 }
 
             }
diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml
index 783a9ebc..974d1840 100644
--- a/resources/qml/TypingIndicator.qml
+++ b/resources/qml/TypingIndicator.qml
@@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
 Item {
-    property var room: TimelineManager.timeline
-
     implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
     Layout.fillWidth: true
 
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 2e5f33c2..0392c73a 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -34,7 +34,7 @@ Item {
             }
 
             TapHandler {
-                onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
+                onSingleTapped: room.saveMedia(model.data.id)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 4e6a73fe..9e076a7a 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -207,7 +207,7 @@ Item {
             roleValue: MtxEvent.PowerLevels
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
+                text: room.formatPowerLevelEvent(model.data.id)
             }
 
         }
@@ -216,7 +216,7 @@ Item {
             roleValue: MtxEvent.RoomJoinRules
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
+                text: room.formatJoinRuleEvent(model.data.id)
             }
 
         }
@@ -225,7 +225,7 @@ Item {
             roleValue: MtxEvent.RoomHistoryVisibility
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
+                text: room.formatHistoryVisibilityEvent(model.data.id)
             }
 
         }
@@ -234,7 +234,7 @@ Item {
             roleValue: MtxEvent.RoomGuestAccess
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
+                text: room.formatGuestAccessEvent(model.data.id)
             }
 
         }
@@ -243,7 +243,7 @@ Item {
             roleValue: MtxEvent.Member
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatMemberEvent(model.data.id)
+                text: room.formatMemberEvent(model.data.id)
             }
 
         }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 0234495d..83864db9 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -121,7 +121,7 @@ Rectangle {
                 onClicked: {
                     switch (button.state) {
                     case "":
-                        TimelineManager.timeline.cacheMedia(model.data.id);
+                        room.cacheMedia(model.data.id);
                         break;
                     case "stopped":
                         media.play();
@@ -174,7 +174,7 @@ Rectangle {
                 }
 
                 Connections {
-                    target: TimelineManager.timeline
+                    target: room
                     onMediaCached: {
                         if (mxcUrl == model.data.url) {
                             media.source = cacheUrl;
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index cec51d75..5f4d23d3 100644
--- a/resources/qml/emoji/EmojiButton.qml
+++ b/resources/qml/emoji/EmojiButton.qml
@@ -17,7 +17,7 @@ ImageButton {
 
     image: ":/icons/icons/ui/smile.png"
     onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
-        TimelineManager.queueReactionMessage(event_id, emoji);
+        room.input.reaction(event_id, emoji);
         TimelineManager.focusMessageInput();
     })
 }
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 5798433a..3106c382 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -35,7 +35,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index a169aca9..2d8e3040 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -42,7 +42,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 7e2146cb..97e39e02 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -45,7 +45,7 @@ Popup {
             Layout.leftMargin: 8
 
             Label {
-                text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName)
+                text: qsTr("Place a call to %1?").arg(room.roomName)
                 color: Nheko.colors.windowText
             }
 
@@ -77,9 +77,9 @@ Popup {
                 Layout.rightMargin: cameraCombo.visible ? 16 : 64
                 width: Nheko.avatarSize
                 height: Nheko.avatarSize
-                url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
-                displayName: TimelineManager.timeline.roomName
-                onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+                url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
+                displayName: room.roomName
+                onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
             }
 
             Button {
@@ -88,7 +88,7 @@ Popup {
                 onClicked: {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE);
+                        CallManager.sendInvite(room.roomId(), CallType.VOICE);
                         close();
                     }
                 }
@@ -102,7 +102,7 @@ Popup {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
                         Settings.camera = cameraCombo.currentText;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO);
+                        CallManager.sendInvite(room.roomId(), CallType.VIDEO);
                         close();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index 258ac9b0..a10057b2 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -27,7 +27,7 @@ Popup {
             Layout.leftMargin: 8
             Layout.rightMargin: 8
             Layout.alignment: Qt.AlignLeft
-            text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
+            text: qsTr("Share desktop with %1?").arg(room.roomName)
             color: Nheko.colors.windowText
         }
 
@@ -136,7 +136,7 @@ Popup {
                         Settings.screenSharePiP = pipCheckBox.checked;
                         Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
                         Settings.screenShareHideCursor = hideCursorCheckBox.checked;
-                        CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex);
+                        CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
                         close();
                     }
                 }
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 166c03ec..bee20d60 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -215,8 +215,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 this->current_room_ = room_id;
         });
         connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
-        connect(
-          room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
 
         connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
                 joinRoom(room_id);
@@ -982,7 +980,7 @@ ChatPage::leaveRoom(const QString &room_id)
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->setHistoryView(room_id);
+        view_manager_->rooms()->setCurrentRoom(room_id);
         room_list_->highlightSelectedRoom(room_id);
 }
 
@@ -1397,7 +1395,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
 
         if (sigil1 == "u") {
                 if (action.isEmpty()) {
-                        view_manager_->activeTimeline()->openUserProfile(mxid1);
+                        if (auto t = view_manager_->rooms()->currentRoom())
+                                t->openUserProfile(mxid1);
                 } else if (action == "chat") {
                         this->startChat(mxid1);
                 }
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..a283d24e 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -508,8 +508,7 @@ InputBar::command(QString command, QString args)
         } else if (command == "react") {
                 auto eventId = room->reply();
                 if (!eventId.isEmpty())
-                        ChatPage::instance()->timelineManager()->queueReactionMessage(
-                          eventId, args.trimmed());
+                        reaction(eventId, args.trimmed());
         } else if (command == "join") {
                 ChatPage::instance()->joinRoom(args);
         } else if (command == "part" || command == "leave") {
@@ -715,3 +714,35 @@ InputBar::stopTyping()
                 }
         });
 }
+
+void
+InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
+{
+        auto reactions = room->reactions(reactedEvent.toStdString());
+
+        QString selfReactedEvent;
+        for (const auto &reaction : reactions) {
+                if (reactionKey == reaction.key_) {
+                        selfReactedEvent = reaction.selfReactedEvent_;
+                        break;
+                }
+        }
+
+        if (selfReactedEvent.startsWith("m"))
+                return;
+
+        // If selfReactedEvent is empty, that means we haven't previously reacted
+        if (selfReactedEvent.isEmpty()) {
+                mtx::events::msg::Reaction reaction;
+                mtx::common::Relation rel;
+                rel.rel_type = mtx::common::RelationType::Annotation;
+                rel.event_id = reactedEvent.toStdString();
+                rel.key      = reactionKey.toStdString();
+                reaction.relations.relations.push_back(rel);
+
+                room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
+                // Otherwise, we have previously reacted and the reaction should be redacted
+        } else {
+                room->redactEvent(selfReactedEvent);
+        }
+}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots:
         void message(QString body,
                      MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
                      bool rainbowify              = false);
+        void reaction(const QString &reactedEvent, const QString &reactionKey);
 
 private slots:
         void startTyping();
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 63054aa9..ad4177a4 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -341,6 +341,8 @@ RoomlistModel::clear()
         models.clear();
         invites.clear();
         roomids.clear();
+        currentRoom_ = nullptr;
+        emit currentRoomChanged();
         endResetModel();
 }
 
@@ -390,6 +392,17 @@ RoomlistModel::leave(QString roomid)
         }
 }
 
+void
+RoomlistModel::setCurrentRoom(QString roomid)
+{
+        nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
+        if (models.contains(roomid)) {
+                currentRoom_ = models.value(roomid);
+                emit currentRoomChanged();
+                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
+        }
+}
+
 namespace {
 enum NotificationImportance : short
 {
@@ -463,6 +476,11 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
                                  invalidate();
                          });
 
+        connect(roomlistmodel,
+                &RoomlistModel::currentRoomChanged,
+                this,
+                &FilteredRoomlistModel::currentRoomChanged);
+
         sort(0);
 }
 
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 2d1e5264..1c6fa833 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -14,12 +14,14 @@
 
 #include 
 
-class TimelineModel;
+#include "TimelineModel.h"
+
 class TimelineViewManager;
 
 class RoomlistModel : public QAbstractListModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         enum Roles
         {
@@ -69,12 +71,15 @@ public slots:
         void acceptInvite(QString roomid);
         void declineInvite(QString roomid);
         void leave(QString roomid);
+        TimelineModel *currentRoom() const { return currentRoom_.get(); }
+        void setCurrentRoom(QString roomid);
 
 private slots:
         void updateReadStatus(const std::map roomReadStatus_);
 
 signals:
         void totalUnreadMessageCountUpdated(int unreadMessages);
+        void currentRoomChanged();
 
 private:
         void addRoom(const QString &room_id, bool suppressInsertNotification = false);
@@ -85,12 +90,15 @@ private:
         QHash> models;
         std::map roomReadStatus;
 
+        QSharedPointer currentRoom_;
+
         friend class FilteredRoomlistModel;
 };
 
 class FilteredRoomlistModel : public QSortFilterProxyModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
         bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
@@ -107,6 +115,12 @@ public slots:
         QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
+        TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
+        void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+
+signals:
+        void currentRoomChanged();
+
 private:
         short int calculateImportance(const QModelIndex &idx) const;
         RoomlistModel *roomlistmodel;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 9fa7f8b6..3b3ea423 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -133,7 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
-  , rooms(new RoomlistModel(this))
+  , rooms_(new RoomlistModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -193,7 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms);
+                  return new FilteredRoomlistModel(self->rooms_);
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
@@ -320,9 +320,9 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
-        this->rooms->sync(rooms_);
+        this->rooms_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -330,37 +330,17 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
         }
 }
 
-void
-TimelineViewManager::setHistoryView(const QString &room_id)
-{
-        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
-
-        if (auto room = rooms->getRoomById(room_id)) {
-                timeline_ = room.get();
-                emit activeTimelineChanged(timeline_);
-                container->setFocus();
-                nhlog::ui()->info("Activated room {}", room_id.toStdString());
-        }
-}
-
-void
-TimelineViewManager::highlightRoom(const QString &room_id)
-{
-        ChatPage::instance()->highlightRoom(room_id);
-}
-
 void
 TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
-                if (timeline_ != room) {
-                        timeline_ = room.get();
-                        emit activeTimelineChanged(timeline_);
+        if (auto room = rooms_->getRoomById(room_id)) {
+                if (rooms_->currentRoom() != room) {
+                        rooms_->setCurrentRoom(room_id);
                         container->setFocus();
                         nhlog::ui()->info("Activated room {}", room_id.toStdString());
                 }
 
-                timeline_->showEvent(event_id);
+                room->showEvent(event_id);
         }
 }
 
@@ -395,17 +375,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
 
         auto imgDialog = new dialogs::ImageOverlay(pixmap);
         imgDialog->showFullScreen();
-        connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
-                // hide the overlay while presenting the save dialog for better
-                // cross platform support.
-                imgDialog->hide();
-
-                if (!timeline_->saveMedia(eventId)) {
-                        imgDialog->show();
-                } else {
-                        imgDialog->close();
-                }
-        });
+
+        auto room = rooms_->currentRoom();
+        connect(
+          imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
+                  // hide the overlay while presenting the save dialog for better
+                  // cross platform support.
+                  imgDialog->hide();
+
+                  if (!room->saveMedia(eventId)) {
+                          imgDialog->show();
+                  } else {
+                          imgDialog->close();
+                  }
+          });
 }
 
 void
@@ -415,14 +398,14 @@ TimelineViewManager::openInviteUsersDialog()
           [this](const QStringList &invitees) { emit inviteUsers(invitees); });
 }
 void
-TimelineViewManager::openMemberListDialog() const
+TimelineViewManager::openMemberListDialog(QString roomid) const
 {
-        MainWindow::instance()->openMemberListDialog(timeline_->roomId());
+        MainWindow::instance()->openMemberListDialog(roomid);
 }
 void
-TimelineViewManager::openLeaveRoomDialog() const
+TimelineViewManager::openLeaveRoomDialog(QString roomid) const
 {
-        MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
+        MainWindow::instance()->openLeaveRoomDialog(roomid);
 }
 
 void
@@ -439,7 +422,7 @@ TimelineViewManager::verifyUser(QString userid)
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
                                 if (auto model =
-                                      rooms->getRoomById(QString::fromStdString(room_id))) {
+                                      rooms_->getRoomById(QString::fromStdString(room_id))) {
                                         auto flow =
                                           DeviceVerificationFlow::InitiateUserVerification(
                                             this, model.data(), userid);
@@ -485,7 +468,7 @@ void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &event_ids)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
+        if (auto room = rooms_->getRoomById(room_id)) {
                 room->markEventsAsRead(event_ids);
         }
 }
@@ -493,7 +476,7 @@ TimelineViewManager::updateReadReceipts(const QString &room_id,
 void
 TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        if (auto room = rooms->getRoomById(QString::fromStdString(room_id))) {
+        if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
                 room->receivedSessionKey(session_id);
         }
 }
@@ -501,7 +484,7 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s
 void
 TimelineViewManager::initializeRoomlist()
 {
-        rooms->initializeRooms();
+        rooms_->initializeRooms();
 }
 
 void
@@ -509,51 +492,17 @@ TimelineViewManager::queueReply(const QString &roomid,
                                 const QString &repliedToEvent,
                                 const QString &replyBody)
 {
-        if (auto room = rooms->getRoomById(roomid)) {
+        if (auto room = rooms_->getRoomById(roomid)) {
                 room->setReply(repliedToEvent);
                 room->input()->message(replyBody);
         }
 }
 
-void
-TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
-{
-        if (!timeline_)
-                return;
-
-        auto reactions = timeline_->reactions(reactedEvent.toStdString());
-
-        QString selfReactedEvent;
-        for (const auto &reaction : reactions) {
-                if (reactionKey == reaction.key_) {
-                        selfReactedEvent = reaction.selfReactedEvent_;
-                        break;
-                }
-        }
-
-        if (selfReactedEvent.startsWith("m"))
-                return;
-
-        // If selfReactedEvent is empty, that means we haven't previously reacted
-        if (selfReactedEvent.isEmpty()) {
-                mtx::events::msg::Reaction reaction;
-                mtx::common::Relation rel;
-                rel.rel_type = mtx::common::RelationType::Annotation;
-                rel.event_id = reactedEvent.toStdString();
-                rel.key      = reactionKey.toStdString();
-                reaction.relations.relations.push_back(rel);
-
-                timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
-                // Otherwise, we have previously reacted and the reaction should be redacted
-        } else {
-                timeline_->redactEvent(selfReactedEvent);
-        }
-}
 void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallInvite &callInvite)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
 }
 
@@ -561,7 +510,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallCandidates &callCandidates)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
 }
 
@@ -569,7 +518,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallAnswer &callAnswer)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
 }
 
@@ -577,7 +526,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallHangUp &callHangUp)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
 
@@ -629,7 +578,7 @@ void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
 {
-        auto room                                                = rooms->getRoomById(roomId);
+        auto room                                                = rooms_->getRoomById(roomId);
         auto content                                             = mtx::accessors::url(*e);
         std::optional encryptionInfo = mtx::accessors::file(*e);
 
@@ -672,7 +621,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                                               ev.content.url = url;
                                                       }
 
-                                                      if (auto room = rooms->getRoomById(roomId)) {
+                                                      if (auto room = rooms_->getRoomById(roomId)) {
                                                               removeReplyFallback(ev);
                                                               ev.content.relations.relations
                                                                 .clear();
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 37e50804..c4707208 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -35,8 +35,6 @@ class TimelineViewManager : public QObject
 {
         Q_OBJECT
 
-        Q_PROPERTY(
-          TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
         Q_PROPERTY(
           bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
         Q_PROPERTY(
@@ -53,14 +51,8 @@ public:
         MxcImageProvider *imageProvider() { return imgProvider; }
         CallManager *callManager() { return callManager_; }
 
-        void clearAll()
-        {
-                timeline_ = nullptr;
-                emit activeTimelineChanged(nullptr);
-                rooms->clear();
-        }
+        void clearAll() { rooms_->clear(); }
 
-        Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
         bool isNarrowView() const { return isNarrowView_; }
         bool isWindowFocused() const { return isWindowFocused_; }
@@ -74,8 +66,8 @@ public:
 
         Q_INVOKABLE void focusMessageInput();
         Q_INVOKABLE void openInviteUsersDialog();
-        Q_INVOKABLE void openMemberListDialog() const;
-        Q_INVOKABLE void openLeaveRoomDialog() const;
+        Q_INVOKABLE void openMemberListDialog(QString roomid) const;
+        Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
         Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
 
         void verifyUser(QString userid);
@@ -107,20 +99,13 @@ public slots:
                 emit focusChanged();
         }
 
-        void setHistoryView(const QString &room_id);
-        void highlightRoom(const QString &room_id);
         void showEvent(const QString &room_id, const QString &event_id);
         void focusTimeline();
-        TimelineModel *getHistoryView(const QString &room_id)
-        {
-                return rooms->getRoomById(room_id).get();
-        }
 
         void updateColorPalette();
         void queueReply(const QString &roomid,
                         const QString &repliedToEvent,
                         const QString &replyBody);
-        void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
@@ -147,6 +132,8 @@ public slots:
         QObject *completerFor(QString completerName, QString roomId = "");
         void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
 
+        RoomlistModel *rooms() { return rooms_; }
+
 private slots:
         void openImageOverlayInternal(QString eventId, QImage img);
 
@@ -162,14 +149,13 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
         bool isInitialSync_   = true;
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms = nullptr;
+        RoomlistModel *rooms_ = nullptr;
 
         QHash userColors;
 
diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index 54f48d3c..bbcedd7e 100644
--- a/src/ui/NhekoDropArea.cpp
+++ b/src/ui/NhekoDropArea.cpp
@@ -35,7 +35,7 @@ void
 NhekoDropArea::dropEvent(QDropEvent *event)
 {
         if (event) {
-                auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_);
+                auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
                 if (model) {
                         model->input()->insertMimeData(event->mimeData());
                 }
-- 
cgit 1.5.1


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8b43559f..5a5e3ba1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -311,8 +311,6 @@ set(SRC_FILES
 	src/ChatPage.cpp
 	src/Clipboard.cpp
 	src/ColorImageProvider.cpp
-	src/CommunitiesList.cpp
-	src/CommunitiesListItem.cpp
 	src/CompletionProxyModel.cpp
 	src/DeviceVerificationFlow.cpp
 	src/EventAccessors.cpp
@@ -324,22 +322,14 @@ set(SRC_FILES
 	src/MxcImageProvider.cpp
 	src/Olm.cpp
 	src/RegisterPage.cpp
-	src/RoomInfoListItem.cpp
-	src/RoomList.cpp
 	src/SSOHandler.cpp
-	src/SideBarActions.cpp
-	src/Splitter.cpp
 	src/TrayIcon.cpp
-	src/UserInfoWidget.cpp
 	src/UserSettingsPage.cpp
 	src/UsersModel.cpp
 	src/RoomsModel.cpp
 	src/Utils.cpp
 	src/WebRTCSession.cpp
 	src/WelcomePage.cpp
-	src/popups/PopupItem.cpp
-	src/popups/SuggestionsPopup.cpp
-	src/popups/UserMentions.cpp
 	src/main.cpp
 
 	third_party/blurhash/blurhash.cpp
@@ -535,8 +525,6 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/CallManager.h
 	src/ChatPage.h
 	src/Clipboard.h
-	src/CommunitiesList.h
-	src/CommunitiesListItem.h
 	src/CompletionProxyModel.h
 	src/DeviceVerificationFlow.h
 	src/InviteeItem.h
@@ -544,21 +532,13 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/MainWindow.h
 	src/MxcImageProvider.h
 	src/RegisterPage.h
-	src/RoomInfoListItem.h
-	src/RoomList.h
 	src/SSOHandler.h
-	src/SideBarActions.h
-	src/Splitter.h
 	src/TrayIcon.h
-	src/UserInfoWidget.h
 	src/UserSettingsPage.h
 	src/UsersModel.h
 	src/RoomsModel.h
 	src/WebRTCSession.h
 	src/WelcomePage.h
-	src/popups/PopupItem.h
-	src/popups/SuggestionsPopup.h
-	src/popups/UserMentions.h
 	)
 
 #
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index a8b6fa52..c23ab97d 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -72,6 +72,15 @@ Page {
         }
     }
 
+    Shortcut {
+        sequence: "Ctrl+Down"
+        onActivated: Rooms.nextRoom();
+    }
+    Shortcut {
+        sequence: "Ctrl+Up"
+        onActivated: Rooms.previousRoom();
+    }
+
     Component {
         id: deviceVerificationDialog
 
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index bee20d60..4ad7bd14 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -23,10 +23,6 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "Olm.h"
-#include "RoomList.h"
-#include "SideBarActions.h"
-#include "Splitter.h"
-#include "UserInfoWidget.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 #include "ui/OverlayModal.h"
@@ -36,7 +32,6 @@
 #include "notifications/Manager.h"
 
 #include "dialogs/ReadReceipts.h"
-#include "popups/UserMentions.h"
 #include "timeline/TimelineViewManager.h"
 
 #include "blurhash.hpp"
@@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         topLayout_->setSpacing(0);
         topLayout_->setMargin(0);
 
-        communitiesList_ = new CommunitiesList(this);
-        topLayout_->addWidget(communitiesList_);
-
-        splitter = new Splitter(this);
-        splitter->setHandleWidth(0);
-
-        topLayout_->addWidget(splitter);
-
-        // SideBar
-        sideBar_ = new QFrame(this);
-        sideBar_->setObjectName("sideBar");
-        sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal);
-        sideBarLayout_ = new QVBoxLayout(sideBar_);
-        sideBarLayout_->setSpacing(0);
-        sideBarLayout_->setMargin(0);
-
-        sideBarTopWidget_ = new QWidget(sideBar_);
-        sidebarActions_   = new SideBarActions(this);
-        connect(
-          sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
-        connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom);
-        connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom);
-
-        user_info_widget_ = new UserInfoWidget(sideBar_);
-        connect(user_info_widget_, &UserInfoWidget::openGlobalUserProfile, this, [this]() {
-                UserProfile *userProfile = new UserProfile("", utils::localUser(), view_manager_);
-                emit view_manager_->openProfile(userProfile);
-        });
-
-        user_mentions_popup_ = new popups::UserMentions();
-        room_list_           = new RoomList(userSettings, sideBar_);
-        connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom);
-
-        sideBarLayout_->addWidget(user_info_widget_);
-        sideBarLayout_->addWidget(room_list_);
-        sideBarLayout_->addWidget(sidebarActions_);
-
-        sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_);
-        sideBarTopWidgetLayout_->setSpacing(0);
-        sideBarTopWidgetLayout_->setMargin(0);
-
-        // Content
-        content_ = new QFrame(this);
-        content_->setObjectName("mainContent");
-        contentLayout_ = new QVBoxLayout(content_);
-        contentLayout_->setSpacing(0);
-        contentLayout_->setMargin(0);
-
         view_manager_ = new TimelineViewManager(callManager_, this);
 
-        contentLayout_->addWidget(view_manager_->getWidget());
-
-        // Splitter
-        splitter->addWidget(sideBar_);
-        splitter->addWidget(content_);
-        splitter->restoreSizes(parent->width());
+        topLayout_->addWidget(view_manager_->getWidget());
 
         connect(this,
                 &ChatPage::downloadedSecrets,
@@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 trySync();
         });
 
-        connect(
-          new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
-                  if (isVisible())
-                          room_list_->nextRoom();
-          });
-        connect(
-          new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() {
-                  if (isVisible())
-                          room_list_->previousRoom();
-          });
-
         connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
         connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
                 if (http::client()->access_token().empty()) {
@@ -185,10 +116,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
 
         connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
 
-        connect(
-          view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList);
         connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
-                const auto room_id = current_room_.toStdString();
+                const auto room_id = currentRoom().toStdString();
 
                 for (int ii = 0; ii < users.size(); ++ii) {
                         QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
@@ -211,29 +140,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 }
         });
 
-        connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) {
-                this->current_room_ = room_id;
-        });
-        connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
-
-        connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
-                joinRoom(room_id);
-                room_list_->removeRoom(room_id, currentRoom() == room_id);
-        });
-
-        connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) {
-                leaveRoom(room_id);
-                room_list_->removeRoom(room_id, currentRoom() == room_id);
-        });
-
-        connect(view_manager_,
-                &TimelineViewManager::updateRoomsLastMessage,
-                room_list_,
-                &RoomList::updateRoomDescription);
-
-        connect(
-          this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
-
         connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
         connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
         connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
@@ -248,60 +154,23 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                         }
                 });
 
-        connect(communitiesList_,
-                &CommunitiesList::communityChanged,
-                this,
-                [this](const QString &groupId) {
-                        current_community_ = groupId;
-
-                        if (groupId == "world") {
-                                auto hidden = communitiesList_->hiddenTagsAndCommunities();
-                                std::set roomsToHide = communitiesList_->roomList(groupId);
-                                for (const auto &hiddenTag : hidden) {
-                                        auto temp = communitiesList_->roomList(hiddenTag);
-                                        roomsToHide.insert(temp.begin(), temp.end());
-                                }
-
-                                room_list_->removeFilter(roomsToHide);
-                        } else {
-                                auto hidden = communitiesList_->hiddenTagsAndCommunities();
-                                hidden.erase(current_community_);
-
-                                auto roomsToShow = communitiesList_->roomList(groupId);
-                                for (const auto &hiddenTag : hidden) {
-                                        for (const auto &r : communitiesList_->roomList(hiddenTag))
-                                                roomsToShow.erase(r);
-                                }
-
-                                room_list_->applyFilter(roomsToShow);
-                        }
-                });
-
         connect(¬ificationsManager,
                 &NotificationsManager::notificationClicked,
                 this,
                 [this](const QString &roomid, const QString &eventid) {
                         Q_UNUSED(eventid)
-                        room_list_->highlightSelectedRoom(roomid);
+                        view_manager_->rooms()->setCurrentRoom(roomid);
                         activateWindow();
                 });
         connect(¬ificationsManager,
                 &NotificationsManager::sendNotificationReply,
                 this,
                 [this](const QString &roomid, const QString &eventid, const QString &body) {
+                        view_manager_->rooms()->setCurrentRoom(roomid);
                         view_manager_->queueReply(roomid, eventid, body);
-                        room_list_->highlightSelectedRoom(roomid);
                         activateWindow();
                 });
 
-        setGroupViewState(userSettings_->groupView());
-
-        connect(userSettings_.data(),
-                &UserSettings::groupViewStateChanged,
-                this,
-                &ChatPage::setGroupViewState);
-
-        connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize);
         connect(
           this,
           &ChatPage::initializeViews,
@@ -312,30 +181,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 &ChatPage::initializeEmptyViews,
                 view_manager_,
                 &TimelineViewManager::initializeRoomlist);
-        connect(this,
-                &ChatPage::initializeMentions,
-                user_mentions_popup_,
-                &popups::UserMentions::initializeMentions);
         connect(
           this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
         connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
-                try {
-                        room_list_->cleanupInvites(cache::invites());
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->error("failed to retrieve invites: {}", e.what());
-                }
-
                 view_manager_->sync(rooms);
-                removeLeftRooms(rooms.leave);
 
                 bool hasNotifications = false;
                 for (const auto &room : rooms.join) {
-                        auto room_id = QString::fromStdString(room.first);
-                        updateRoomNotificationCount(
-                          room_id,
-                          room.second.unread_notifications.notification_count,
-                          room.second.unread_notifications.highlight_count);
-
                         if (room.second.unread_notifications.notification_count > 0)
                                 hasNotifications = true;
                 }
@@ -358,16 +210,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                                   emit notificationsRetrieved(std::move(res));
                           });
         });
-        connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
-        connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
-
-        // Callbacks to update the user info (top left corner of the page).
-        connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
-        connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) {
-                auto userid = utils::localUser();
-                user_info_widget_->setUserId(userid);
-                user_info_widget_->setDisplayName(name);
-        });
 
         connect(
           this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
@@ -420,8 +262,6 @@ ChatPage::dropToLoginPage(const QString &msg)
 void
 ChatPage::resetUI()
 {
-        room_list_->clear();
-        user_info_widget_->reset();
         view_manager_->clearAll();
 
         emit unreadMessages(0);
@@ -474,9 +314,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                         view_manager_,
                         &TimelineViewManager::updateReadReceipts);
 
-                connect(
-                  cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus);
-
                 connect(cache::client(),
                         &Cache::removeNotification,
                         ¬ificationsManager,
@@ -553,9 +390,7 @@ ChatPage::loadStateFromCache()
                 olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
 
                 emit initializeEmptyViews();
-                emit initializeRoomList(cache::roomInfo());
                 emit initializeMentions(cache::getTimelineMentions());
-                emit syncTags(cache::roomInfo().toStdMap());
 
                 cache::calculateRoomReadStatus();
 
@@ -593,38 +428,6 @@ ChatPage::removeRoom(const QString &room_id)
                 nhlog::db()->critical("failure while removing room: {}", e.what());
                 // TODO: Notify the user.
         }
-
-        room_list_->removeRoom(room_id, room_id == current_room_);
-}
-
-void
-ChatPage::removeLeftRooms(const std::map &rooms)
-{
-        for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) {
-                const auto room_id = QString::fromStdString(it->first);
-                room_list_->removeRoom(room_id, room_id == current_room_);
-        }
-}
-
-void
-ChatPage::setGroupViewState(bool isEnabled)
-{
-        if (!isEnabled) {
-                communitiesList_->communityChanged("world");
-                communitiesList_->hide();
-
-                return;
-        }
-
-        communitiesList_->show();
-}
-
-void
-ChatPage::updateRoomNotificationCount(const QString &room_id,
-                                      uint16_t notification_count,
-                                      uint16_t highlight_count)
-{
-        room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count);
 }
 
 void
@@ -672,18 +475,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
         }
 }
 
-void
-ChatPage::showNotificationsDialog(const QPoint &widgetPos)
-{
-        auto notifDialog = user_mentions_popup_;
-
-        notifDialog->setGeometry(
-          widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2);
-
-        notifDialog->raise();
-        notifDialog->showPopup();
-}
-
 void
 ChatPage::tryInitialSync()
 {
@@ -782,11 +573,9 @@ ChatPage::startInitialSync()
                           olm::handle_to_device_messages(res.to_device.events);
 
                           emit initializeViews(std::move(res.rooms));
-                          emit initializeRoomList(cache::roomInfo());
                           emit initializeMentions(cache::getTimelineMentions());
 
                           cache::calculateRoomReadStatus();
-                          emit syncTags(cache::roomInfo().toStdMap());
                   } catch (const lmdb::error &e) {
                           nhlog::db()->error("failed to save state after initial sync: {}",
                                              e.what());
@@ -823,12 +612,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string
 
                 auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
 
-                emit syncRoomlist(updates);
-
                 emit syncUI(res.rooms);
 
-                emit syncTags(cache::getRoomInfo(cache::client()->roomsWithTagUpdates(res)));
-
                 // if we process a lot of syncs (1 every 200ms), this means we clean the
                 // db every 100s
                 static int syncCounter = 0;
@@ -932,7 +717,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
                           emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
                   }
 
-                  room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
+                  view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
           });
 }
 
@@ -981,18 +766,17 @@ void
 ChatPage::changeRoom(const QString &room_id)
 {
         view_manager_->rooms()->setCurrentRoom(room_id);
-        room_list_->highlightSelectedRoom(room_id);
 }
 
 void
 ChatPage::inviteUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm invite"),
                                   tr("Do you really want to invite %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1014,12 +798,12 @@ ChatPage::inviteUser(QString userid, QString reason)
 void
 ChatPage::kickUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm kick"),
                                   tr("Do you really want to kick %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1041,12 +825,12 @@ ChatPage::kickUser(QString userid, QString reason)
 void
 ChatPage::banUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm ban"),
                                   tr("Do you really want to ban %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1068,12 +852,12 @@ ChatPage::banUser(QString userid, QString reason)
 void
 ChatPage::unbanUser(QString userid, QString reason)
 {
-        auto room = current_room_;
+        auto room = currentRoom();
 
         if (QMessageBox::question(this,
                                   tr("Confirm unban"),
                                   tr("Do you really want to unban %1 (%2)?")
-                                    .arg(cache::displayName(current_room_, userid))
+                                    .arg(cache::displayName(room, userid))
                                     .arg(userid)) != QMessageBox::Yes)
                 return;
 
@@ -1175,51 +959,6 @@ ChatPage::getProfileInfo()
 
                   emit setUserAvatar(QString::fromStdString(res.avatar_url));
           });
-
-        http::client()->joined_groups(
-          [this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->critical("failed to retrieve joined groups: {} {}",
-                                                 static_cast(err->status_code),
-                                                 err->matrix_error.error);
-                          emit updateGroupsInfo({});
-                          return;
-                  }
-
-                  emit updateGroupsInfo(res);
-          });
-}
-
-void
-ChatPage::hideSideBars()
-{
-        // Don't hide side bar, if we are currently only showing the side bar!
-        if (view_manager_->getWidget()->isVisible()) {
-                communitiesList_->hide();
-                sideBar_->hide();
-        }
-        view_manager_->enableBackButton();
-}
-
-void
-ChatPage::showSideBars()
-{
-        if (userSettings_->groupView())
-                communitiesList_->show();
-
-        sideBar_->show();
-        view_manager_->disableBackButton();
-        content_->show();
-}
-
-uint64_t
-ChatPage::timelineWidth()
-{
-        int sidebarWidth = sideBar_->minimumSize().width();
-        sidebarWidth += communitiesList_->minimumSize().width();
-        nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth);
-
-        return size().width() - sidebarWidth;
 }
 
 void
@@ -1305,7 +1044,8 @@ ChatPage::startChat(QString userid)
                         if (std::find(room_members.begin(),
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
-                                room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
+                                view_manager_->rooms()->setCurrentRoom(
+                                  QString::fromStdString(room_id));
                                 return;
                         }
                 }
@@ -1406,7 +1146,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
 
                 for (auto roomid : joined_rooms) {
                         if (roomid == targetRoomId) {
-                                room_list_->highlightSelectedRoom(mxid1);
+                                view_manager_->rooms()->setCurrentRoom(mxid1);
                                 if (!mxid2.isEmpty())
                                         view_manager_->showEvent(mxid1, mxid2);
                                 return;
@@ -1424,7 +1164,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
                         auto aliases = cache::client()->getRoomAliases(roomid);
                         if (aliases) {
                                 if (aliases->alias == targetRoomAlias) {
-                                        room_list_->highlightSelectedRoom(
+                                        view_manager_->rooms()->setCurrentRoom(
                                           QString::fromStdString(roomid));
                                         if (!mxid2.isEmpty())
                                                 view_manager_->showEvent(
@@ -1446,8 +1186,17 @@ ChatPage::handleMatrixUri(const QUrl &uri)
         handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
 }
 
-void
-ChatPage::highlightRoom(const QString &room_id)
+bool
+ChatPage::isRoomActive(const QString &room_id)
+{
+        return isActiveWindow() && currentRoom() == room_id;
+}
+
+QString
+ChatPage::currentRoom() const
 {
-        room_list_->highlightSelectedRoom(room_id);
+        if (view_manager_->rooms()->currentRoom())
+                return view_manager_->rooms()->currentRoom()->roomId();
+        else
+                return "";
 }
diff --git a/src/ChatPage.h b/src/ChatPage.h
index eb60047d..751e7074 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -27,15 +27,10 @@
 
 #include "CacheCryptoStructs.h"
 #include "CacheStructs.h"
-#include "CommunitiesList.h"
 #include "notifications/Manager.h"
 
 class OverlayModal;
-class RoomList;
-class SideBarActions;
-class Splitter;
 class TimelineViewManager;
-class UserInfoWidget;
 class UserSettings;
 class NotificationsManager;
 class TimelineModel;
@@ -53,11 +48,6 @@ struct Notifications;
 struct Sync;
 struct Timeline;
 struct Rooms;
-struct LeftRoom;
-}
-
-namespace popups {
-class UserMentions;
 }
 
 using SecretsToDecrypt = std::map;
@@ -71,7 +61,6 @@ public:
 
         // Initialize all the components of the UI.
         void bootstrap(QString userid, QString homeserver, QString token);
-        QString currentRoom() const { return current_room_; }
 
         static ChatPage *instance() { return instance_; }
 
@@ -80,14 +69,6 @@ public:
         TimelineViewManager *timelineManager() { return view_manager_; }
         void deleteConfigs();
 
-        CommunitiesList *communitiesList() { return communitiesList_; }
-
-        //! Calculate the width of the message timeline.
-        uint64_t timelineWidth();
-        //! Hide the room & group list (if it was visible).
-        void hideSideBars();
-        //! Show the room/group list (if it was visible).
-        void showSideBars();
         void initiateLogout();
 
         QString status() const;
@@ -95,6 +76,9 @@ public:
 
         mtx::presence::PresenceState currentPresence() const;
 
+        // TODO(Nico): Get rid of this!
+        QString currentRoom() const;
+
 public slots:
         void handleMatrixUri(const QByteArray &uri);
         void handleMatrixUri(const QUrl &uri);
@@ -102,7 +86,6 @@ public slots:
         void startChat(QString userid);
         void leaveRoom(const QString &room_id);
         void createRoom(const mtx::requests::CreateRoom &req);
-        void highlightRoom(const QString &room_id);
         void joinRoom(const QString &room);
         void joinRoomVia(const std::string &room_id,
                          const std::vector &via,
@@ -145,13 +128,10 @@ signals:
         void leftRoom(const QString &room_id);
         void newRoom(const QString &room_id);
 
-        void initializeRoomList(QMap);
         void initializeViews(const mtx::responses::Rooms &rooms);
         void initializeEmptyViews();
         void initializeMentions(const QMap ¬ifs);
         void syncUI(const mtx::responses::Rooms &rooms);
-        void syncRoomlist(const std::map &updates);
-        void syncTags(const std::map &updates);
         void dropToLoginPageCb(const QString &msg);
 
         void notifyMessage(const QString &roomid,
@@ -161,7 +141,6 @@ signals:
                            const QString &message,
                            const QImage &icon);
 
-        void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
         void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
         void themeChanged();
         void decryptSidebarChanged();
@@ -207,65 +186,31 @@ private:
         void getProfileInfo();
 
         //! Check if the given room is currently open.
-        bool isRoomActive(const QString &room_id)
-        {
-                return isActiveWindow() && currentRoom() == room_id;
-        }
+        bool isRoomActive(const QString &room_id);
 
         using UserID      = QString;
         using Membership  = mtx::events::StateEvent;
         using Memberships = std::map;
 
-        using LeftRooms = std::map;
-        void removeLeftRooms(const LeftRooms &rooms);
-
         void loadStateFromCache();
         void resetUI();
-        //! Decides whether or not to hide the group's sidebar.
-        void setGroupViewState(bool isEnabled);
 
         template
         Memberships getMemberships(const std::vector &events) const;
 
-        //! Update the room with the new notification count.
-        void updateRoomNotificationCount(const QString &room_id,
-                                         uint16_t notification_count,
-                                         uint16_t highlight_count);
         //! Send desktop notification for the received messages.
         void sendNotifications(const mtx::responses::Notifications &);
 
-        void showNotificationsDialog(const QPoint &point);
-
         template
         void connectCallMessage();
 
         QHBoxLayout *topLayout_;
-        Splitter *splitter;
-
-        QWidget *sideBar_;
-        QVBoxLayout *sideBarLayout_;
-        QWidget *sideBarTopWidget_;
-        QVBoxLayout *sideBarTopWidgetLayout_;
-
-        QFrame *content_;
-        QVBoxLayout *contentLayout_;
-
-        CommunitiesList *communitiesList_;
-        RoomList *room_list_;
 
         TimelineViewManager *view_manager_;
-        SideBarActions *sidebarActions_;
 
         QTimer connectivityTimer_;
         std::atomic_bool isConnected_;
 
-        QString current_room_;
-        QString current_community_;
-
-        UserInfoWidget *user_info_widget_;
-
-        popups::UserMentions *user_mentions_popup_;
-
         // Global user settings.
         QSharedPointer userSettings_;
 
diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp
deleted file mode 100644
index 7cc5d10e..00000000
--- a/src/CommunitiesList.cpp
+++ /dev/null
@@ -1,345 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "CommunitiesList.h"
-#include "Cache.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "MxcImageProvider.h"
-#include "Splitter.h"
-#include "UserSettingsPage.h"
-
-#include 
-#include 
-
-#include 
-
-CommunitiesList::CommunitiesList(QWidget *parent)
-  : QWidget(parent)
-{
-        QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
-        sizePolicy.setHorizontalStretch(0);
-        sizePolicy.setVerticalStretch(1);
-        setSizePolicy(sizePolicy);
-
-        topLayout_ = new QVBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
-
-        const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{});
-        setFixedWidth(sideBarSizes.groups);
-
-        scrollArea_ = new QScrollArea(this);
-        scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
-        scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter);
-
-        contentsLayout_ = new QVBoxLayout();
-        contentsLayout_->setSpacing(0);
-        contentsLayout_->setMargin(0);
-
-        addGlobalItem();
-        contentsLayout_->addStretch(1);
-
-        scrollArea_->setLayout(contentsLayout_);
-        topLayout_->addWidget(scrollArea_);
-
-        connect(
-          this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar);
-}
-
-void
-CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response)
-{
-        // remove all non-tag communities
-        auto it = communities_.begin();
-        while (it != communities_.end()) {
-                if (it->second->is_tag()) {
-                        ++it;
-                } else {
-                        it = communities_.erase(it);
-                }
-        }
-
-        addGlobalItem();
-
-        for (const auto &group : response.groups)
-                addCommunity(group);
-
-        communities_["world"]->setPressedState(true);
-        selectedCommunity_ = "world";
-        emit communityChanged("world");
-        sortEntries();
-}
-
-void
-CommunitiesList::syncTags(const std::map &info)
-{
-        for (const auto &room : info)
-                setTagsForRoom(room.first, room.second.tags);
-        emit communityChanged(selectedCommunity_);
-        sortEntries();
-}
-
-void
-CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector &tags)
-{
-        // create missing tag if any
-        for (const auto &tag : tags) {
-                // filter out tags we should ignore according to the spec
-                // https://matrix.org/docs/spec/client_server/r0.4.0.html#id154
-                // nheko currently does not make use of internal tags
-                // so we ignore any tag containig a `.` (which would indicate a tag
-                // in the form `tld.domain.*`) except for `m.*` and `u.*`.
-                if (tag.find(".") != ::std::string::npos && tag.compare(0, 2, "m.") &&
-                    tag.compare(0, 2, "u."))
-                        continue;
-                QString name = QString("tag:") + QString::fromStdString(tag);
-                if (!communityExists(name)) {
-                        addCommunity(std::string("tag:") + tag);
-                }
-        }
-        // update membership of the room for all tags
-        auto it = communities_.begin();
-        while (it != communities_.end()) {
-                // Skip if the community is not a tag
-                if (!it->second->is_tag()) {
-                        ++it;
-                        continue;
-                }
-                // insert or remove the room from the tag as appropriate
-                std::string current_tag =
-                  it->first.right(static_cast(it->first.size() - strlen("tag:")))
-                    .toStdString();
-                if (std::find(tags.begin(), tags.end(), current_tag) != tags.end()) {
-                        // the room has this tag
-                        it->second->addRoom(room_id);
-                } else {
-                        // the room does not have this tag
-                        it->second->delRoom(room_id);
-                }
-                // Check if the tag is now empty, if yes delete it
-                if (it->second->rooms().empty()) {
-                        it = communities_.erase(it);
-                } else {
-                        ++it;
-                }
-        }
-}
-
-void
-CommunitiesList::addCommunity(const std::string &group_id)
-{
-        auto hiddenTags = UserSettings::instance()->hiddenTags();
-
-        const auto id = QString::fromStdString(group_id);
-
-        CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_);
-
-        if (hiddenTags.contains(id))
-                list_item->setDisabled(true);
-
-        communities_.emplace(id, QSharedPointer(list_item));
-        contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
-
-        connect(list_item,
-                &CommunitiesListItem::clicked,
-                this,
-                &CommunitiesList::highlightSelectedCommunity);
-        connect(list_item, &CommunitiesListItem::isDisabledChanged, this, [this]() {
-                for (const auto &community : communities_) {
-                        if (community.second->isPressed()) {
-                                emit highlightSelectedCommunity(community.first);
-                                break;
-                        }
-                }
-
-                auto hiddenTags = hiddenTagsAndCommunities();
-                // Qt < 5.14 compat
-                QStringList hiddenTags_;
-                for (auto &&t : hiddenTags)
-                        hiddenTags_.push_back(t);
-                UserSettings::instance()->setHiddenTags(hiddenTags_);
-        });
-
-        if (group_id.empty() || group_id.front() != '+')
-                return;
-
-        nhlog::ui()->debug("Add community: {}", group_id);
-
-        connect(this,
-                &CommunitiesList::groupProfileRetrieved,
-                this,
-                [this](const QString &id, const mtx::responses::GroupProfile &profile) {
-                        if (communities_.find(id) == communities_.end())
-                                return;
-
-                        communities_.at(id)->setName(QString::fromStdString(profile.name));
-
-                        if (!profile.avatar_url.empty())
-                                fetchCommunityAvatar(id,
-                                                     QString::fromStdString(profile.avatar_url));
-                });
-        connect(this,
-                &CommunitiesList::groupRoomsRetrieved,
-                this,
-                [this](const QString &id, const std::set &rooms) {
-                        nhlog::ui()->info(
-                          "Fetched rooms for {}: {}", id.toStdString(), rooms.size());
-                        if (communities_.find(id) == communities_.end())
-                                return;
-
-                        communities_.at(id)->setRooms(rooms);
-                });
-
-        http::client()->group_profile(
-          group_id, [id, this](const mtx::responses::GroupProfile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          return;
-                  }
-
-                  emit groupProfileRetrieved(id, res);
-          });
-
-        http::client()->group_rooms(
-          group_id, [id, this](const nlohmann::json &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          return;
-                  }
-
-                  std::set room_ids;
-                  for (const auto &room : res.at("chunk"))
-                          room_ids.emplace(QString::fromStdString(room.at("room_id")));
-
-                  emit groupRoomsRetrieved(id, room_ids);
-          });
-}
-
-void
-CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img)
-{
-        if (!communityExists(community_id)) {
-                nhlog::ui()->warn("Avatar update on nonexistent community {}",
-                                  community_id.toStdString());
-                return;
-        }
-
-        communities_.at(community_id)->setAvatar(img.toImage());
-}
-
-void
-CommunitiesList::highlightSelectedCommunity(const QString &community_id)
-{
-        if (!communityExists(community_id)) {
-                nhlog::ui()->debug("CommunitiesList: clicked unknown community");
-                return;
-        }
-
-        selectedCommunity_ = community_id;
-        emit communityChanged(community_id);
-
-        for (const auto &community : communities_) {
-                if (community.first != community_id) {
-                        community.second->setPressedState(false);
-                } else {
-                        community.second->setPressedState(true);
-                        scrollArea_->ensureWidgetVisible(community.second.data());
-                }
-        }
-}
-
-void
-CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl)
-{
-        MxcImageProvider::download(
-          QString(avatarUrl).remove(QStringLiteral("mxc://")),
-          QSize(96, 96),
-          [this, id](QString, QSize, QImage img, QString) {
-                  if (img.isNull()) {
-                          nhlog::net()->warn("failed to download avatar: {})", id.toStdString());
-                          return;
-                  }
-
-                  emit avatarRetrieved(id, QPixmap::fromImage(img));
-          });
-}
-
-std::set
-CommunitiesList::roomList(const QString &id) const
-{
-        if (communityExists(id))
-                return communities_.at(id)->rooms();
-
-        return {};
-}
-
-std::vector
-CommunitiesList::currentTags() const
-{
-        std::vector tags;
-        for (auto &entry : communities_) {
-                CommunitiesListItem *item = entry.second.data();
-                if (item->is_tag())
-                        tags.push_back(entry.first.mid(4).toStdString());
-        }
-        return tags;
-}
-
-std::set
-CommunitiesList::hiddenTagsAndCommunities() const
-{
-        std::set hiddenTags;
-        for (auto &entry : communities_) {
-                if (entry.second->isDisabled())
-                        hiddenTags.insert(entry.first);
-        }
-
-        return hiddenTags;
-}
-
-void
-CommunitiesList::sortEntries()
-{
-        std::vector header;
-        std::vector communities;
-        std::vector tags;
-        std::vector footer;
-        // remove all the contents and sort them in the 4 vectors
-        for (auto &entry : communities_) {
-                CommunitiesListItem *item = entry.second.data();
-                contentsLayout_->removeWidget(item);
-                // world is handled separately
-                if (entry.first == "world")
-                        continue;
-                // sort the rest
-                if (item->is_tag())
-                        if (entry.first == "tag:m.favourite")
-                                header.push_back(item);
-                        else if (entry.first == "tag:m.lowpriority")
-                                footer.push_back(item);
-                        else
-                                tags.push_back(item);
-                else
-                        communities.push_back(item);
-        }
-
-        // now there remains only the stretch in the layout, remove it
-        QLayoutItem *stretch = contentsLayout_->itemAt(0);
-        contentsLayout_->removeItem(stretch);
-
-        contentsLayout_->addWidget(communities_["world"].data());
-
-        auto insert_widgets = [this](auto &vec) {
-                for (auto item : vec)
-                        contentsLayout_->addWidget(item);
-        };
-        insert_widgets(header);
-        insert_widgets(communities);
-        insert_widgets(tags);
-        insert_widgets(footer);
-
-        contentsLayout_->addItem(stretch);
-}
diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
deleted file mode 100644
index 12b275b0..00000000
--- a/src/CommunitiesList.h
+++ /dev/null
@@ -1,65 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-#include "CacheStructs.h"
-#include "CommunitiesListItem.h"
-
-namespace mtx::responses {
-struct GroupProfile;
-struct JoinedGroups;
-}
-
-class CommunitiesList : public QWidget
-{
-        Q_OBJECT
-
-public:
-        CommunitiesList(QWidget *parent = nullptr);
-
-        void clear() { communities_.clear(); }
-
-        void addCommunity(const std::string &id);
-        void removeCommunity(const QString &id) { communities_.erase(id); };
-        std::set roomList(const QString &id) const;
-
-        void syncTags(const std::map &info);
-        void setTagsForRoom(const QString &id, const std::vector &tags);
-        std::vector currentTags() const;
-        std::set hiddenTagsAndCommunities() const;
-
-signals:
-        void communityChanged(const QString &id);
-        void avatarRetrieved(const QString &id, const QPixmap &img);
-        void groupProfileRetrieved(const QString &group_id, const mtx::responses::GroupProfile &);
-        void groupRoomsRetrieved(const QString &group_id, const std::set &res);
-
-public slots:
-        void updateCommunityAvatar(const QString &id, const QPixmap &img);
-        void highlightSelectedCommunity(const QString &id);
-        void setCommunities(const mtx::responses::JoinedGroups &groups);
-
-private:
-        void fetchCommunityAvatar(const QString &id, const QString &avatarUrl);
-        void addGlobalItem() { addCommunity("world"); }
-        void sortEntries();
-
-        //! Check whether or not a community id is currently managed.
-        bool communityExists(const QString &id) const
-        {
-                return communities_.find(id) != communities_.end();
-        }
-
-        QString selectedCommunity_;
-        QVBoxLayout *topLayout_;
-        QVBoxLayout *contentsLayout_;
-        QScrollArea *scrollArea_;
-
-        std::map> communities_;
-};
diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp
deleted file mode 100644
index a2f2777d..00000000
--- a/src/CommunitiesListItem.cpp
+++ /dev/null
@@ -1,201 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "CommunitiesListItem.h"
-
-#include 
-#include 
-
-#include "Utils.h"
-#include "ui/Painter.h"
-#include "ui/Ripple.h"
-#include "ui/RippleOverlay.h"
-
-CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent)
-  : QWidget(parent)
-  , groupId_(group_id)
-{
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        QPainterPath path;
-        path.addRect(0, 0, parent->width(), height());
-        rippleOverlay_ = new RippleOverlay(this);
-        rippleOverlay_->setClipPath(path);
-        rippleOverlay_->setClipping(true);
-
-        menu_ = new QMenu(this);
-        hideRoomsWithTagAction_ =
-          new QAction(tr("Hide rooms with this tag or from this community"), this);
-        hideRoomsWithTagAction_->setCheckable(true);
-        menu_->addAction(hideRoomsWithTagAction_);
-        connect(menu_, &QMenu::aboutToShow, this, [this]() {
-                hideRoomsWithTagAction_->setChecked(isDisabled_);
-        });
-
-        connect(hideRoomsWithTagAction_, &QAction::triggered, this, [this](bool checked) {
-                this->setDisabled(checked);
-        });
-
-        updateTooltip();
-}
-
-void
-CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        menu_->popup(event->globalPos());
-}
-
-void
-CommunitiesListItem::setName(QString name)
-{
-        name_ = name;
-        updateTooltip();
-}
-
-void
-CommunitiesListItem::setPressedState(bool state)
-{
-        if (isPressed_ != state) {
-                isPressed_ = state;
-                update();
-        }
-}
-
-void
-CommunitiesListItem::setDisabled(bool state)
-{
-        if (isDisabled_ != state) {
-                isDisabled_ = state;
-                update();
-                emit isDisabledChanged();
-        }
-}
-
-void
-CommunitiesListItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() == Qt::RightButton) {
-                QWidget::mousePressEvent(event);
-                return;
-        }
-
-        emit clicked(groupId_);
-
-        setPressedState(true);
-
-        QPoint pos           = event->pos();
-        qreal radiusEndValue = static_cast(width()) / 3;
-
-        auto ripple = new Ripple(pos);
-        ripple->setRadiusEndValue(radiusEndValue);
-        ripple->setOpacityStartValue(0.15);
-        ripple->setColor("white");
-        ripple->radiusAnimation()->setDuration(200);
-        ripple->opacityAnimation()->setDuration(400);
-        rippleOverlay_->addRipple(ripple);
-}
-
-void
-CommunitiesListItem::paintEvent(QPaintEvent *)
-{
-        Painter p(this);
-        PainterHighQualityEnabler hq(p);
-
-        if (isPressed_)
-                p.fillRect(rect(), highlightedBackgroundColor_);
-        else if (isDisabled_)
-                p.fillRect(rect(), disabledBackgroundColor_);
-        else if (underMouse())
-                p.fillRect(rect(), hoverBackgroundColor_);
-        else
-                p.fillRect(rect(), backgroundColor_);
-
-        if (avatar_.isNull()) {
-                QPixmap source;
-                if (groupId_ == "world")
-                        source = QPixmap(":/icons/icons/ui/world.png");
-                else if (groupId_ == "tag:m.favourite")
-                        source = QPixmap(":/icons/icons/ui/star.png");
-                else if (groupId_ == "tag:m.lowpriority")
-                        source = QPixmap(":/icons/icons/ui/lowprio.png");
-                else if (groupId_.startsWith("tag:"))
-                        source = QPixmap(":/icons/icons/ui/tag.png");
-
-                if (source.isNull()) {
-                        QFont font;
-                        font.setPointSizeF(font.pointSizeF() * 1.3);
-                        p.setFont(font);
-
-                        p.drawLetterAvatar(utils::firstChar(resolveName()),
-                                           avatarFgColor_,
-                                           avatarBgColor_,
-                                           width(),
-                                           height(),
-                                           IconSize);
-                } else {
-                        QPainter painter(&source);
-                        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
-                        painter.fillRect(source.rect(), avatarFgColor_);
-                        painter.end();
-
-                        const int imageSz = 32;
-                        p.drawPixmap(
-                          QRect(
-                            (width() - imageSz) / 2, (height() - imageSz) / 2, imageSz, imageSz),
-                          source);
-                }
-        } else {
-                p.save();
-
-                p.drawAvatar(avatar_, width(), height(), IconSize);
-                p.restore();
-        }
-}
-
-void
-CommunitiesListItem::setAvatar(const QImage &img)
-{
-        avatar_ = utils::scaleImageToPixmap(img, IconSize);
-        update();
-}
-
-QString
-CommunitiesListItem::resolveName() const
-{
-        if (!name_.isEmpty())
-                return name_;
-        if (groupId_.startsWith("tag:"))
-                return groupId_.right(static_cast(groupId_.size() - strlen("tag:")));
-        if (!groupId_.startsWith("+"))
-                return QString("Group"); // Group with no name or id.
-
-        // Extract the localpart of the group.
-        auto firstPart = groupId_.split(':').at(0);
-        return firstPart.right(firstPart.size() - 1);
-}
-
-void
-CommunitiesListItem::updateTooltip()
-{
-        if (groupId_ == "world")
-                setToolTip(tr("All rooms"));
-        else if (is_tag()) {
-                QStringRef tag =
-                  groupId_.rightRef(static_cast(groupId_.size() - strlen("tag:")));
-                if (tag == "m.favourite")
-                        setToolTip(tr("Favourite rooms"));
-                else if (tag == "m.lowpriority")
-                        setToolTip(tr("Low priority rooms"));
-                else if (tag == "m.server_notice")
-                        setToolTip(tr("Server Notices", "Tag translation for m.server_notice"));
-                else if (tag.startsWith("u."))
-                        setToolTip(tag.right(tag.size() - 2) + tr(" (tag)"));
-                else
-                        setToolTip(tag + tr(" (tag)"));
-        } else {
-                QString name = resolveName();
-                setToolTip(name + tr(" (community)"));
-        }
-}
diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
deleted file mode 100644
index e7468611..00000000
--- a/src/CommunitiesListItem.h
+++ /dev/null
@@ -1,107 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-
-#include "Config.h"
-
-class RippleOverlay;
-class QMouseEvent;
-class QMenu;
-
-class CommunitiesListItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE
-                     setHighlightedBackgroundColor)
-        Q_PROPERTY(QColor disabledBackgroundColor READ disabledBackgroundColor WRITE
-                     setDisabledBackgroundColor)
-        Q_PROPERTY(
-          QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor)
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-        Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor)
-        Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor)
-
-public:
-        CommunitiesListItem(QString group_id, QWidget *parent = nullptr);
-
-        void setName(QString name);
-        bool isPressed() const { return isPressed_; }
-        bool isDisabled() const { return isDisabled_; }
-        void setAvatar(const QImage &img);
-
-        void setRooms(std::set room_ids) { room_ids_ = std::move(room_ids); }
-        void addRoom(const QString &id) { room_ids_.insert(id); }
-        void delRoom(const QString &id) { room_ids_.erase(id); }
-        std::set rooms() const { return room_ids_; }
-
-        bool is_tag() const { return groupId_.startsWith("tag:"); }
-
-        QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
-        QColor disabledBackgroundColor() const { return disabledBackgroundColor_; }
-        QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor avatarFgColor() const { return avatarFgColor_; }
-        QColor avatarBgColor() const { return avatarBgColor_; }
-
-        void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
-        void setDisabledBackgroundColor(QColor &color) { disabledBackgroundColor_ = color; }
-        void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
-        void setBackgroundColor(QColor &color) { backgroundColor_ = color; }
-
-        void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; }
-        void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; }
-
-        QSize sizeHint() const override
-        {
-                return QSize(IconSize + IconSize / 3, IconSize + IconSize / 3);
-        }
-
-signals:
-        void clicked(const QString &group_id);
-        void isDisabledChanged();
-
-public slots:
-        void setPressedState(bool state);
-        void setDisabled(bool state);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        const int IconSize = 36;
-
-        QString resolveName() const;
-        void updateTooltip();
-
-        std::set room_ids_;
-
-        QString name_;
-        QString groupId_;
-        QPixmap avatar_;
-
-        QColor highlightedBackgroundColor_;
-        QColor disabledBackgroundColor_;
-        QColor hoverBackgroundColor_;
-        QColor backgroundColor_;
-
-        QColor avatarFgColor_;
-        QColor avatarBgColor_;
-
-        bool isPressed_  = false;
-        bool isDisabled_ = false;
-
-        RippleOverlay *rippleOverlay_;
-        QMenu *menu_;
-        QAction *hideRoomsWithTagAction_;
-};
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index e2b625b0..057ee4af 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -109,10 +109,6 @@ MainWindow::MainWindow(QWidget *parent)
           userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
         connect(
           userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
-        connect(userSettingsPage_,
-                &UserSettingsPage::decryptSidebarChanged,
-                chat_page_,
-                &ChatPage::decryptSidebarChanged);
         connect(trayIcon_,
                 SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
                 this,
@@ -176,20 +172,6 @@ MainWindow::setWindowTitle(int notificationCount)
         QMainWindow::setWindowTitle(name);
 }
 
-void
-MainWindow::showEvent(QShowEvent *event)
-{
-        adjustSideBars();
-        QMainWindow::showEvent(event);
-}
-
-void
-MainWindow::resizeEvent(QResizeEvent *event)
-{
-        adjustSideBars();
-        QMainWindow::resizeEvent(event);
-}
-
 bool
 MainWindow::event(QEvent *event)
 {
@@ -203,22 +185,6 @@ MainWindow::event(QEvent *event)
         return QMainWindow::event(event);
 }
 
-void
-MainWindow::adjustSideBars()
-{
-        const auto sz = splitter::calculateSidebarSizes(QFont{});
-
-        const uint64_t timelineWidth     = chat_page_->timelineWidth();
-        const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups;
-
-        nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth);
-        if (timelineWidth < minAvailableWidth) {
-                chat_page_->hideSideBars();
-        } else {
-                chat_page_->showSideBars();
-        }
-}
-
 void
 MainWindow::restoreWindowSize()
 {
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 69d07e62..3571f079 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -77,13 +77,9 @@ public:
 
 protected:
         void closeEvent(QCloseEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void showEvent(QShowEvent *event) override;
         bool event(QEvent *event) override;
 
 private slots:
-        //! Show or hide the sidebars based on window's size.
-        void adjustSideBars();
         //! Handle interaction with the tray icon.
         void iconActivated(QSystemTrayIcon::ActivationReason reason);
 
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
deleted file mode 100644
index ea5de674..00000000
--- a/src/RoomInfoListItem.cpp
+++ /dev/null
@@ -1,522 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "AvatarProvider.h"
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "RoomInfoListItem.h"
-#include "Splitter.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/Ripple.h"
-#include "ui/RippleOverlay.h"
-
-constexpr int MaxUnreadCountDisplayed = 99;
-
-struct WidgetMetrics
-{
-        int maxHeight;
-        int iconSize;
-        int padding;
-        int unit;
-
-        int unreadLineWidth;
-        int unreadLineOffset;
-
-        int inviteBtnX;
-        int inviteBtnY;
-};
-
-WidgetMetrics
-getMetrics(const QFont &font)
-{
-        WidgetMetrics m;
-
-        const int height = QFontMetrics(font).lineSpacing();
-
-        m.unit             = height;
-        m.maxHeight        = std::ceil((double)height * 3.8);
-        m.iconSize         = std::ceil((double)height * 2.8);
-        m.padding          = std::ceil((double)height / 2.0);
-        m.unreadLineWidth  = m.padding - m.padding / 3;
-        m.unreadLineOffset = m.padding - m.padding / 4;
-
-        m.inviteBtnX = m.iconSize + 2 * m.padding;
-        m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0;
-
-        return m;
-}
-
-void
-RoomInfoListItem::init(QWidget *parent)
-{
-        setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        auto wm = getMetrics(QFont{});
-        setFixedHeight(wm.maxHeight);
-
-        QPainterPath path;
-        path.addRect(0, 0, parent->width(), height());
-
-        ripple_overlay_ = new RippleOverlay(this);
-        ripple_overlay_->setClipPath(path);
-        ripple_overlay_->setClipping(true);
-
-        avatar_ = new Avatar(nullptr, wm.iconSize);
-        avatar_->setLetter(utils::firstChar(roomName_));
-        avatar_->resize(wm.iconSize, wm.iconSize);
-
-        unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8);
-        unreadCountFont_.setBold(true);
-
-        bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3;
-
-        menu_      = new QMenu(this);
-        leaveRoom_ = new QAction(tr("Leave room"), this);
-        connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); });
-
-        connect(menu_, &QMenu::aboutToShow, this, [this]() {
-                menu_->clear();
-                menu_->addAction(leaveRoom_);
-
-                menu_->addSection(QIcon(":/icons/icons/ui/tag.png"), tr("Tag room as:"));
-
-                auto roomInfo = cache::singleRoomInfo(roomId_.toStdString());
-
-                auto tags = ChatPage::instance()->communitiesList()->currentTags();
-
-                // add default tag, remove server notice tag
-                if (std::find(tags.begin(), tags.end(), "m.favourite") == tags.end())
-                        tags.push_back("m.favourite");
-                if (std::find(tags.begin(), tags.end(), "m.lowpriority") == tags.end())
-                        tags.push_back("m.lowpriority");
-                if (auto it = std::find(tags.begin(), tags.end(), "m.server_notice");
-                    it != tags.end())
-                        tags.erase(it);
-
-                for (const auto &tag : tags) {
-                        QString tagName;
-                        if (tag == "m.favourite")
-                                tagName = tr("Favourite", "Standard matrix tag for favourites");
-                        else if (tag == "m.lowpriority")
-                                tagName =
-                                  tr("Low Priority", "Standard matrix tag for low priority rooms");
-                        else if (tag == "m.server_notice")
-                                tagName =
-                                  tr("Server Notice", "Standard matrix tag for server notices");
-                        else if ((tag.size() > 2 && tag.substr(0, 2) == "u.") ||
-                                 tag.find(".") !=
-                                   std::string::npos) // tag manager creates tags without u., which
-                                                      // is wrong, but we still want to display them
-                                tagName = QString::fromStdString(tag.substr(2));
-
-                        if (tagName.isEmpty())
-                                continue;
-
-                        auto tagAction = menu_->addAction(tagName);
-                        tagAction->setCheckable(true);
-                        tagAction->setWhatsThis(tr("Adds or removes the specified tag.",
-                                                   "WhatsThis hint for tag menu actions"));
-
-                        for (const auto &riTag : roomInfo.tags) {
-                                if (riTag == tag) {
-                                        tagAction->setChecked(true);
-                                        break;
-                                }
-                        }
-
-                        connect(tagAction, &QAction::triggered, this, [this, tag](bool checked) {
-                                if (checked)
-                                        http::client()->put_tag(
-                                          roomId_.toStdString(),
-                                          tag,
-                                          {},
-                                          [tag](mtx::http::RequestErr err) {
-                                                  if (err) {
-                                                          nhlog::ui()->error(
-                                                            "Failed to add tag: {}, {}",
-                                                            tag,
-                                                            err->matrix_error.error);
-                                                  }
-                                          });
-                                else
-                                        http::client()->delete_tag(
-                                          roomId_.toStdString(),
-                                          tag,
-                                          [tag](mtx::http::RequestErr err) {
-                                                  if (err) {
-                                                          nhlog::ui()->error(
-                                                            "Failed to delete tag: {}, {}",
-                                                            tag,
-                                                            err->matrix_error.error);
-                                                  }
-                                          });
-                        });
-                }
-
-                auto newTagAction = menu_->addAction(tr("New tag...", "Add a new tag to the room"));
-                connect(newTagAction, &QAction::triggered, this, [this]() {
-                        QString tagName =
-                          QInputDialog::getText(this,
-                                                tr("New Tag", "Tag name prompt title"),
-                                                tr("Tag:", "Tag name prompt"));
-                        if (tagName.isEmpty())
-                                return;
-
-                        std::string tag = "u." + tagName.toStdString();
-
-                        http::client()->put_tag(
-                          roomId_.toStdString(), tag, {}, [tag](mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::ui()->error("Failed to add tag: {}, {}",
-                                                             tag,
-                                                             err->matrix_error.error);
-                                  }
-                          });
-                });
-        });
-}
-
-RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent)
-  : QWidget(parent)
-  , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined}
-  , roomId_(std::move(room_id))
-  , roomName_{QString::fromStdString(std::move(info.name))}
-  , isPressed_(false)
-  , unreadMsgCount_(0)
-  , unreadHighlightedMsgCount_(0)
-{
-        init(parent);
-}
-
-void
-RoomInfoListItem::resizeEvent(QResizeEvent *)
-{
-        // Update ripple's clipping path.
-        QPainterPath path;
-        path.addRect(0, 0, width(), height());
-
-        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() > sidebarSizes.small)
-                setToolTip("");
-        else
-                setToolTip(roomName_);
-
-        ripple_overlay_->setClipPath(path);
-        ripple_overlay_->setClipping(true);
-}
-
-void
-RoomInfoListItem::paintEvent(QPaintEvent *event)
-{
-        Q_UNUSED(event);
-
-        QPainter p(this);
-        p.setRenderHint(QPainter::TextAntialiasing);
-        p.setRenderHint(QPainter::SmoothPixmapTransform);
-        p.setRenderHint(QPainter::Antialiasing);
-
-        QFontMetrics metrics(QFont{});
-
-        QPen titlePen(titleColor_);
-        QPen subtitlePen(subtitleColor_);
-
-        auto wm = getMetrics(QFont{});
-
-        QPixmap pixmap(avatar_->size() * p.device()->devicePixelRatioF());
-        pixmap.setDevicePixelRatio(p.device()->devicePixelRatioF());
-        if (isPressed_) {
-                p.fillRect(rect(), highlightedBackgroundColor_);
-                titlePen.setColor(highlightedTitleColor_);
-                subtitlePen.setColor(highlightedSubtitleColor_);
-                pixmap.fill(highlightedBackgroundColor_);
-        } else if (underMouse()) {
-                p.fillRect(rect(), hoverBackgroundColor_);
-                titlePen.setColor(hoverTitleColor_);
-                subtitlePen.setColor(hoverSubtitleColor_);
-                pixmap.fill(hoverBackgroundColor_);
-        } else {
-                p.fillRect(rect(), backgroundColor_);
-                titlePen.setColor(titleColor_);
-                subtitlePen.setColor(subtitleColor_);
-                pixmap.fill(backgroundColor_);
-        }
-
-        avatar_->render(&pixmap, QPoint(), QRegion(), RenderFlags(DrawChildren));
-        p.drawPixmap(QPoint(wm.padding, wm.padding), pixmap);
-
-        // Description line with the default font.
-        int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2;
-
-        const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{});
-
-        if (width() > sidebarSizes.small) {
-                QFont headingFont;
-                headingFont.setWeight(QFont::Medium);
-                p.setFont(headingFont);
-                p.setPen(titlePen);
-
-                QFont tsFont;
-                tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9);
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
-                const int msgStampWidth =
-                  QFontMetrics(tsFont).width(lastMsgInfo_.descriptiveTime) + 4;
-#else
-                const int msgStampWidth =
-                  QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.descriptiveTime) + 4;
-#endif
-                // We use the full width of the widget if there is no unread msg bubble.
-                const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0;
-
-                // Name line.
-                QFontMetrics fontNameMetrics(headingFont);
-                int top_y = 2 * wm.padding + fontNameMetrics.ascent() / 2;
-
-                const auto name = metrics.elidedText(
-                  roomName(),
-                  Qt::ElideRight,
-                  (width() - wm.iconSize - 2 * wm.padding - msgStampWidth) * 0.8);
-                p.drawText(QPoint(2 * wm.padding + wm.iconSize, top_y), name);
-
-                if (roomType_ == RoomType::Joined) {
-                        p.setFont(QFont{});
-                        p.setPen(subtitlePen);
-
-                        int descriptionLimit = std::max(
-                          0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
-                        auto description =
-                          metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
-                        p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
-
-                        // We show the last message timestamp.
-                        p.save();
-                        if (isPressed_) {
-                                p.setPen(QPen(highlightedTimestampColor_));
-                        } else if (underMouse()) {
-                                p.setPen(QPen(hoverTimestampColor_));
-                        } else {
-                                p.setPen(QPen(timestampColor_));
-                        }
-
-                        p.setFont(tsFont);
-                        p.drawText(QPoint(width() - wm.padding - msgStampWidth, top_y),
-                                   lastMsgInfo_.descriptiveTime);
-                        p.restore();
-                } else {
-                        int btnWidth = (width() - wm.iconSize - 6 * wm.padding) / 2;
-
-                        acceptBtnRegion_  = QRectF(wm.inviteBtnX, wm.inviteBtnY, btnWidth, 20);
-                        declineBtnRegion_ = QRectF(
-                          wm.inviteBtnX + btnWidth + 2 * wm.padding, wm.inviteBtnY, btnWidth, 20);
-
-                        QPainterPath acceptPath;
-                        acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10);
-
-                        p.setPen(Qt::NoPen);
-                        p.fillPath(acceptPath, btnColor_);
-                        p.drawPath(acceptPath);
-
-                        QPainterPath declinePath;
-                        declinePath.addRoundedRect(declineBtnRegion_, 10, 10);
-
-                        p.setPen(Qt::NoPen);
-                        p.fillPath(declinePath, btnColor_);
-                        p.drawPath(declinePath);
-
-                        p.setPen(QPen(btnTextColor_));
-                        p.setFont(QFont{});
-                        p.drawText(acceptBtnRegion_,
-                                   Qt::AlignCenter,
-                                   metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth));
-                        p.drawText(declineBtnRegion_,
-                                   Qt::AlignCenter,
-                                   metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth));
-                }
-        }
-
-        p.setPen(Qt::NoPen);
-
-        if (unreadMsgCount_ > 0) {
-                QBrush brush;
-                brush.setStyle(Qt::SolidPattern);
-                if (unreadHighlightedMsgCount_ > 0) {
-                        brush.setColor(mentionedColor());
-                } else {
-                        brush.setColor(bubbleBgColor());
-                }
-
-                if (isPressed_)
-                        brush.setColor(bubbleFgColor());
-
-                p.setBrush(brush);
-                p.setPen(Qt::NoPen);
-                p.setFont(unreadCountFont_);
-
-                // Extra space on the x-axis to accomodate the extra character space
-                // inside the bubble.
-                const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed
-                                      ? QFontMetrics(p.font()).averageCharWidth()
-                                      : 0;
-
-                QRectF r(width() - bubbleDiameter_ - wm.padding - x_width,
-                         bottom_y - bubbleDiameter_ / 2 - 5,
-                         bubbleDiameter_ + x_width,
-                         bubbleDiameter_);
-
-                if (width() == sidebarSizes.small)
-                        r = QRectF(width() - bubbleDiameter_ - 5,
-                                   height() - bubbleDiameter_ - 5,
-                                   bubbleDiameter_ + x_width,
-                                   bubbleDiameter_);
-
-                p.setPen(Qt::NoPen);
-                p.drawEllipse(r);
-
-                p.setPen(QPen(bubbleFgColor()));
-
-                if (isPressed_)
-                        p.setPen(QPen(bubbleBgColor()));
-
-                auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed
-                                  ? QString("99+")
-                                  : QString::number(unreadMsgCount_);
-
-                p.setBrush(Qt::NoBrush);
-                p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt);
-        }
-
-        if (!isPressed_ && hasUnreadMessages_) {
-                QPen pen;
-                pen.setWidth(wm.unreadLineWidth);
-                pen.setColor(highlightedBackgroundColor_);
-
-                p.setPen(pen);
-                p.drawLine(0, wm.unreadLineOffset, 0, height() - wm.unreadLineOffset);
-        }
-}
-
-void
-RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount)
-{
-        unreadMsgCount_            = count;
-        unreadHighlightedMsgCount_ = highlightedCount;
-        update();
-}
-
-enum NotificationImportance : short
-{
-        ImportanceDisabled = -1,
-        AllEventsRead      = 0,
-        NewMessage         = 1,
-        NewMentions        = 2,
-        Invite             = 3
-};
-
-short int
-RoomInfoListItem::calculateImportance() const
-{
-        // Returns the degree of importance of the unread messages in the room.
-        // If sorting by importance is disabled in settings, this only ever
-        // returns ImportanceDisabled or Invite
-        if (isInvite()) {
-                return Invite;
-        } else if (!ChatPage::instance()->userSettings()->sortByImportance()) {
-                return ImportanceDisabled;
-        } else if (unreadHighlightedMsgCount_) {
-                return NewMentions;
-        } else if (unreadMsgCount_) {
-                return NewMessage;
-        } else {
-                return AllEventsRead;
-        }
-}
-
-void
-RoomInfoListItem::setPressedState(bool state)
-{
-        if (isPressed_ != state) {
-                isPressed_ = state;
-                update();
-        }
-}
-
-void
-RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event)
-{
-        Q_UNUSED(event);
-
-        if (roomType_ == RoomType::Invited)
-                return;
-
-        menu_->popup(event->globalPos());
-}
-
-void
-RoomInfoListItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() == Qt::RightButton) {
-                QWidget::mousePressEvent(event);
-                return;
-        } else if (event->buttons() == Qt::LeftButton) {
-                if (roomType_ == RoomType::Invited) {
-                        const auto point = event->pos();
-
-                        if (acceptBtnRegion_.contains(point))
-                                emit acceptInvite(roomId_);
-
-                        if (declineBtnRegion_.contains(point))
-                                emit declineInvite(roomId_);
-
-                        return;
-                }
-
-                emit clicked(roomId_);
-
-                setPressedState(true);
-
-                // Ripple on mouse position by default.
-                QPoint pos           = event->pos();
-                qreal radiusEndValue = static_cast(width()) / 3;
-
-                Ripple *ripple = new Ripple(pos);
-
-                ripple->setRadiusEndValue(radiusEndValue);
-                ripple->setOpacityStartValue(0.15);
-                ripple->setColor(QColor("white"));
-                ripple->radiusAnimation()->setDuration(200);
-                ripple->opacityAnimation()->setDuration(400);
-
-                ripple_overlay_->addRipple(ripple);
-        }
-}
-
-void
-RoomInfoListItem::setAvatar(const QString &avatar_url)
-{
-        if (avatar_url.isEmpty())
-                avatar_->setLetter(utils::firstChar(roomName_));
-        else
-                avatar_->setImage(avatar_url);
-}
-
-void
-RoomInfoListItem::setDescriptionMessage(const DescInfo &info)
-{
-        lastMsgInfo_ = info;
-        update();
-}
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
deleted file mode 100644
index a5e0009e..00000000
--- a/src/RoomInfoListItem.h
+++ /dev/null
@@ -1,210 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "CacheStructs.h"
-#include "UserSettingsPage.h"
-#include "ui/Avatar.h"
-
-class QMenu;
-class RippleOverlay;
-
-class RoomInfoListItem : public QWidget
-{
-        Q_OBJECT
-        Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE
-                     setHighlightedBackgroundColor)
-        Q_PROPERTY(
-          QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor)
-        Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
-
-        Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor)
-        Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor)
-
-        Q_PROPERTY(QColor titleColor READ titleColor WRITE setTitleColor)
-        Q_PROPERTY(QColor subtitleColor READ subtitleColor WRITE setSubtitleColor)
-
-        Q_PROPERTY(QColor timestampColor READ timestampColor WRITE setTimestampColor)
-        Q_PROPERTY(QColor highlightedTimestampColor READ highlightedTimestampColor WRITE
-                     setHighlightedTimestampColor)
-        Q_PROPERTY(QColor hoverTimestampColor READ hoverTimestampColor WRITE setHoverTimestampColor)
-
-        Q_PROPERTY(
-          QColor highlightedTitleColor READ highlightedTitleColor WRITE setHighlightedTitleColor)
-        Q_PROPERTY(QColor highlightedSubtitleColor READ highlightedSubtitleColor WRITE
-                     setHighlightedSubtitleColor)
-
-        Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor)
-        Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor)
-
-        Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor)
-        Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor)
-        Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor)
-
-public:
-        RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr);
-
-        void updateUnreadMessageCount(int count, int highlightedCount);
-        void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); };
-
-        short int calculateImportance() const;
-
-        QString roomId() { return roomId_; }
-        bool isPressed() const { return isPressed_; }
-        int unreadMessageCount() const { return unreadMsgCount_; }
-
-        void setAvatar(const QString &avatar_url);
-        void setDescriptionMessage(const DescInfo &info);
-        DescInfo lastMessageInfo() const { return lastMsgInfo_; }
-
-        QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
-        QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
-        QColor hoverTitleColor() const { return hoverTitleColor_; }
-        QColor hoverSubtitleColor() const { return hoverSubtitleColor_; }
-        QColor hoverTimestampColor() const { return hoverTimestampColor_; }
-        QColor backgroundColor() const { return backgroundColor_; }
-
-        QColor highlightedTitleColor() const { return highlightedTitleColor_; }
-        QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; }
-        QColor highlightedTimestampColor() const { return highlightedTimestampColor_; }
-
-        QColor titleColor() const { return titleColor_; }
-        QColor subtitleColor() const { return subtitleColor_; }
-        QColor timestampColor() const { return timestampColor_; }
-        QColor btnColor() const { return btnColor_; }
-        QColor btnTextColor() const { return btnTextColor_; }
-
-        QColor bubbleFgColor() const { return bubbleFgColor_; }
-        QColor bubbleBgColor() const { return bubbleBgColor_; }
-        QColor mentionedColor() const { return mentionedFontColor_; }
-
-        void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
-        void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
-        void setHoverSubtitleColor(QColor &color) { hoverSubtitleColor_ = color; }
-        void setHoverTitleColor(QColor &color) { hoverTitleColor_ = color; }
-        void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; }
-        void setBackgroundColor(QColor &color) { backgroundColor_ = color; }
-        void setTimestampColor(QColor &color) { timestampColor_ = color; }
-
-        void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; }
-        void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; }
-        void setHighlightedTimestampColor(QColor &color) { highlightedTimestampColor_ = color; }
-
-        void setTitleColor(QColor &color) { titleColor_ = color; }
-        void setSubtitleColor(QColor &color) { subtitleColor_ = color; }
-
-        void setBtnColor(QColor &color) { btnColor_ = color; }
-        void setBtnTextColor(QColor &color) { btnTextColor_ = color; }
-
-        void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; }
-        void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; }
-        void setMentionedColor(QColor &color) { mentionedFontColor_ = color; }
-
-        void setRoomName(const QString &name) { roomName_ = name; }
-        void setRoomType(bool isInvite)
-        {
-                if (isInvite)
-                        roomType_ = RoomType::Invited;
-                else
-                        roomType_ = RoomType::Joined;
-        }
-
-        bool isInvite() const { return roomType_ == RoomType::Invited; }
-        void setReadState(bool hasUnreadMessages)
-        {
-                if (hasUnreadMessages_ != hasUnreadMessages) {
-                        hasUnreadMessages_ = hasUnreadMessages;
-                        update();
-                }
-        }
-
-signals:
-        void clicked(const QString &room_id);
-        void leaveRoom(const QString &room_id);
-        void acceptInvite(const QString &room_id);
-        void declineInvite(const QString &room_id);
-
-public slots:
-        void setPressedState(bool state);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-        void paintEvent(QPaintEvent *event) override;
-        void resizeEvent(QResizeEvent *event) override;
-        void contextMenuEvent(QContextMenuEvent *event) override;
-
-private:
-        void init(QWidget *parent);
-        QString roomName() { return roomName_; }
-
-        RippleOverlay *ripple_overlay_;
-        Avatar *avatar_;
-
-        enum class RoomType
-        {
-                Joined,
-                Invited,
-        };
-
-        RoomType roomType_ = RoomType::Joined;
-
-        // State information for the invited rooms.
-        mtx::responses::InvitedRoom invitedRoom_;
-
-        QString roomId_;
-        QString roomName_;
-
-        DescInfo lastMsgInfo_;
-
-        QMenu *menu_;
-        QAction *leaveRoom_;
-
-        bool isPressed_         = false;
-        bool hasUnreadMessages_ = true;
-
-        int unreadMsgCount_            = 0;
-        int unreadHighlightedMsgCount_ = 0;
-
-        QColor highlightedBackgroundColor_;
-        QColor hoverBackgroundColor_;
-        QColor backgroundColor_;
-
-        QColor highlightedTitleColor_;
-        QColor highlightedSubtitleColor_;
-
-        QColor titleColor_;
-        QColor subtitleColor_;
-
-        QColor hoverTitleColor_;
-        QColor hoverSubtitleColor_;
-
-        QColor btnColor_;
-        QColor btnTextColor_;
-
-        QRectF acceptBtnRegion_;
-        QRectF declineBtnRegion_;
-
-        // Fonts
-        QColor mentionedFontColor_;
-        QFont unreadCountFont_;
-        int bubbleDiameter_;
-
-        QColor timestampColor_;
-        QColor highlightedTimestampColor_;
-        QColor hoverTimestampColor_;
-
-        QColor bubbleBgColor_;
-        QColor bubbleFgColor_;
-
-        friend struct room_sort;
-};
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
deleted file mode 100644
index 5839c4a0..00000000
--- a/src/RoomList.cpp
+++ /dev/null
@@ -1,535 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Logging.h"
-#include "MainWindow.h"
-#include "RoomInfoListItem.h"
-#include "RoomList.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "ui/OverlayModal.h"
-
-RoomList::RoomList(QSharedPointer userSettings, QWidget *parent)
-  : QWidget(parent)
-{
-        topLayout_ = new QVBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
-
-        scrollArea_ = new QScrollArea(this);
-        scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-        scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
-        scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter);
-        scrollArea_->setAttribute(Qt::WA_AcceptTouchEvents);
-
-        QScroller::grabGesture(scrollArea_, QScroller::TouchGesture);
-        QScroller::grabGesture(scrollArea_, QScroller::LeftMouseButtonGesture);
-
-// The scrollbar on macOS will hide itself when not active so it won't interfere
-// with the content.
-#if not defined(Q_OS_MAC)
-        scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-#endif
-
-        scrollAreaContents_ = new QWidget(this);
-        scrollAreaContents_->setObjectName("roomlist_area");
-
-        contentsLayout_ = new QVBoxLayout(scrollAreaContents_);
-        contentsLayout_->setAlignment(Qt::AlignTop);
-        contentsLayout_->setSpacing(0);
-        contentsLayout_->setMargin(0);
-
-        scrollArea_->setWidget(scrollAreaContents_);
-        topLayout_->addWidget(scrollArea_);
-
-        connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar);
-        connect(userSettings.data(),
-                &UserSettings::roomSortingChanged,
-                this,
-                &RoomList::sortRoomsByLastMessage);
-}
-
-void
-RoomList::addRoom(const QString &room_id, const RoomInfo &info)
-{
-        auto room_item = new RoomInfoListItem(room_id, info, scrollArea_);
-        room_item->setRoomName(QString::fromStdString(std::move(info.name)));
-
-        connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom);
-        connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) {
-                MainWindow::instance()->openLeaveRoomDialog(room_id);
-        });
-
-        QSharedPointer roomWidget(room_item, &QObject::deleteLater);
-        rooms_.emplace(room_id, roomWidget);
-        rooms_sort_cache_.push_back(roomWidget);
-
-        if (!info.avatar_url.empty())
-                updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-
-        int pos = contentsLayout_->count() - 1;
-        contentsLayout_->insertWidget(pos, room_item);
-}
-
-void
-RoomList::updateAvatar(const QString &room_id, const QString &url)
-{
-        emit updateRoomAvatarCb(room_id, url);
-}
-
-void
-RoomList::removeRoom(const QString &room_id, bool reset)
-{
-        auto roomIt = rooms_.find(room_id);
-        if (roomIt == rooms_.end()) {
-                return;
-        }
-
-        for (auto roomSortIt = rooms_sort_cache_.begin(); roomSortIt != rooms_sort_cache_.end();
-             ++roomSortIt) {
-                if (roomIt->second == *roomSortIt) {
-                        rooms_sort_cache_.erase(roomSortIt);
-                        break;
-                }
-        }
-        rooms_.erase(room_id);
-
-        if (rooms_.empty() || !reset)
-                return;
-
-        auto room = firstRoom();
-
-        if (room.second.isNull())
-                return;
-
-        room.second->setPressedState(true);
-        emit roomChanged(room.first);
-}
-
-void
-RoomList::updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount)
-{
-        if (!roomExists(roomid)) {
-                nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}",
-                                  roomid.toStdString());
-                return;
-        }
-
-        rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount);
-
-        calculateUnreadMessageCount();
-
-        sortRoomsByLastMessage();
-}
-
-void
-RoomList::calculateUnreadMessageCount()
-{
-        int total_unread_msgs = 0;
-
-        for (const auto &room : rooms_) {
-                if (!room.second.isNull())
-                        total_unread_msgs += room.second->unreadMessageCount();
-        }
-
-        emit totalUnreadMessageCountUpdated(total_unread_msgs);
-}
-
-void
-RoomList::initialize(const QMap &info)
-{
-        nhlog::ui()->info("initialize room list");
-
-        rooms_.clear();
-
-        // prevent flickering and save time sorting over and over again
-        setUpdatesEnabled(false);
-        for (auto it = info.begin(); it != info.end(); it++) {
-                if (it.value().is_invite)
-                        addInvitedRoom(it.key(), it.value());
-                else
-                        addRoom(it.key(), it.value());
-        }
-
-        for (auto it = info.begin(); it != info.end(); it++)
-                updateRoomDescription(it.key(), it.value().msgInfo);
-
-        setUpdatesEnabled(true);
-
-        if (rooms_.empty())
-                return;
-
-        sortRoomsByLastMessage();
-
-        auto room = firstRoom();
-        if (room.second.isNull())
-                return;
-
-        room.second->setPressedState(true);
-        emit roomChanged(room.first);
-}
-
-void
-RoomList::cleanupInvites(const QHash &invites)
-{
-        if (invites.size() == 0)
-                return;
-
-        utils::erase_if(rooms_, [invites](auto &room) {
-                auto room_id = room.first;
-                auto item    = room.second;
-
-                if (!item)
-                        return false;
-
-                return item->isInvite() && (invites.find(room_id) == invites.end());
-        });
-}
-
-void
-RoomList::sync(const std::map &info)
-
-{
-        for (const auto &room : info)
-                updateRoom(room.first, room.second);
-
-        if (!info.empty())
-                sortRoomsByLastMessage();
-}
-
-void
-RoomList::highlightSelectedRoom(const QString &room_id)
-{
-        emit roomChanged(room_id);
-
-        if (!roomExists(room_id)) {
-                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                return;
-        }
-
-        for (auto const &room : rooms_) {
-                if (room.second.isNull())
-                        continue;
-
-                if (room.first != room_id) {
-                        room.second->setPressedState(false);
-                } else {
-                        room.second->setPressedState(true);
-                        scrollArea_->ensureWidgetVisible(room.second.data());
-                }
-        }
-
-        selectedRoom_ = room_id;
-}
-
-void
-RoomList::nextRoom()
-{
-        for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) {
-                auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget());
-
-                if (!room)
-                        continue;
-
-                if (room->roomId() == selectedRoom_) {
-                        auto nextRoom = qobject_cast(
-                          contentsLayout_->itemAt(ii + 1)->widget());
-
-                        // Not a room message.
-                        if (!nextRoom || nextRoom->isInvite())
-                                return;
-
-                        emit roomChanged(nextRoom->roomId());
-                        if (!roomExists(nextRoom->roomId())) {
-                                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                                return;
-                        }
-
-                        room->setPressedState(false);
-                        nextRoom->setPressedState(true);
-
-                        scrollArea_->ensureWidgetVisible(nextRoom);
-                        selectedRoom_ = nextRoom->roomId();
-                        return;
-                }
-        }
-}
-
-void
-RoomList::previousRoom()
-{
-        for (int ii = 1; ii < contentsLayout_->count(); ++ii) {
-                auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget());
-
-                if (!room)
-                        continue;
-
-                if (room->roomId() == selectedRoom_) {
-                        auto nextRoom = qobject_cast(
-                          contentsLayout_->itemAt(ii - 1)->widget());
-
-                        // Not a room message.
-                        if (!nextRoom || nextRoom->isInvite())
-                                return;
-
-                        emit roomChanged(nextRoom->roomId());
-                        if (!roomExists(nextRoom->roomId())) {
-                                nhlog::ui()->warn("roomlist: clicked unknown room_id");
-                                return;
-                        }
-
-                        room->setPressedState(false);
-                        nextRoom->setPressedState(true);
-
-                        scrollArea_->ensureWidgetVisible(nextRoom);
-                        selectedRoom_ = nextRoom->roomId();
-                        return;
-                }
-        }
-}
-
-void
-RoomList::updateRoomAvatar(const QString &roomid, const QString &img)
-{
-        if (!roomExists(roomid)) {
-                return;
-        }
-
-        rooms_[roomid]->setAvatar(img);
-
-        // Used to inform other widgets for the new image data.
-        emit roomAvatarChanged(roomid, img);
-}
-
-void
-RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
-{
-        if (!roomExists(roomid)) {
-                return;
-        }
-
-        rooms_[roomid]->setDescriptionMessage(info);
-
-        if (underMouse()) {
-                // When the user hover out of the roomlist a sort will be triggered.
-                isSortPending_ = true;
-                return;
-        }
-
-        isSortPending_ = false;
-
-        emit sortRoomsByLastMessage();
-}
-
-struct room_sort
-{
-        bool operator()(const QSharedPointer &a,
-                        const QSharedPointer &b) const
-        {
-                // Sort by "importance" (i.e. invites before mentions before
-                // notifs before new events before old events), then secondly
-                // by recency.
-
-                // Checking importance first
-                const auto a_importance = a->calculateImportance();
-                const auto b_importance = b->calculateImportance();
-                if (a_importance != b_importance) {
-                        return a_importance > b_importance;
-                }
-
-                // Now sort by recency
-                // Zero if empty, otherwise the time that the event occured
-                const uint64_t a_recency =
-                  a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp;
-                const uint64_t b_recency =
-                  b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp;
-                return a_recency > b_recency;
-        }
-};
-
-void
-RoomList::sortRoomsByLastMessage()
-{
-        isSortPending_ = false;
-
-        std::stable_sort(begin(rooms_sort_cache_), end(rooms_sort_cache_), room_sort{});
-
-        int newIndex = 0;
-        for (const auto &roomWidget : rooms_sort_cache_) {
-                const auto currentIndex = contentsLayout_->indexOf(roomWidget.data());
-
-                if (currentIndex != newIndex) {
-                        contentsLayout_->removeWidget(roomWidget.data());
-                        contentsLayout_->insertWidget(newIndex, roomWidget.data());
-                }
-                newIndex++;
-        }
-}
-
-void
-RoomList::leaveEvent(QEvent *event)
-{
-        if (isSortPending_)
-                QTimer::singleShot(700, this, &RoomList::sortRoomsByLastMessage);
-
-        QWidget::leaveEvent(event);
-}
-
-void
-RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
-{
-        joinRoomModal_->hide();
-
-        if (isJoining)
-                emit joinRoom(roomAlias);
-}
-
-void
-RoomList::removeFilter(const std::set &roomsToHide)
-{
-        setUpdatesEnabled(false);
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto widget =
-                  qobject_cast(contentsLayout_->itemAt(i)->widget());
-                if (widget) {
-                        if (roomsToHide.find(widget->roomId()) == roomsToHide.end())
-                                widget->show();
-                        else
-                                widget->hide();
-                }
-        }
-        setUpdatesEnabled(true);
-}
-
-void
-RoomList::applyFilter(const std::set &filter)
-{
-        // Disabling paint updates will resolve issues with screen flickering on big room lists.
-        setUpdatesEnabled(false);
-
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                // If filter contains the room for the current RoomInfoListItem,
-                // show the list item, otherwise hide it
-                auto listitem =
-                  qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (!listitem)
-                        continue;
-
-                if (filter.find(listitem->roomId()) != filter.end())
-                        listitem->show();
-                else
-                        listitem->hide();
-        }
-
-        setUpdatesEnabled(true);
-
-        // If the already selected room is part of the group, make sure it's visible.
-        if (!selectedRoom_.isEmpty() && (filter.find(selectedRoom_) != filter.end()))
-                return;
-
-        selectFirstVisibleRoom();
-}
-
-void
-RoomList::selectFirstVisibleRoom()
-{
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto item = qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (item && item->isVisible()) {
-                        highlightSelectedRoom(item->roomId());
-                        break;
-                }
-        }
-}
-
-void
-RoomList::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-RoomList::updateRoom(const QString &room_id, const RoomInfo &info)
-{
-        if (!roomExists(room_id)) {
-                if (info.is_invite)
-                        addInvitedRoom(room_id, info);
-                else
-                        addRoom(room_id, info);
-
-                return;
-        }
-
-        auto room = rooms_[room_id];
-        updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-        room->setRoomName(QString::fromStdString(info.name));
-        room->setRoomType(info.is_invite);
-        room->update();
-}
-
-void
-RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info)
-{
-        auto room_item = new RoomInfoListItem(room_id, info, scrollArea_);
-
-        connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite);
-        connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite);
-
-        QSharedPointer roomWidget(room_item);
-        rooms_.emplace(room_id, roomWidget);
-        rooms_sort_cache_.push_back(roomWidget);
-
-        updateAvatar(room_id, QString::fromStdString(info.avatar_url));
-
-        int pos = contentsLayout_->count() - 1;
-        contentsLayout_->insertWidget(pos, room_item);
-}
-
-std::pair>
-RoomList::firstRoom() const
-{
-        for (int i = 0; i < contentsLayout_->count(); i++) {
-                auto item = qobject_cast(contentsLayout_->itemAt(i)->widget());
-
-                if (item) {
-                        auto topRoom = rooms_.find(item->roomId());
-                        if (topRoom != rooms_.end()) {
-                                return std::pair>(
-                                  item->roomId(), topRoom->second);
-                        }
-                }
-        }
-
-        return {};
-}
-
-void
-RoomList::updateReadStatus(const std::map &status)
-{
-        for (const auto &room : status) {
-                if (roomExists(room.first)) {
-                        auto item = rooms_.at(room.first);
-
-                        if (item)
-                                item->setReadState(room.second);
-                }
-        }
-}
diff --git a/src/RoomList.h b/src/RoomList.h
deleted file mode 100644
index af792fd7..00000000
--- a/src/RoomList.h
+++ /dev/null
@@ -1,101 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Konstantinos Sideris 
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include "CacheStructs.h"
-#include "UserSettingsPage.h"
-
-class LeaveRoomDialog;
-class OverlayModal;
-class RoomInfoListItem;
-class Sync;
-struct DescInfo;
-struct RoomInfo;
-
-class RoomList : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit RoomList(QSharedPointer userSettings, QWidget *parent = nullptr);
-
-        void initialize(const QMap &info);
-        void sync(const std::map &info);
-
-        void clear()
-        {
-                rooms_.clear();
-                rooms_sort_cache_.clear();
-        };
-        void updateAvatar(const QString &room_id, const QString &url);
-
-        void addRoom(const QString &room_id, const RoomInfo &info);
-        void addInvitedRoom(const QString &room_id, const RoomInfo &info);
-        void removeRoom(const QString &room_id, bool reset);
-        //! Hide rooms that are not present in the given filter.
-        void applyFilter(const std::set &rooms);
-        //! Show all the available rooms.
-        void removeFilter(const std::set &roomsToHide);
-        void updateRoom(const QString &room_id, const RoomInfo &info);
-        void cleanupInvites(const QHash &invites);
-
-signals:
-        void roomChanged(const QString &room_id);
-        void totalUnreadMessageCountUpdated(int count);
-        void acceptInvite(const QString &room_id);
-        void declineInvite(const QString &room_id);
-        void roomAvatarChanged(const QString &room_id, const QString &img);
-        void joinRoom(const QString &room_id);
-        void updateRoomAvatarCb(const QString &room_id, const QString &img);
-
-public slots:
-        void updateRoomAvatar(const QString &roomid, const QString &img);
-        void highlightSelectedRoom(const QString &room_id);
-        void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount);
-        void updateRoomDescription(const QString &roomid, const DescInfo &info);
-        void closeJoinRoomDialog(bool isJoining, QString roomAlias);
-        void updateReadStatus(const std::map &status);
-        void nextRoom();
-        void previousRoom();
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-        void leaveEvent(QEvent *event) override;
-
-private slots:
-        void sortRoomsByLastMessage();
-
-private:
-        //! Return the first non-null room.
-        std::pair> firstRoom() const;
-        void calculateUnreadMessageCount();
-        bool roomExists(const QString &room_id) { return rooms_.find(room_id) != rooms_.end(); }
-        //! Select the first visible room in the room list.
-        void selectFirstVisibleRoom();
-
-        QVBoxLayout *topLayout_;
-        QVBoxLayout *contentsLayout_;
-        QScrollArea *scrollArea_;
-        QWidget *scrollAreaContents_;
-
-        QPushButton *joinRoomButton_;
-
-        OverlayModal *joinRoomModal_;
-
-        std::map> rooms_;
-        std::vector> rooms_sort_cache_;
-        QString selectedRoom_;
-
-        bool isSortPending_ = false;
-};
diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp
deleted file mode 100644
index 2daa6143..00000000
--- a/src/popups/PopupItem.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-
-#include "../Utils.h"
-#include "../ui/Avatar.h"
-#include "PopupItem.h"
-
-constexpr int PopupHMargin    = 4;
-constexpr int PopupItemMargin = 3;
-
-PopupItem::PopupItem(QWidget *parent)
-  : QWidget(parent)
-  , avatar_{new Avatar(this, conf::popup::avatar)}
-  , hovering_{false}
-{
-        setMouseTracking(true);
-        setAttribute(Qt::WA_Hover);
-
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setContentsMargins(
-          PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin);
-
-        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-}
-
-void
-PopupItem::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-
-        if (underMouse() || hovering_)
-                p.fillRect(rect(), hoverColor_);
-}
-
-RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res)
-  : PopupItem(parent)
-  , roomId_{QString::fromStdString(res.room_id)}
-{
-        auto name = QFontMetrics(QFont()).elidedText(
-          QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10);
-
-        avatar_->setLetter(utils::firstChar(name));
-
-        roomName_ = new QLabel(name, this);
-        roomName_->setMargin(0);
-
-        topLayout_->addWidget(avatar_);
-        topLayout_->addWidget(roomName_, 1);
-
-        if (!res.info.avatar_url.empty())
-                avatar_->setImage(QString::fromStdString(res.info.avatar_url));
-}
-
-void
-RoomItem::updateItem(const RoomSearchResult &result)
-{
-        roomId_ = QString::fromStdString(std::move(result.room_id));
-
-        auto name =
-          QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)),
-                                           Qt::ElideRight,
-                                           parentWidget()->width() - 10);
-
-        roomName_->setText(name);
-
-        // if there is not an avatar set for the room, we want to at least show the letter
-        // correctly!
-        avatar_->setLetter(utils::firstChar(name));
-        if (!result.info.avatar_url.empty())
-                avatar_->setImage(QString::fromStdString(result.info.avatar_url));
-}
-
-void
-RoomItem::mousePressEvent(QMouseEvent *event)
-{
-        if (event->buttons() != Qt::RightButton)
-                emit clicked(selectedText());
-
-        QWidget::mousePressEvent(event);
-}
diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h
deleted file mode 100644
index fc24915e..00000000
--- a/src/popups/PopupItem.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include "../AvatarProvider.h"
-#include "../ChatPage.h"
-
-class Avatar;
-struct SearchResult;
-class QLabel;
-class QHBoxLayout;
-
-class PopupItem : public QWidget
-{
-        Q_OBJECT
-
-        Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
-        Q_PROPERTY(bool hovering READ hovering WRITE setHovering)
-
-public:
-        PopupItem(QWidget *parent);
-
-        QString selectedText() const { return QString(); }
-        QColor hoverColor() const { return hoverColor_; }
-        void setHoverColor(QColor &color) { hoverColor_ = color; }
-
-        bool hovering() const { return hovering_; }
-        void setHovering(const bool hover) { hovering_ = hover; };
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-signals:
-        void clicked(const QString &text);
-
-protected:
-        QHBoxLayout *topLayout_;
-        Avatar *avatar_;
-        QColor hoverColor_;
-
-        //! Set if the item is currently being
-        //! hovered during tab completion (cycling).
-        bool hovering_;
-};
-
-class RoomItem : public PopupItem
-{
-        Q_OBJECT
-
-public:
-        RoomItem(QWidget *parent, const RoomSearchResult &res);
-        QString selectedText() const { return roomId_; }
-        void updateItem(const RoomSearchResult &res);
-
-protected:
-        void mousePressEvent(QMouseEvent *event) override;
-
-private:
-        QLabel *roomName_;
-        QString roomId_;
-        RoomSearchResult info_;
-};
diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp
deleted file mode 100644
index 7b545d61..00000000
--- a/src/popups/SuggestionsPopup.cpp
+++ /dev/null
@@ -1,164 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-
-#include "../Config.h"
-#include "../Utils.h"
-#include "../ui/Avatar.h"
-#include "../ui/DropShadow.h"
-#include "ChatPage.h"
-#include "PopupItem.h"
-#include "SuggestionsPopup.h"
-
-SuggestionsPopup::SuggestionsPopup(QWidget *parent)
-  : QWidget(parent)
-{
-        setAttribute(Qt::WA_ShowWithoutActivating, true);
-        setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
-
-        layout_ = new QVBoxLayout(this);
-        layout_->setMargin(0);
-        layout_->setSpacing(0);
-}
-
-QString
-SuggestionsPopup::displayName(QString room, QString user)
-{
-        return cache::displayName(room, user);
-}
-
-void
-SuggestionsPopup::addRooms(const std::vector &rooms)
-{
-        if (rooms.empty()) {
-                hide();
-                return;
-        }
-
-        const int layoutCount = (int)layout_->count();
-        const int roomCount   = (int)rooms.size();
-
-        // Remove the extra widgets from the layout.
-        if (roomCount < layoutCount)
-                removeLayoutItemsAfter(roomCount - 1);
-
-        for (int i = 0; i < roomCount; ++i) {
-                auto item = layout_->itemAt(i);
-
-                // Create a new widget if there isn't already one in that
-                // layout position.
-                if (!item) {
-                        auto room = new RoomItem(this, rooms.at(i));
-                        connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected);
-                        layout_->addWidget(room);
-                } else {
-                        // Update the current widget with the new data.
-                        auto room = qobject_cast(item->widget());
-                        if (room)
-                                room->updateItem(rooms.at(i));
-                }
-        }
-
-        resetSelection();
-        adjustSize();
-
-        resize(geometry().width(), 40 * (int)rooms.size());
-
-        selectNextSuggestion();
-}
-
-void
-SuggestionsPopup::hoverSelection()
-{
-        resetHovering();
-        setHovering(selectedItem_);
-        update();
-}
-
-void
-SuggestionsPopup::selectHoveredSuggestion()
-{
-        const auto item = layout_->itemAt(selectedItem_);
-        if (!item)
-                return;
-
-        const auto &widget = qobject_cast(item->widget());
-        emit itemSelected(displayName(ChatPage::instance()->currentRoom(), widget->selectedText()));
-
-        resetSelection();
-}
-
-void
-SuggestionsPopup::selectNextSuggestion()
-{
-        selectedItem_++;
-        if (selectedItem_ >= layout_->count())
-                selectFirstItem();
-
-        hoverSelection();
-}
-
-void
-SuggestionsPopup::selectPreviousSuggestion()
-{
-        selectedItem_--;
-        if (selectedItem_ < 0)
-                selectLastItem();
-
-        hoverSelection();
-}
-
-void
-SuggestionsPopup::resetHovering()
-{
-        for (int i = 0; i < layout_->count(); ++i) {
-                const auto item = qobject_cast(layout_->itemAt(i)->widget());
-
-                if (item)
-                        item->setHovering(false);
-        }
-}
-
-void
-SuggestionsPopup::setHovering(int pos)
-{
-        const auto &item   = layout_->itemAt(pos);
-        const auto &widget = qobject_cast(item->widget());
-
-        if (widget)
-                widget->setHovering(true);
-}
-
-void
-SuggestionsPopup::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-SuggestionsPopup::selectLastItem()
-{
-        selectedItem_ = layout_->count() - 1;
-}
-
-void
-SuggestionsPopup::removeLayoutItemsAfter(size_t startingPos)
-{
-        size_t posToRemove = layout_->count() - 1;
-
-        QLayoutItem *item;
-        while (startingPos <= posToRemove &&
-               (item = layout_->takeAt((int)posToRemove)) != nullptr) {
-                delete item->widget();
-                delete item;
-
-                posToRemove = layout_->count() - 1;
-        }
-}
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
deleted file mode 100644
index 281edddb..00000000
--- a/src/popups/SuggestionsPopup.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include "CacheStructs.h"
-
-class QVBoxLayout;
-class QLayoutItem;
-
-class SuggestionsPopup : public QWidget
-{
-        Q_OBJECT
-
-public:
-        explicit SuggestionsPopup(QWidget *parent = nullptr);
-
-        void selectHoveredSuggestion();
-
-public slots:
-        void addRooms(const std::vector &rooms);
-
-        //! Move to the next available suggestion item.
-        void selectNextSuggestion();
-        //! Move to the previous available suggestion item.
-        void selectPreviousSuggestion();
-        //! Remove hovering from all items.
-        void resetHovering();
-        //! Set hovering to the item in the given layout position.
-        void setHovering(int pos);
-
-protected:
-        void paintEvent(QPaintEvent *event) override;
-
-signals:
-        void itemSelected(const QString &user);
-
-private:
-        QString displayName(QString roomid, QString userid);
-        void hoverSelection();
-        void resetSelection() { selectedItem_ = -1; }
-        void selectFirstItem() { selectedItem_ = 0; }
-        void selectLastItem();
-        void removeLayoutItemsAfter(size_t startingPos);
-
-        QVBoxLayout *layout_;
-
-        //! Counter for tab completion (cycling).
-        int selectedItem_ = -1;
-};
diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp
deleted file mode 100644
index 56b57503..00000000
--- a/src/popups/UserMentions.cpp
+++ /dev/null
@@ -1,178 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "EventAccessors.h"
-#include "Logging.h"
-#include "UserMentions.h"
-
-using namespace popups;
-
-UserMentions::UserMentions(QWidget *parent)
-  : QWidget{parent}
-{
-        setAttribute(Qt::WA_ShowWithoutActivating, true);
-        setWindowFlags(Qt::FramelessWindowHint | Qt::Popup);
-
-        tab_layout_ = new QTabWidget(this);
-
-        top_layout_ = new QVBoxLayout(this);
-        top_layout_->setSpacing(0);
-        top_layout_->setMargin(0);
-
-        local_scroll_area_ = new QScrollArea(this);
-        local_scroll_area_->setWidgetResizable(true);
-        local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        local_scroll_widget_ = new QWidget(this);
-        local_scroll_widget_->setObjectName("local_scroll_widget");
-
-        all_scroll_area_ = new QScrollArea(this);
-        all_scroll_area_->setWidgetResizable(true);
-        all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-        all_scroll_widget_ = new QWidget(this);
-        all_scroll_widget_->setObjectName("all_scroll_widget");
-
-        // Height of the typing display.
-        QFont f;
-        f.setPointSizeF(f.pointSizeF() * 0.9);
-        const int bottomMargin = QFontMetrics(f).height() + 6;
-
-        local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_);
-        local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        local_scroll_layout_->setSpacing(0);
-        local_scroll_layout_->setObjectName("localscrollarea");
-
-        all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_);
-        all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
-        all_scroll_layout_->setSpacing(0);
-        all_scroll_layout_->setObjectName("allscrollarea");
-
-        local_scroll_area_->setWidget(local_scroll_widget_);
-        local_scroll_area_->setAlignment(Qt::AlignBottom);
-
-        all_scroll_area_->setWidget(all_scroll_widget_);
-        all_scroll_area_->setAlignment(Qt::AlignBottom);
-
-        tab_layout_->addTab(local_scroll_area_, tr("This Room"));
-        tab_layout_->addTab(all_scroll_area_, tr("All Rooms"));
-        top_layout_->addWidget(tab_layout_);
-
-        setLayout(top_layout_);
-}
-
-void
-UserMentions::initializeMentions(const QMap ¬ifs)
-{
-        nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
-
-        for (const auto &item : notifs) {
-                for (const auto ¬if : item.notifications) {
-                        const auto event_id =
-                          QString::fromStdString(mtx::accessors::event_id(notif.event));
-
-                        try {
-                                const auto room_id = QString::fromStdString(notif.room_id);
-                                const auto user_id =
-                                  QString::fromStdString(mtx::accessors::sender(notif.event));
-                                const auto body =
-                                  QString::fromStdString(mtx::accessors::body(notif.event));
-
-                                pushItem(event_id,
-                                         user_id,
-                                         body,
-                                         room_id,
-                                         ChatPage::instance()->currentRoom());
-
-                        } catch (const lmdb::error &e) {
-                                nhlog::db()->warn("error while sending desktop notification: {}",
-                                                  e.what());
-                        }
-                }
-        }
-}
-
-void
-UserMentions::showPopup()
-{
-        for (auto widget : all_scroll_layout_->findChildren()) {
-                delete widget;
-        }
-        for (auto widget : local_scroll_layout_->findChildren()) {
-                delete widget;
-        }
-
-        auto notifs = cache::getTimelineMentions();
-
-        initializeMentions(notifs);
-        show();
-}
-
-void
-UserMentions::pushItem(const QString &event_id,
-                       const QString &user_id,
-                       const QString &body,
-                       const QString &room_id,
-                       const QString ¤t_room_id)
-{
-        (void)event_id;
-        (void)user_id;
-        (void)body;
-        (void)room_id;
-        (void)current_room_id;
-        //        setUpdatesEnabled(false);
-        //
-        //        // Add to the 'all' section
-        //        TimelineItem *view_item = new TimelineItem(
-        //          mtx::events::MessageType::Text, user_id, body, true, room_id,
-        //          all_scroll_widget_);
-        //        view_item->setEventId(event_id);
-        //        view_item->hide();
-        //
-        //        all_scroll_layout_->addWidget(view_item);
-        //        QTimer::singleShot(0, this, [view_item, this]() {
-        //                view_item->show();
-        //                view_item->adjustSize();
-        //                setUpdatesEnabled(true);
-        //        });
-        //
-        //        // if it matches the current room... add it to the current room as well.
-        //        if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
-        //                // Add to the 'local' section
-        //                TimelineItem *local_view_item = new
-        //                TimelineItem(mtx::events::MessageType::Text,
-        //                                                                 user_id,
-        //                                                                 body,
-        //                                                                 true,
-        //                                                                 room_id,
-        //                                                                 local_scroll_widget_);
-        //                local_view_item->setEventId(event_id);
-        //                local_view_item->hide();
-        //                local_scroll_layout_->addWidget(local_view_item);
-        //
-        //                QTimer::singleShot(0, this, [local_view_item]() {
-        //                        local_view_item->show();
-        //                        local_view_item->adjustSize();
-        //                });
-        //        }
-}
-
-void
-UserMentions::paintEvent(QPaintEvent *)
-{
-        QStyleOption opt;
-        opt.init(this);
-        QPainter p(this);
-        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h
deleted file mode 100644
index f0b662d8..00000000
--- a/src/popups/UserMentions.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-#include 
-
-class QPaintEvent;
-class QTabWidget;
-class QScrollArea;
-class QVBoxLayout;
-
-namespace popups {
-
-class UserMentions : public QWidget
-{
-        Q_OBJECT
-public:
-        UserMentions(QWidget *parent = nullptr);
-
-        void initializeMentions(const QMap ¬ifs);
-        void showPopup();
-
-protected:
-        void paintEvent(QPaintEvent *) override;
-
-private:
-        void pushItem(const QString &event_id,
-                      const QString &user_id,
-                      const QString &body,
-                      const QString &room_id,
-                      const QString ¤t_room_id);
-        QTabWidget *tab_layout_;
-        QVBoxLayout *top_layout_;
-        QVBoxLayout *local_scroll_layout_;
-        QVBoxLayout *all_scroll_layout_;
-
-        QScrollArea *local_scroll_area_;
-        QWidget *local_scroll_widget_;
-
-        QScrollArea *all_scroll_area_;
-        QWidget *all_scroll_widget_;
-};
-}
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index a283d24e..c309daab 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -20,6 +20,7 @@
 #include "Cache.h"
 #include "ChatPage.h"
 #include "CompletionProxyModel.h"
+#include "Config.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index ad4177a4..9f926d2b 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -530,3 +530,33 @@ FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
                   });
         }
 }
+
+void
+FilteredRoomlistModel::nextRoom()
+{
+        auto r = currentRoom();
+
+        if (r) {
+                int idx = roomidToIndex(r->roomId());
+                idx++;
+                if (idx < rowCount()) {
+                        setCurrentRoom(
+                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
+                }
+        }
+}
+
+void
+FilteredRoomlistModel::previousRoom()
+{
+        auto r = currentRoom();
+
+        if (r) {
+                int idx = roomidToIndex(r->roomId());
+                idx--;
+                if (idx > 0) {
+                        setCurrentRoom(
+                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
+                }
+        }
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 1c6fa833..d3e1e1f9 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -118,6 +118,9 @@ public slots:
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
         void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
 
+        void nextRoom();
+        void previousRoom();
+
 signals:
         void currentRoomChanged();
 
-- 
cgit 1.5.1


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

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


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 4ad7bd14..0f16f205 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -171,6 +171,14 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                         activateWindow();
                 });
 
+        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+                // ensure the qml context is shutdown before we destroy all other singletons
+                // Otherwise Qml will try to access the room list or settings, after they have been
+                // destroyed
+                topLayout_->removeWidget(view_manager_->getWidget());
+                delete view_manager_->getWidget();
+        });
+
         connect(
           this,
           &ChatPage::initializeViews,
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index d2ba0dc3..283224f1 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -13,7 +13,8 @@
 #include "UserSettingsPage.h"
 
 RoomlistModel::RoomlistModel(TimelineViewManager *parent)
-  : manager(parent)
+  : QAbstractListModel(parent)
+  , manager(parent)
 {
         connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
                 auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3b3ea423..dd623f2f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -129,7 +129,8 @@ TimelineViewManager::userStatus(QString id) const
 }
 
 TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
-  : imgProvider(new MxcImageProvider())
+  : QObject(parent)
+  , imgProvider(new MxcImageProvider())
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
@@ -230,7 +231,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                                          "Error: Only enums");
 
 #ifdef USE_QUICK_VIEW
-        view      = new QQuickView();
+        view      = new QQuickView(parent);
         container = QWidget::createWindowContainer(view, parent);
 #else
         view      = new QQuickWidget(parent);
-- 
cgit 1.5.1


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5a5e3ba1..3d9d793c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -265,6 +265,7 @@ set(SRC_FILES
 
 
 	# Timeline
+	src/timeline/CommunitiesModel.cpp
 	src/timeline/EventStore.cpp
 	src/timeline/InputBar.cpp
 	src/timeline/Reaction.cpp
@@ -481,6 +482,7 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/emoji/Provider.h
 
 	# Timeline
+	src/timeline/CommunitiesModel.h
 	src/timeline/EventStore.h
 	src/timeline/InputBar.h
 	src/timeline/Reaction.h
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 21973b77..a6637467 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -43,12 +43,10 @@ Page {
 
             property string roomid
             property var tags
-            property var allTags
 
             function show(roomid_, tags_) {
                 roomid = roomid_;
                 tags = tags_;
-                allTags = Rooms.tags();
                 open();
             }
 
@@ -72,7 +70,7 @@ Page {
             }
 
             Instantiator {
-                model: roomContextMenu.allTags
+                model: Communities.tags
                 onObjectAdded: roomContextMenu.insertItem(index + 2, object)
                 onObjectRemoved: roomContextMenu.removeItem(object)
 
@@ -92,7 +90,7 @@ Page {
                         }
                     }
                     checkable: true
-                    checked: roomContextMenu.tags.includes(t)
+                    checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
                     onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
                 }
 
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
new file mode 100644
index 00000000..cedaacce
--- /dev/null
+++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "CommunitiesModel.h"
+
+#include 
+
+#include "Cache.h"
+#include "UserSettingsPage.h"
+
+CommunitiesModel::CommunitiesModel(QObject *parent)
+  : QAbstractListModel(parent)
+{}
+
+QHash
+CommunitiesModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {DisplayName, "displayName"},
+          {Tooltip, "tooltip"},
+          {ChildrenHidden, "childrenHidden"},
+        };
+}
+
+QVariant
+CommunitiesModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() == 0) {
+                switch (role) {
+                case CommunitiesModel::Roles::AvatarUrl:
+                        return QString(":/icons/icons/ui/world.png");
+                case CommunitiesModel::Roles::DisplayName:
+                        return tr("All rooms");
+                case CommunitiesModel::Roles::Tooltip:
+                        return tr("Shows all rooms without filtering.");
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return false;
+                case CommunitiesModel::Roles::Id:
+                        return "";
+                }
+        } else if (index.row() - 1 < tags_.size()) {
+                auto tag = tags_.at(index.row() - 1);
+                if (tag == "m.favourite") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/star.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Favourites");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Rooms you have favourited.");
+                        }
+                } else if (tag == "m.lowpriority") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/star.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Low Priority");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Rooms with low priority.");
+                        }
+                } else if (tag == "m.server_notice") {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/tag.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tr("Server Notices");
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tr("Messages from your server or administrator.");
+                        }
+                } else {
+                        switch (role) {
+                        case CommunitiesModel::Roles::AvatarUrl:
+                                return QString(":/icons/icons/ui/tag.png");
+                        case CommunitiesModel::Roles::DisplayName:
+                                return tag.right(2);
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tag.right(2);
+                        }
+                }
+
+                switch (role) {
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return UserSettings::instance()->hiddenTags().contains("tag:" + tag);
+                case CommunitiesModel::Roles::Id:
+                        return "tag:" + tag;
+                }
+        }
+        return QVariant();
+}
+
+void
+CommunitiesModel::initializeSidebar()
+{
+        std::set ts;
+        for (const auto &e : cache::roomInfo()) {
+                for (const auto &t : e.tags) {
+                        if (t.find("u.") == 0 || t.find("m." == 0)) {
+                                ts.insert(t);
+                        }
+                }
+        }
+
+        beginResetModel();
+        tags_.clear();
+        for (const auto &t : ts)
+                tags_.push_back(QString::fromStdString(t));
+        endResetModel();
+
+        emit tagsChanged();
+}
+
+void
+CommunitiesModel::clear()
+{
+        beginResetModel();
+        tags_.clear();
+        endResetModel();
+
+        emit tagsChanged();
+}
+
+void
+CommunitiesModel::sync(const mtx::responses::Rooms &rooms)
+{
+        bool tagsUpdated = false;
+
+        for (const auto &[roomid, room] : rooms.join) {
+                (void)roomid;
+                for (const auto &e : room.account_data.events)
+                        if (std::holds_alternative<
+                              mtx::events::AccountDataEvent>(e)) {
+                                tagsUpdated = true;
+                        }
+        }
+
+        if (tagsUpdated)
+                initializeSidebar();
+}
+
+void
+CommunitiesModel::setCurrentTagId(QString tagId)
+{
+        if (tagId.startsWith("tag:")) {
+                auto tag = tagId.remove(0, 4);
+                for (const auto &t : tags_) {
+                        if (t == tag) {
+                                this->currentTagId_ = tagId;
+                                emit currentTagIdChanged();
+                                return;
+                        }
+                }
+        }
+
+        this->currentTagId_ = "";
+        emit currentTagIdChanged();
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
new file mode 100644
index 00000000..3f6a2a4c
--- /dev/null
+++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+class CommunitiesModel : public QAbstractListModel
+{
+        Q_OBJECT
+        Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY
+                     currentTagIdChanged RESET resetCurrentTagId)
+        Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged)
+
+public:
+        enum Roles
+        {
+                AvatarUrl = Qt::UserRole,
+                DisplayName,
+                Tooltip,
+                ChildrenHidden,
+                Id,
+        };
+
+        CommunitiesModel(QObject *parent = nullptr);
+        QHash roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override
+        {
+                (void)parent;
+                return 1 + tags_.size();
+        }
+        QVariant data(const QModelIndex &index, int role) const override;
+
+public slots:
+        void initializeSidebar();
+        void sync(const mtx::responses::Rooms &rooms);
+        void clear();
+        QString currentTagId() const { return currentTagId_; }
+        void setCurrentTagId(QString tagId);
+        void resetCurrentTagId()
+        {
+                currentTagId_.clear();
+                emit currentTagIdChanged();
+        }
+        QStringList tags() const { return tags_; }
+
+signals:
+        void currentTagIdChanged();
+        void tagsChanged();
+
+private:
+        QStringList tags_;
+        QString currentTagId_;
+};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 283224f1..4dd44b30 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -485,29 +485,6 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
         sort(0);
 }
 
-QStringList
-FilteredRoomlistModel::tags()
-{
-        std::set ts;
-        for (const auto &e : cache::roomInfo()) {
-                for (const auto &t : e.tags) {
-                        if (t.find("u.") == 0) {
-                                ts.insert(t);
-                        }
-                }
-        }
-
-        QStringList ret{{
-          "m.favourite",
-          "m.lowpriority",
-        }};
-
-        for (const auto &t : ts)
-                ret.push_back(QString::fromStdString(t));
-
-        return ret;
-}
-
 void
 FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index fa991f6b..7ee0419f 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -119,7 +119,6 @@ public slots:
         void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
         void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
         void leave(QString roomid) { roomlistmodel->leave(roomid); }
-        QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index dd623f2f..faf56b85 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -135,6 +135,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
   , rooms_(new RoomlistModel(this))
+  , communities_(new CommunitiesModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -196,6 +197,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   return new FilteredRoomlistModel(self->rooms_);
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->communities_;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   auto ptr = ChatPage::instance()->userSettings().data();
@@ -324,6 +331,7 @@ void
 TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
         this->rooms_->sync(rooms_res);
+        this->communities_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -486,6 +494,7 @@ void
 TimelineViewManager::initializeRoomlist()
 {
         rooms_->initializeRooms();
+        communities_->initializeSidebar();
 }
 
 void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 68d9cd1b..556bcf4c 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,7 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/CommunitiesModel.h"
 #include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
@@ -131,7 +132,8 @@ private:
         bool isInitialSync_   = true;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms_ = nullptr;
+        RoomlistModel *rooms_          = nullptr;
+        CommunitiesModel *communities_ = nullptr;
 
         QHash userColors;
 
-- 
cgit 1.5.1


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

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


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

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

(limited to 'src/timeline/RoomlistModel.cpp')

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