summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--CMakeLists.txt6
-rw-r--r--include/ChatPage.h18
-rw-r--r--include/CommunitiesList.h42
-rw-r--r--include/CommunitiesListItem.h98
-rw-r--r--include/Community.h62
-rw-r--r--include/MatrixClient.h8
-rw-r--r--include/RoomInfoListItem.h13
-rw-r--r--include/RoomList.h6
-rw-r--r--include/ui/Theme.h5
-rw-r--r--resources/icons/ui/world.pngbin0 -> 2863 bytes
-rw-r--r--resources/icons/ui/world.svg98
-rw-r--r--resources/res.qrc1
-rw-r--r--resources/styles/nheko-dark.qss10
-rw-r--r--resources/styles/nheko.qss11
-rw-r--r--resources/styles/system.qss6
-rw-r--r--src/ChatPage.cc80
-rw-r--r--src/CommunitiesList.cc150
-rw-r--r--src/CommunitiesListItem.cc200
-rw-r--r--src/Community.cc44
-rw-r--r--src/MatrixClient.cc148
-rw-r--r--src/RoomInfoListItem.cc5
-rw-r--r--src/RoomList.cc53
23 files changed, 1054 insertions, 13 deletions
diff --git a/.gitignore b/.gitignore
index 96ace50f..e7df9077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,9 @@ ui_*.h
 
 *.autosave
 
+# VSCode
+.vscode/*
+
 #QtCtreator Qml
 *.qmlproject.user
 *.qmlproject.user.*
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b0a5c610..29c28527 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -186,6 +186,9 @@ set(SRC_FILES
     src/AvatarProvider.cc
     src/Cache.cc
     src/ChatPage.cc
+    src/CommunitiesListItem.cc
+    src/CommunitiesList.cc
+    src/Community.cc
     src/Deserializable.cc
     src/InviteeItem.cc
     src/InputValidator.cc
@@ -265,6 +268,9 @@ qt5_wrap_cpp(MOC_HEADERS
 
     include/AvatarProvider.h
     include/ChatPage.h
+    include/CommunitiesListItem.h
+    include/CommunitiesList.h
+    include/Community.h
     include/LoginPage.h
     include/MainWindow.h
     include/InviteeItem.h
diff --git a/include/ChatPage.h b/include/ChatPage.h
index 584424c0..754ee0f4 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -24,6 +24,8 @@
 #include <QTimer>
 #include <QWidget>
 
+#include "CommunitiesList.h"
+#include "Community.h"
 #include <mtx.hpp>
 
 class Cache;
@@ -80,6 +82,7 @@ private slots:
         void showUnreadMessageNotification(int count);
         void updateTopBarAvatar(const QString &roomid, const QPixmap &img);
         void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name);
+        void updateOwnCommunitiesInfo(const QList<QString> &own_communities);
         void setOwnAvatar(const QPixmap &img);
         void initialSyncCompleted(const mtx::responses::Sync &response);
         void syncCompleted(const mtx::responses::Sync &response);
@@ -126,13 +129,21 @@ private:
         QHBoxLayout *topLayout_;
         Splitter *splitter;
 
-        QFrame *sideBar_;
+        QWidget *sideBar_;
+        QWidget *communitiesSideBar_;
+        QVBoxLayout *communitiesSideBarLayout_;
         QVBoxLayout *sideBarLayout_;
+        QVBoxLayout *sideBarTopLayout_;
+        QVBoxLayout *sideBarMainLayout_;
+        QWidget *sideBarTopWidget_;
+        QVBoxLayout *sideBarTopWidgetLayout_;
 
         QFrame *content_;
         QVBoxLayout *contentLayout_;
 
+        CommunitiesList *communitiesList_;
         RoomList *room_list_;
+
         TimelineViewManager *view_manager_;
         SideBarActions *sidebarActions_;
 
@@ -145,13 +156,18 @@ private:
         QTimer *consensusTimer_;
 
         QString current_room_;
+        QString current_community_;
+
         QMap<QString, QPixmap> room_avatars_;
+        QMap<QString, QPixmap> community_avatars_;
 
         UserInfoWidget *user_info_widget_;
 
         QMap<QString, RoomState> state_manager_;
         QMap<QString, QSharedPointer<RoomSettings>> settingsManager_;
 
+        QMap<QString, QSharedPointer<Community>> communityManager_;
+
         // Keeps track of the users currently typing on each room.
         QMap<QString, QList<QString>> typingUsers_;
         QTimer *typingRefresher_;
diff --git a/include/CommunitiesList.h b/include/CommunitiesList.h
new file mode 100644
index 00000000..53715363
--- /dev/null
+++ b/include/CommunitiesList.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <QScrollArea>
+#include <QSharedPointer>
+#include <QVBoxLayout>
+#include <QWidget>
+
+#include "CommunitiesListItem.h"
+#include "Community.h"
+#include "MatrixClient.h"
+#include "ui/Theme.h"
+
+class CommunitiesList : public QWidget
+{
+        Q_OBJECT
+
+public:
+        CommunitiesList(QSharedPointer<MatrixClient> client, QWidget *parent = nullptr);
+        ~CommunitiesList();
+
+        void setCommunities(const QMap<QString, QSharedPointer<Community>> &communities);
+        void clear();
+
+        void addCommunity(QSharedPointer<Community> community, const QString &community_id);
+        void removeCommunity(const QString &community_id);
+signals:
+        void communityChanged(const QString &community_id);
+
+public slots:
+        void updateCommunityAvatar(const QString &community_id, const QPixmap &img);
+        void highlightSelectedCommunity(const QString &community_id);
+
+private:
+        QVBoxLayout *topLayout_;
+        QVBoxLayout *contentsLayout_;
+        QWidget *scrollAreaContents_;
+        QScrollArea *scrollArea_;
+
+        QMap<QString, QSharedPointer<CommunitiesListItem>> communities_;
+
+        QSharedPointer<MatrixClient> client_;
+};
diff --git a/include/CommunitiesListItem.h b/include/CommunitiesListItem.h
new file mode 100644
index 00000000..099b4fa2
--- /dev/null
+++ b/include/CommunitiesListItem.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include <QDebug>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QSharedPointer>
+#include <QWidget>
+
+#include "Community.h"
+#include "Menu.h"
+#include "ui/Theme.h"
+
+class CommunitiesListItem : 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)
+
+public:
+        CommunitiesListItem(QSharedPointer<Community> community,
+                            QString community_id,
+                            QWidget *parent = nullptr);
+
+        ~CommunitiesListItem();
+
+        void setCommunity(QSharedPointer<Community> community);
+
+        inline bool isPressed() const;
+        inline void setAvatar(const QImage &avatar_image);
+
+        QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; }
+        QColor hoverBackgroundColor() const { return hoverBackgroundColor_; }
+        QColor backgroundColor() const { return backgroundColor_; }
+
+        void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; }
+        void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; }
+        void setBackgroundColor(QColor &color) { backgroundColor_ = color; }
+
+        QColor highlightedBackgroundColor_;
+        QColor hoverBackgroundColor_;
+        QColor backgroundColor_;
+
+signals:
+        void clicked(const QString &community_id);
+
+public slots:
+        void setPressedState(bool state);
+
+protected:
+        void mousePressEvent(QMouseEvent *event) override;
+        void paintEvent(QPaintEvent *event) override;
+        void contextMenuEvent(QContextMenuEvent *event) override;
+
+private:
+        const int IconSize = 55;
+
+        QSharedPointer<Community> community_;
+        QString communityId_;
+        QString communityName_;
+        QString communityShortDescription;
+
+        QPixmap communityAvatar_;
+
+        Menu *menu_;
+        bool isPressed_ = false;
+};
+
+inline bool
+CommunitiesListItem::isPressed() const
+{
+        return isPressed_;
+}
+
+inline void
+CommunitiesListItem::setAvatar(const QImage &avatar_image)
+{
+        communityAvatar_ = QPixmap::fromImage(
+          avatar_image.scaled(IconSize, IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
+        update();
+}
+
+class WorldCommunityListItem : public CommunitiesListItem
+{
+        Q_OBJECT
+public:
+        WorldCommunityListItem(QWidget *parent = nullptr);
+        ~WorldCommunityListItem();
+
+protected:
+        void mousePressEvent(QMouseEvent *event) override;
+        void paintEvent(QPaintEvent *event) override;
+
+private:
+        const int IconSize = 55;
+};
diff --git a/include/Community.h b/include/Community.h
new file mode 100644
index 00000000..0d70dee1
--- /dev/null
+++ b/include/Community.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <QJsonObject>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+class Community : public QObject
+{
+        Q_OBJECT
+
+public:
+        void parseProfile(const QJsonObject &profile);
+        void parseRooms(const QJsonObject &rooms);
+
+        inline QUrl getAvatar() const;
+        inline QString getName() const;
+        inline QString getShortDescription() const;
+        inline QString getLongDescription() const;
+        inline const QList<QString> getRoomList() const;
+
+signals:
+        void roomsChanged(QList<QString> &rooms);
+
+private:
+        QUrl avatar_;
+        QString name_;
+        QString short_description_;
+        QString long_description_;
+
+        QList<QString> rooms_;
+};
+
+inline QUrl
+Community::getAvatar() const
+{
+        return avatar_;
+}
+
+inline QString
+Community::getName() const
+{
+        return name_;
+}
+
+inline QString
+Community::getShortDescription() const
+{
+        return short_description_;
+}
+
+inline QString
+Community::getLongDescription() const
+{
+        return long_description_;
+}
+
+inline const QList<QString>
+Community::getRoomList() const
+{
+        return rooms_;
+}
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 2627f578..8936003f 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -48,6 +48,9 @@ public:
         void versions() noexcept;
         void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
         void fetchUserAvatar(const QString &userId, const QUrl &avatarUrl);
+        void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl);
+        void fetchCommunityProfile(const QString &communityId);
+        void fetchCommunityRooms(const QString &communityId);
         void fetchOwnAvatar(const QUrl &avatar_url);
         void downloadImage(const QString &event_id, const QUrl &url);
         void downloadFile(const QString &event_id, const QUrl &url);
@@ -71,6 +74,7 @@ public:
 
 public slots:
         void getOwnProfile() noexcept;
+        void getOwnCommunities() noexcept;
         void logout() noexcept;
 
         void setServer(const QString &server)
@@ -103,12 +107,16 @@ signals:
                                  const QString &url,
                                  const QByteArray &data);
         void userAvatarRetrieved(const QString &userId, const QImage &img);
+        void communityAvatarRetrieved(const QString &communityId, const QPixmap &img);
+        void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile);
+        void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms);
         void ownAvatarRetrieved(const QPixmap &img);
         void imageDownloaded(const QString &event_id, const QPixmap &img);
         void fileDownloaded(const QString &event_id, const QByteArray &data);
 
         // Returned profile data for the user's account.
         void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name);
+        void getOwnCommunitiesResponse(const QList<QString> &own_communities);
         void initialSyncCompleted(const mtx::responses::Sync &response);
         void initialSyncFailed(const QString &msg);
         void syncCompleted(const mtx::responses::Sync &response);
diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h
index 799e95bb..5cfea783 100644
--- a/include/RoomInfoListItem.h
+++ b/include/RoomInfoListItem.h
@@ -73,9 +73,10 @@ public:
         void clearUnreadMessageCount();
         void setState(const RoomState &state);
 
-        bool isPressed() const { return isPressed_; }
-        RoomState state() const { return state_; }
-        int unreadMessageCount() const { return unreadMsgCount_; }
+        QString roomId();
+        bool isPressed() const { return isPressed_; };
+        RoomState state() const { return state_; };
+        int unreadMessageCount() const { return unreadMsgCount_; };
 
         void setAvatar(const QImage &avatar_image);
         void setDescriptionMessage(const DescInfo &info);
@@ -182,3 +183,9 @@ private:
         QRectF acceptBtnRegion_;
         QRectF declineBtnRegion_;
 };
+
+inline QString
+RoomInfoListItem::roomId()
+{
+        return roomId_;
+}
diff --git a/include/RoomList.h b/include/RoomList.h
index 6b2151a2..d10cf5db 100644
--- a/include/RoomList.h
+++ b/include/RoomList.h
@@ -64,6 +64,8 @@ public:
                      const QString &room_id);
         void addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room);
         void removeRoom(const QString &room_id, bool reset);
+        void setFilterRooms(bool filterRooms);
+        void setRoomFilter(QList<QString> room_ids);
 
 signals:
         void roomChanged(const QString &room_id);
@@ -105,6 +107,10 @@ private:
         QSharedPointer<dialogs::LeaveRoom> leaveRoomDialog_;
 
         QMap<QString, QSharedPointer<RoomInfoListItem>> rooms_;
+        QString selectedRoom_;
+
+        bool filterRooms_          = false;
+        QList<QString> roomFilter_ = QList<QString>(); // which rooms to include in the room list
 
         QSharedPointer<MatrixClient> client_;
         QSharedPointer<Cache> cache_;
diff --git a/include/ui/Theme.h b/include/ui/Theme.h
index c6c39553..c2e4ab59 100644
--- a/include/ui/Theme.h
+++ b/include/ui/Theme.h
@@ -13,8 +13,9 @@ enum class AvatarType
 };
 
 namespace sidebar {
-static const int SmallSize  = 60;
-static const int NormalSize = 300;
+static const int SmallSize              = 60;
+static const int NormalSize             = 300;
+static const int CommunitiesSidebarSize = 64;
 }
 // Default font size.
 const int FontSize = 16;
diff --git a/resources/icons/ui/world.png b/resources/icons/ui/world.png
new file mode 100644
index 00000000..d687d141
--- /dev/null
+++ b/resources/icons/ui/world.png
Binary files differdiff --git a/resources/icons/ui/world.svg b/resources/icons/ui/world.svg
new file mode 100644
index 00000000..c3acf162
--- /dev/null
+++ b/resources/icons/ui/world.svg
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48px"
+   height="48px"
+   id="svg3304"
+   sodipodi:version="0.32"
+   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+   sodipodi:docname="world.svg"
+   version="1.1"
+   inkscape:export-filename="/home/max/Program/nheko/resources/icons/world.png"
+   inkscape:export-xdpi="256"
+   inkscape:export-ydpi="256">
+  <defs
+     id="defs3306" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="7.9375"
+     inkscape:cx="11.531663"
+     inkscape:cy="15.491127"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:grid-points="true"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:window-width="1280"
+     inkscape:window-height="704"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata3309">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer">
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 12.998425,32.054724 c 6.286614,-2.35748 15.716536,-2.35748 22.00315,0"
+       id="path5512"
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 11.402021,24 H 36.597979"
+       id="path5516"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 20.856693,11.074078 c -3.929134,8.210961 -3.929134,18.426709 0,26.284977"
+       id="path5520"
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path6250"
+       d="m 12.998425,15.945276 c 6.286614,2.35748 15.716536,2.35748 22.00315,0"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       inkscape:connector-curvature="0" />
+    <path
+       sodipodi:nodetypes="cc"
+       id="path9156"
+       d="m 27.143307,11.074078 c 3.929134,8.210961 3.929134,18.426709 0,26.284977"
+       style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       inkscape:connector-curvature="0" />
+    <circle
+       style="fill:none;fill-opacity:0.67634858;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path1767"
+       cx="24"
+       cy="24"
+       r="13.609846" />
+  </g>
+</svg>
diff --git a/resources/res.qrc b/resources/res.qrc
index 83415e9b..a5461718 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -22,6 +22,7 @@
         <file>icons/ui/paper-clip-outline@2x.png</file>
         <file>icons/ui/angle-pointing-to-left.png</file>
         <file>icons/ui/angle-pointing-to-left@2x.png</file>
+        <file>icons/ui/world.png</file>
         <file>icons/ui/angle-arrow-down.png</file>
         <file>icons/ui/angle-arrow-down@2x.png</file>
         <file>icons/ui/arrow-pointing-down.png</file>
diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss
index a78fb612..26425590 100644
--- a/resources/styles/nheko-dark.qss
+++ b/resources/styles/nheko-dark.qss
@@ -17,6 +17,10 @@ RoomList > * {
     background-color: #383c4a;
 }
 
+CommunitiesList,
+CommunitiesList > * {
+    background-color: #383c4a;
+}
 FlatButton {
     qproperty-foregroundColor: #caccd1;
     qproperty-backgroundColor: #333;
@@ -54,6 +58,12 @@ RoomInfoListItem {
     qproperty-btnTextColor: white;
 }
 
+CommunitiesListItem {
+    qproperty-highlightedBackgroundColor: #5294e2;
+    qproperty-hoverBackgroundColor: #39679e;
+    qproperty-backgroundColor: #383c4a;
+}
+
 LoadingIndicator {
     qproperty-color: #caccd1;
 }
diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss
index ce86e212..c135c12a 100644
--- a/resources/styles/nheko.qss
+++ b/resources/styles/nheko.qss
@@ -17,6 +17,11 @@ RoomList > * {
     background-color: white;
 }
 
+CommunitiesList,
+CommunitiesList > * {
+    background-color: white;
+}
+
 FlatButton {
     qproperty-foregroundColor: #333;
 }
@@ -52,6 +57,12 @@ RoomInfoListItem {
     qproperty-btnTextColor: #333;
 }
 
+CommunitiesListItem {
+    qproperty-highlightedBackgroundColor: #38A3D8;
+    qproperty-hoverBackgroundColor: rgba(200, 200, 200, 128);
+    qproperty-backgroundColor: white;
+}
+
 #ChatPageLoadSpinner {
     qproperty-color: #acc7dc;
 }
diff --git a/resources/styles/system.qss b/resources/styles/system.qss
index afb2ad26..42aba09d 100644
--- a/resources/styles/system.qss
+++ b/resources/styles/system.qss
@@ -60,6 +60,12 @@ RoomInfoListItem {
     qproperty-btnTextColor: palette(text);
 }
 
+CommunitiesListItem {
+    qproperty-highlightedBackgroundColor: palette(highlight);
+    qproperty-hoverBackgroundColor: palette(mid);
+    qproperty-backgroundColor: palette(window);
+}
+
 LoadingIndicator {
     qproperty-color: palette(highlight);
 }
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 3958e2c2..3a78e1cc 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -60,6 +60,17 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
         topLayout_->setSpacing(0);
         topLayout_->setMargin(0);
 
+        communitiesSideBar_ = new QWidget(this);
+        communitiesSideBar_->setFixedWidth(ui::sidebar::CommunitiesSidebarSize);
+        communitiesSideBarLayout_ = new QVBoxLayout(communitiesSideBar_);
+        communitiesSideBarLayout_->setSpacing(0);
+        communitiesSideBarLayout_->setMargin(0);
+
+        communitiesList_ = new CommunitiesList(client, this);
+        communitiesSideBarLayout_->addWidget(communitiesList_);
+        // communitiesSideBarLayout_->addStretch(1);
+        topLayout_->addWidget(communitiesSideBar_);
+
         auto splitter = new Splitter(this);
         splitter->setHandleWidth(0);
 
@@ -72,7 +83,18 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
         sideBarLayout_->setSpacing(0);
         sideBarLayout_->setMargin(0);
 
-        sidebarActions_ = new SideBarActions(this);
+        sideBarTopLayout_ = new QVBoxLayout();
+        sideBarTopLayout_->setSpacing(0);
+        sideBarTopLayout_->setMargin(0);
+        sideBarMainLayout_ = new QVBoxLayout();
+        sideBarMainLayout_->setSpacing(0);
+        sideBarMainLayout_->setMargin(0);
+
+        sideBarLayout_->addLayout(sideBarTopLayout_);
+        sideBarLayout_->addLayout(sideBarMainLayout_);
+
+        sideBarTopWidget_ = new QWidget(sideBar_);
+        sidebarActions_   = new SideBarActions(this);
         connect(
           sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
         connect(
@@ -87,6 +109,10 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
         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");
@@ -274,6 +300,32 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
                 &MatrixClient::getOwnProfileResponse,
                 this,
                 &ChatPage::updateOwnProfileInfo);
+        connect(client_.data(),
+                SIGNAL(getOwnCommunitiesResponse(QList<QString>)),
+                this,
+                SLOT(updateOwnCommunitiesInfo(QList<QString>)));
+        connect(client_.data(),
+                &MatrixClient::communityProfileRetrieved,
+                this,
+                [=](QString communityId, QJsonObject profile) {
+                        communityManager_[communityId]->parseProfile(profile);
+                });
+        connect(client_.data(),
+                &MatrixClient::communityRoomsRetrieved,
+                this,
+                [=](QString communityId, QJsonObject rooms) {
+                        communityManager_[communityId]->parseRooms(rooms);
+
+                        if (communityId == current_community_) {
+                                if (communityId == "world") {
+                                        room_list_->setFilterRooms(false);
+                                } else {
+                                        room_list_->setRoomFilter(
+                                          communityManager_[communityId]->getRoomList());
+                                }
+                        }
+                });
+
         connect(client_.data(), &MatrixClient::ownAvatarRetrieved, this, &ChatPage::setOwnAvatar);
         connect(client_.data(), &MatrixClient::joinedRoom, this, [=](const QString &room_id) {
                 emit showNotification("You joined the room.");
@@ -304,6 +356,19 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
                 }
         });
 
+        connect(communitiesList_,
+                &CommunitiesList::communityChanged,
+                this,
+                [=](const QString &communityId) {
+                        current_community_ = communityId;
+                        if (communityId == "world") {
+                                room_list_->setFilterRooms(false);
+                        } else {
+                                room_list_->setRoomFilter(
+                                  communityManager_[communityId]->getRoomList());
+                        }
+                });
+
         AvatarProvider::init(client);
 
         instance_ = this;
@@ -359,6 +424,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
         client_->setServer(homeserver);
         client_->setAccessToken(token);
         client_->getOwnProfile();
+        client_->getOwnCommunities();
 
         cache_ = QSharedPointer<Cache>(new Cache(userid));
         room_list_->setCache(cache_);
@@ -501,6 +567,18 @@ ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_na
 }
 
 void
+ChatPage::updateOwnCommunitiesInfo(const QList<QString> &own_communities)
+{
+        for (int i = 0; i < own_communities.size(); i++) {
+                QSharedPointer<Community> community = QSharedPointer<Community>(new Community());
+
+                communityManager_[own_communities[i]] = community;
+        }
+
+        communitiesList_->setCommunities(communityManager_);
+}
+
+void
 ChatPage::changeTopRoomInfo(const QString &room_id)
 {
         if (!state_manager_.contains(room_id))
diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc
new file mode 100644
index 00000000..c40155e5
--- /dev/null
+++ b/src/CommunitiesList.cc
@@ -0,0 +1,150 @@
+#include "CommunitiesList.h"
+
+#include <QLabel>
+
+CommunitiesList::CommunitiesList(QSharedPointer<MatrixClient> client, QWidget *parent)
+  : QWidget(parent)
+  , client_(client)
+{
+        QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+        sizePolicy.setHorizontalStretch(0);
+        sizePolicy.setVerticalStretch(1);
+        setSizePolicy(sizePolicy);
+
+        setStyleSheet("border-style: none;");
+
+        topLayout_ = new QVBoxLayout(this);
+        topLayout_->setSpacing(0);
+        topLayout_->setMargin(0);
+
+        setFixedWidth(ui::sidebar::CommunitiesSidebarSize);
+
+        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);
+
+        scrollAreaContents_ = new QWidget();
+
+        contentsLayout_ = new QVBoxLayout(scrollAreaContents_);
+        contentsLayout_->setSpacing(0);
+        contentsLayout_->setMargin(0);
+
+        WorldCommunityListItem *world_list_item = new WorldCommunityListItem();
+        contentsLayout_->addWidget(world_list_item);
+        communities_.insert("world", QSharedPointer<CommunitiesListItem>(world_list_item));
+        connect(world_list_item,
+                &WorldCommunityListItem::clicked,
+                this,
+                &CommunitiesList::highlightSelectedCommunity);
+        contentsLayout_->addStretch(1);
+
+        scrollArea_->setWidget(scrollAreaContents_);
+        topLayout_->addWidget(scrollArea_);
+
+        connect(client_.data(),
+                &MatrixClient::communityProfileRetrieved,
+                this,
+                [=](QString communityId, QJsonObject profile) {
+                        client_->fetchCommunityAvatar(communityId,
+                                                      QUrl(profile["avatar_url"].toString()));
+                });
+        connect(client_.data(),
+                SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)),
+                this,
+                SLOT(updateCommunityAvatar(const QString &, const QPixmap &)));
+}
+
+CommunitiesList::~CommunitiesList() {}
+
+void
+CommunitiesList::setCommunities(const QMap<QString, QSharedPointer<Community>> &communities)
+{
+        communities_.clear();
+
+        // TODO: still not sure how to handle the "world" special-case
+        WorldCommunityListItem *world_list_item = new WorldCommunityListItem();
+        communities_.insert("world", QSharedPointer<CommunitiesListItem>(world_list_item));
+        connect(world_list_item,
+                &WorldCommunityListItem::clicked,
+                this,
+                &CommunitiesList::highlightSelectedCommunity);
+        contentsLayout_->insertWidget(0, world_list_item);
+
+        for (auto it = communities.constBegin(); it != communities.constEnd(); it++) {
+                const auto community_id = it.key();
+                const auto community    = it.value();
+
+                addCommunity(community, community_id);
+
+                client_->fetchCommunityProfile(community_id);
+                client_->fetchCommunityRooms(community_id);
+        }
+
+        world_list_item->setPressedState(true);
+        emit communityChanged("world");
+}
+
+void
+CommunitiesList::clear()
+{
+        communities_.clear();
+}
+
+void
+CommunitiesList::addCommunity(QSharedPointer<Community> community, const QString &community_id)
+{
+        CommunitiesListItem *list_item =
+          new CommunitiesListItem(community, community_id, scrollArea_);
+
+        communities_.insert(community_id, QSharedPointer<CommunitiesListItem>(list_item));
+
+        client_->fetchCommunityAvatar(community_id, community->getAvatar());
+
+        contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item);
+
+        connect(list_item,
+                &CommunitiesListItem::clicked,
+                this,
+                &CommunitiesList::highlightSelectedCommunity);
+}
+
+void
+CommunitiesList::removeCommunity(const QString &community_id)
+{
+        communities_.remove(community_id);
+}
+
+void
+CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img)
+{
+        if (!communities_.contains(community_id)) {
+                qWarning() << "Avatar update on nonexistent community" << community_id;
+                return;
+        }
+
+        communities_.value(community_id)->setAvatar(img.toImage());
+}
+
+void
+CommunitiesList::highlightSelectedCommunity(const QString &community_id)
+{
+        emit communityChanged(community_id);
+
+        if (!communities_.contains(community_id)) {
+                qDebug() << "CommunitiesList: clicked unknown community";
+                return;
+        }
+
+        for (auto it = communities_.constBegin(); it != communities_.constEnd(); it++) {
+                if (it.key() != community_id) {
+                        it.value()->setPressedState(false);
+                } else {
+                        it.value()->setPressedState(true);
+                        scrollArea_->ensureWidgetVisible(
+                          qobject_cast<QWidget *>(it.value().data()));
+                }
+        }
+}
diff --git a/src/CommunitiesListItem.cc b/src/CommunitiesListItem.cc
new file mode 100644
index 00000000..a7789df7
--- /dev/null
+++ b/src/CommunitiesListItem.cc
@@ -0,0 +1,200 @@
+#include "CommunitiesListItem.h"
+
+CommunitiesListItem::CommunitiesListItem(QSharedPointer<Community> community,
+                                         QString community_id,
+                                         QWidget *parent)
+  : QWidget(parent)
+  , community_(community)
+  , communityId_(community_id)
+{
+        // menu_ = new Menu(this);
+        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+        setFixedHeight(ui::sidebar::CommunitiesSidebarSize);
+        setFixedWidth(ui::sidebar::CommunitiesSidebarSize);
+}
+
+CommunitiesListItem::~CommunitiesListItem() {}
+
+void
+CommunitiesListItem::setCommunity(QSharedPointer<Community> community)
+{
+        community_ = community;
+}
+
+void
+CommunitiesListItem::setPressedState(bool state)
+{
+        if (isPressed_ != state) {
+                isPressed_ = state;
+                update();
+        }
+}
+
+void
+CommunitiesListItem::mousePressEvent(QMouseEvent *event)
+{
+        if (event->buttons() == Qt::RightButton) {
+                QWidget::mousePressEvent(event);
+                return;
+        }
+
+        emit clicked(communityId_);
+
+        setPressedState(true);
+}
+
+void
+CommunitiesListItem::paintEvent(QPaintEvent *event)
+{
+        Q_UNUSED(event);
+
+        QPainter p(this);
+        p.setRenderHint(QPainter::TextAntialiasing);
+        p.setRenderHint(QPainter::SmoothPixmapTransform);
+        p.setRenderHint(QPainter::Antialiasing);
+
+        if (isPressed_)
+                p.fillRect(rect(), highlightedBackgroundColor_);
+        else if (underMouse())
+                p.fillRect(rect(), hoverBackgroundColor_);
+        else
+                p.fillRect(rect(), backgroundColor_);
+
+        QFont font;
+        font.setPixelSize(conf::fontSize);
+
+        p.setPen(QColor("#333"));
+
+        QRect avatarRegion((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize);
+
+        font.setBold(false);
+        p.setPen(Qt::NoPen);
+
+        // We using the first letter of room's name.
+        if (communityAvatar_.isNull()) {
+                QBrush brush;
+                brush.setStyle(Qt::SolidPattern);
+                brush.setColor("#eee");
+
+                p.setPen(Qt::NoPen);
+                p.setBrush(brush);
+
+                p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2);
+
+                font.setPixelSize(conf::roomlist::fonts::bubble);
+                p.setFont(font);
+                p.setPen(QColor("#000"));
+                p.setBrush(Qt::NoBrush);
+                p.drawText(
+                  avatarRegion.translated(0, -1), Qt::AlignCenter, QChar(community_->getName()[0]));
+        } else {
+                p.save();
+
+                QPainterPath path;
+                path.addEllipse(
+                  (width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize);
+                p.setClipPath(path);
+
+                p.drawPixmap(avatarRegion, communityAvatar_);
+                p.restore();
+        }
+
+        // TODO: Discord-style community ping counts?
+        /*if (unreadMsgCount_ > 0) {
+                QColor textColor("white");
+                QColor backgroundColor("#38A3D8");
+
+                QBrush brush;
+                brush.setStyle(Qt::SolidPattern);
+                brush.setColor(backgroundColor);
+
+                if (isPressed_)
+                        brush.setColor(textColor);
+
+                QFont unreadCountFont;
+                unreadCountFont.setPixelSize(conf::roomlist::fonts::badge);
+                unreadCountFont.setBold(true);
+
+                p.setBrush(brush);
+                p.setPen(Qt::NoPen);
+                p.setFont(unreadCountFont);
+
+                int diameter = 20;
+
+                QRectF r(
+                  width() - diameter - 5, height() - diameter - 5, diameter, diameter);
+
+                p.setPen(Qt::NoPen);
+                p.drawEllipse(r);
+
+                p.setPen(QPen(textColor));
+
+                if (isPressed_)
+                        p.setPen(QPen(backgroundColor));
+
+                p.setBrush(Qt::NoBrush);
+                p.drawText(
+                  r.translated(0, -0.5), Qt::AlignCenter, QString::number(unreadMsgCount_));
+        }*/
+}
+
+void
+CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event)
+{
+        Q_UNUSED(event);
+
+        // menu_->popup(event->globalPos());
+}
+
+WorldCommunityListItem::WorldCommunityListItem(QWidget *parent)
+  : CommunitiesListItem(QSharedPointer<Community>(), "", parent)
+{}
+
+WorldCommunityListItem::~WorldCommunityListItem() {}
+
+void
+WorldCommunityListItem::mousePressEvent(QMouseEvent *event)
+{
+        if (event->buttons() == Qt::RightButton) {
+                QWidget::mousePressEvent(event);
+                return;
+        }
+
+        emit CommunitiesListItem::clicked("world");
+
+        setPressedState(true);
+}
+
+void
+WorldCommunityListItem::paintEvent(QPaintEvent *event)
+{
+        Q_UNUSED(event);
+
+        static QPixmap worldIcon(":/icons/icons/ui/world.png");
+
+        QPainter p(this);
+        p.setRenderHint(QPainter::SmoothPixmapTransform);
+        p.setRenderHint(QPainter::Antialiasing);
+
+        if (isPressed())
+                p.fillRect(rect(), highlightedBackgroundColor_);
+        else if (underMouse())
+                p.fillRect(rect(), hoverBackgroundColor_);
+        else
+                p.fillRect(rect(), backgroundColor_);
+
+        QBrush brush;
+        brush.setStyle(Qt::SolidPattern);
+        brush.setColor("#FFFFFF");
+
+        p.setPen(Qt::NoPen);
+        p.setBrush(brush);
+
+        QRect avatarRegion((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize);
+        p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2);
+        QPainterPath path;
+        path.addEllipse((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize);
+        p.setClipPath(path);
+
+        p.drawPixmap(avatarRegion, worldIcon);
+}
diff --git a/src/Community.cc b/src/Community.cc
new file mode 100644
index 00000000..df425e88
--- /dev/null
+++ b/src/Community.cc
@@ -0,0 +1,44 @@
+#include "include/Community.h"
+
+#include <QJsonArray>
+#include <QJsonValue>
+
+void
+Community::parseProfile(const QJsonObject &profile)
+{
+        if (profile["name"].type() == QJsonValue::Type::String) {
+                name_ = profile["name"].toString();
+        } else {
+                name_ = "Unnamed Community"; // TODO: what is correct here?
+        }
+
+        if (profile["avatar_url"].type() == QJsonValue::Type::String) {
+                avatar_ = QUrl(profile["avatar_url"].toString());
+        } else {
+                avatar_ = QUrl();
+        }
+
+        if (profile["short_description"].type() == QJsonValue::Type::String) {
+                short_description_ = profile["short_description"].toString();
+        } else {
+                short_description_ = "";
+        }
+
+        if (profile["long_description"].type() == QJsonValue::Type::String) {
+                long_description_ = profile["long_description"].toString();
+        } else {
+                long_description_ = "";
+        }
+}
+
+void
+Community::parseRooms(const QJsonObject &rooms)
+{
+        rooms_.clear();
+
+        for (auto i = 0; i < rooms["chunk"].toArray().size(); i++) {
+                rooms_.append(rooms["chunk"].toArray()[i].toObject()["room_id"].toString());
+        }
+
+        emit roomsChanged(rooms_);
+}
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 1b2e020d..72467385 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -112,7 +112,6 @@ MatrixClient::login(const QString &username, const QString &password) noexcept
                 }
         });
 }
-
 void
 MatrixClient::logout() noexcept
 {
@@ -445,6 +444,46 @@ MatrixClient::getOwnProfile() noexcept
 }
 
 void
+MatrixClient::getOwnCommunities() noexcept
+{
+        QUrlQuery query;
+        query.addQueryItem("access_token", token_);
+
+        QUrl endpoint(server_);
+        endpoint.setPath(clientApiUrl_ + "/joined_groups");
+        endpoint.setQuery(query);
+
+        QNetworkRequest request(QString(endpoint.toEncoded()));
+
+        QNetworkReply *reply = get(request);
+        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+                reply->deleteLater();
+
+                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+                if (status >= 400) {
+                        qWarning() << reply->errorString();
+                        return;
+                }
+
+                auto data = reply->readAll();
+                auto json = QJsonDocument::fromJson(data).object();
+
+                try {
+                        QList<QString> response;
+                        for (auto it = json["groups"].toArray().constBegin();
+                             it != json["groups"].toArray().constEnd();
+                             it++) {
+                                response.append(it->toString());
+                        }
+                        emit getOwnCommunitiesResponse(response);
+                } catch (DeserializationException &e) {
+                        qWarning() << "Own communities:" << e.what();
+                }
+        });
+}
+
+void
 MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
 {
         QList<QString> url_parts = avatar_url.toString().split("mxc://");
@@ -491,6 +530,113 @@ MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url)
 }
 
 void
+MatrixClient::fetchCommunityAvatar(const QString &communityId, const QUrl &avatar_url)
+{
+        QList<QString> url_parts = avatar_url.toString().split("mxc://");
+
+        if (url_parts.size() != 2) {
+                qDebug() << "Invalid format for community avatar " << avatar_url.toString();
+                return;
+        }
+
+        QUrlQuery query;
+        query.addQueryItem("width", "512");
+        query.addQueryItem("height", "512");
+        query.addQueryItem("method", "crop");
+
+        QString media_url =
+          QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
+
+        QUrl endpoint(media_url);
+        endpoint.setQuery(query);
+
+        QNetworkRequest avatar_request(endpoint);
+
+        QNetworkReply *reply = get(avatar_request);
+        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
+                reply->deleteLater();
+
+                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+                if (status == 0 || status >= 400) {
+                        qWarning() << reply->errorString();
+                        return;
+                }
+
+                auto img = reply->readAll();
+
+                if (img.size() == 0)
+                        return;
+
+                QPixmap pixmap;
+                pixmap.loadFromData(img);
+
+                emit communityAvatarRetrieved(communityId, pixmap);
+        });
+}
+
+void
+MatrixClient::fetchCommunityProfile(const QString &communityId)
+{
+        QUrlQuery query;
+        query.addQueryItem("access_token", token_);
+
+        QUrl endpoint(server_);
+        endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/profile");
+        endpoint.setQuery(query);
+
+        QNetworkRequest request(QString(endpoint.toEncoded()));
+
+        QNetworkReply *reply = get(request);
+
+        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
+                reply->deleteLater();
+
+                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+                if (status >= 400) {
+                        qWarning() << reply->errorString();
+                        return;
+                }
+
+                auto data       = reply->readAll();
+                const auto json = QJsonDocument::fromJson(data).object();
+
+                emit communityProfileRetrieved(communityId, json);
+        });
+}
+
+void
+MatrixClient::fetchCommunityRooms(const QString &communityId)
+{
+        QUrlQuery query;
+        query.addQueryItem("access_token", token_);
+
+        QUrl endpoint(server_);
+        endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/rooms");
+        endpoint.setQuery(query);
+
+        QNetworkRequest request(QString(endpoint.toEncoded()));
+
+        QNetworkReply *reply = get(request);
+        connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() {
+                reply->deleteLater();
+
+                int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+                if (status >= 400) {
+                        qWarning() << reply->errorString();
+                        return;
+                }
+
+                auto data       = reply->readAll();
+                const auto json = QJsonDocument::fromJson(data).object();
+
+                emit communityRoomsRetrieved(communityId, json);
+        });
+}
+
+void
 MatrixClient::fetchUserAvatar(const QString &userId, const QUrl &avatarUrl)
 {
         QList<QString> url_parts = avatarUrl.toString().split("mxc://");
diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
index 551895d6..f8989948 100644
--- a/src/RoomInfoListItem.cc
+++ b/src/RoomInfoListItem.cc
@@ -315,10 +315,7 @@ RoomInfoListItem::clearUnreadMessageCount()
 void
 RoomInfoListItem::setPressedState(bool state)
 {
-        if (!isPressed_ && state) {
-                isPressed_ = state;
-                update();
-        } else if (isPressed_ && !state) {
+        if (isPressed_ != state) {
                 isPressed_ = state;
                 update();
         }
diff --git a/src/RoomList.cc b/src/RoomList.cc
index 0274cefe..30be6cf6 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -47,7 +47,7 @@ RoomList::RoomList(QSharedPointer<MatrixClient> client,
         scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
         scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
         scrollArea_->setWidgetResizable(true);
-        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
+        scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter);
 
         scrollAreaContents_ = new QWidget(this);
 
@@ -181,6 +181,8 @@ RoomList::setInitialRooms(const QMap<QString, QSharedPointer<RoomSettings>> &set
         if (rooms_.isEmpty())
                 return;
 
+        setFilterRooms(filterRooms_);
+
         auto first_room = rooms_.first();
         first_room->setPressedState(true);
 
@@ -271,6 +273,8 @@ RoomList::highlightSelectedRoom(const QString &room_id)
                           qobject_cast<QWidget *>(it.value().data()));
                 }
         }
+
+        selectedRoom_ = room_id;
 }
 
 void
@@ -374,6 +378,46 @@ RoomList::closeLeaveRoomDialog(bool leaving, const QString &room_id)
 }
 
 void
+RoomList::setFilterRooms(bool filterRooms)
+{
+        filterRooms_ = filterRooms;
+
+        for (int i = 0; i < contentsLayout_->count(); i++) {
+                // If roomFilter_ contains the room for the current RoomInfoListItem,
+                // show the list item, otherwise hide it
+                RoomInfoListItem *listitem =
+                  (RoomInfoListItem *)contentsLayout_->itemAt(i)->widget();
+
+                if (listitem != nullptr) {
+                        if (!filterRooms) {
+                                contentsLayout_->itemAt(i)->widget()->show();
+                        } else if (roomFilter_.contains(listitem->roomId())) {
+                                contentsLayout_->itemAt(i)->widget()->show();
+                        } else {
+                                contentsLayout_->itemAt(i)->widget()->hide();
+                        }
+                }
+        }
+
+        if (filterRooms_ && !roomFilter_.contains(selectedRoom_)) {
+                RoomInfoListItem *firstVisibleRoom = nullptr;
+                for (int i = 0; i < contentsLayout_->count(); i++) {
+                        QWidget *item = contentsLayout_->itemAt(i)->widget();
+                        if (item != nullptr && item->isVisible()) {
+                                firstVisibleRoom = (RoomInfoListItem *)item;
+                                break;
+                        }
+                }
+                if (firstVisibleRoom != nullptr) {
+                        highlightSelectedRoom(firstVisibleRoom->roomId());
+                }
+        } else {
+                scrollArea_->ensureWidgetVisible(
+                  qobject_cast<QWidget *>(rooms_.value(selectedRoom_).data()));
+        }
+}
+
+void
 RoomList::paintEvent(QPaintEvent *)
 {
         QStyleOption opt;
@@ -394,6 +438,13 @@ RoomList::syncInvites(const std::map<std::string, mtx::responses::InvitedRoom> &
 }
 
 void
+RoomList::setRoomFilter(QList<QString> room_ids)
+{
+        roomFilter_ = room_ids;
+        setFilterRooms(true);
+}
+
+void
 RoomList::addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room)
 {
         auto room_item = new RoomInfoListItem(room_id, room, scrollArea_);