summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--include/Cache.h2
-rw-r--r--include/ChatPage.h2
-rw-r--r--include/JoinRoomDialog.h22
-rw-r--r--include/LeaveRoomDialog.h19
-rw-r--r--include/MatrixClient.h8
-rw-r--r--include/RoomInfoListItem.h2
-rw-r--r--include/RoomList.h20
-rw-r--r--include/Sync.h33
-rw-r--r--include/TimelineViewManager.h4
-rw-r--r--include/TopRoomBar.h12
-rw-r--r--src/Cache.cc10
-rw-r--r--src/ChatPage.cc83
-rw-r--r--src/EmojiPanel.cc13
-rw-r--r--src/InputValidator.cc4
-rw-r--r--src/JoinRoomDialog.cc49
-rw-r--r--src/LeaveRoomDialog.cc44
-rw-r--r--src/MatrixClient.cc78
-rw-r--r--src/RoomInfoListItem.cc5
-rw-r--r--src/RoomList.cc74
-rw-r--r--src/Sync.cc41
-rw-r--r--src/TimelineViewManager.cc56
-rw-r--r--src/TopRoomBar.cc25
23 files changed, 570 insertions, 40 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 699623bd..08d3cf68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,6 +123,8 @@ set(SRC_FILES
     src/TimelineView.cc
     src/TimelineViewManager.cc
     src/InputValidator.cc
+    src/JoinRoomDialog.cc
+    src/LeaveRoomDialog.cc
     src/Login.cc
     src/LoginPage.cc
     src/LogoutDialog.cc
@@ -203,9 +205,11 @@ qt5_wrap_cpp(MOC_HEADERS
     include/EmojiPickButton.h
     include/ImageItem.h
     include/ImageOverlayDialog.h
+    include/JoinRoomDialog.h
     include/TimelineItem.h
     include/TimelineView.h
     include/TimelineViewManager.h
+    include/LeaveRoomDialog.h
     include/LoginPage.h
     include/LogoutDialog.h
     include/MainWindow.h
diff --git a/include/Cache.h b/include/Cache.h
index 1be56620..0f6e5cd2 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -37,6 +37,8 @@ public:
         inline void unmount();
         inline QString memberDbName(const QString &roomid);
 
+        void removeRoom(const QString &roomid);
+
 private:
         void setNextBatchToken(lmdb::txn &txn, const QString &token);
         void insertRoomState(lmdb::txn &txn, const QString &roomid, const RoomState &state);
diff --git a/include/ChatPage.h b/include/ChatPage.h
index 0ea7ea38..8becc17f 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -61,6 +61,8 @@ private slots:
         void changeTopRoomInfo(const QString &room_id);
         void startSync();
         void logout();
+        void addRoom(const QString &room_id);
+        void removeRoom(const QString &room_id);
 
 protected:
         void keyPressEvent(QKeyEvent *event) override;
diff --git a/include/JoinRoomDialog.h b/include/JoinRoomDialog.h
new file mode 100644
index 00000000..6c3fbdcf
--- /dev/null
+++ b/include/JoinRoomDialog.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <QFrame>
+#include <QLineEdit>
+
+#include "FlatButton.h"
+
+class JoinRoomDialog : public QFrame
+{
+        Q_OBJECT
+public:
+        JoinRoomDialog(QWidget *parent = nullptr);
+
+signals:
+        void closing(bool isJoining, QString roomAlias);
+
+private:
+        FlatButton *confirmBtn_;
+        FlatButton *cancelBtn_;
+
+        QLineEdit *roomAliasEdit_;
+};
diff --git a/include/LeaveRoomDialog.h b/include/LeaveRoomDialog.h
new file mode 100644
index 00000000..1639a578
--- /dev/null
+++ b/include/LeaveRoomDialog.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <QFrame>
+
+#include "FlatButton.h"
+
+class LeaveRoomDialog : public QFrame
+{
+        Q_OBJECT
+public:
+        explicit LeaveRoomDialog(QWidget *parent = nullptr);
+
+signals:
+        void closing(bool isLeaving);
+
+private:
+        FlatButton *confirmBtn_;
+        FlatButton *cancelBtn_;
+};
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 8d6c60a7..cd023650 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -52,6 +52,8 @@ public:
         void downloadImage(const QString &event_id, const QUrl &url);
         void messages(const QString &room_id, const QString &from_token, int limit = 20) noexcept;
         void uploadImage(const QString &roomid, const QString &filename);
+        void joinRoom(const QString &roomIdOrAlias);
+        void leaveRoom(const QString &roomId);
 
         inline QUrl getHomeServer();
         inline int transactionId();
@@ -94,6 +96,8 @@ signals:
         void messageSent(const QString &event_id, const QString &roomid, const int txn_id);
         void emoteSent(const QString &event_id, const QString &roomid, const int txn_id);
         void messagesRetrieved(const QString &room_id, const RoomMessages &msgs);
+        void joinedRoom(const QString &room_id);
+        void leftRoom(const QString &room_id);
 
 private slots:
         void onResponse(QNetworkReply *reply);
@@ -115,6 +119,8 @@ private:
                 Sync,
                 UserAvatar,
                 Versions,
+                JoinRoom,
+                LeaveRoom,
         };
 
         // Response handlers.
@@ -132,6 +138,8 @@ private:
         void onSyncResponse(QNetworkReply *reply);
         void onUserAvatarResponse(QNetworkReply *reply);
         void onVersionsResponse(QNetworkReply *reply);
+        void onJoinRoomResponse(QNetworkReply *reply);
+        void onLeaveRoomResponse(QNetworkReply *reply);
 
         // Client API prefix.
         QString clientApiUrl_;
diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h
index 8947ee8e..03023038 100644
--- a/include/RoomInfoListItem.h
+++ b/include/RoomInfoListItem.h
@@ -57,6 +57,7 @@ public:
 
 signals:
         void clicked(const QString &room_id);
+        void leaveRoom(const QString &room_id);
 
 public slots:
         void setPressedState(bool state);
@@ -86,6 +87,7 @@ private:
 
         Menu *menu_;
         QAction *toggleNotifications_;
+        QAction *leaveRoom_;
 
         QSharedPointer<RoomSettings> roomSettings_;
 
diff --git a/include/RoomList.h b/include/RoomList.h
index 573f43a8..c2f4255d 100644
--- a/include/RoomList.h
+++ b/include/RoomList.h
@@ -17,12 +17,16 @@
 
 #pragma once
 
+#include <QPushButton>
 #include <QScrollArea>
 #include <QSharedPointer>
 #include <QVBoxLayout>
 #include <QWidget>
 
+#include "JoinRoomDialog.h"
+#include "LeaveRoomDialog.h"
 #include "MatrixClient.h"
+#include "OverlayModal.h"
 #include "RoomInfoListItem.h"
 #include "RoomState.h"
 #include "Sync.h"
@@ -41,6 +45,11 @@ public:
 
         void clear();
 
+        void addRoom(const QSharedPointer<RoomSettings> &settings,
+                     const RoomState &state,
+                     const QString &room_id);
+        void removeRoom(const QString &room_id, bool reset);
+
 signals:
         void roomChanged(const QString &room_id);
         void totalUnreadMessageCountUpdated(int count);
@@ -50,6 +59,9 @@ public slots:
         void highlightSelectedRoom(const QString &room_id);
         void updateUnreadMessageCount(const QString &roomid, int count);
         void updateRoomDescription(const QString &roomid, const DescInfo &info);
+        void closeJoinRoomDialog(bool isJoining, QString roomAlias);
+        void openLeaveRoomDialog(const QString &room_id);
+        void closeLeaveRoomDialog(bool leaving, const QString &room_id);
 
 private:
         void calculateUnreadMessageCount();
@@ -59,6 +71,14 @@ private:
         QScrollArea *scrollArea_;
         QWidget *scrollAreaContents_;
 
+        QPushButton *joinRoomButton_;
+
+        OverlayModal *joinRoomModal_;
+        JoinRoomDialog *joinRoomDialog_;
+
+        OverlayModal *leaveRoomModal;
+        LeaveRoomDialog *leaveRoomDialog_;
+
         QMap<QString, QSharedPointer<RoomInfoListItem>> rooms_;
 
         QSharedPointer<MatrixClient> client_;
diff --git a/include/Sync.h b/include/Sync.h
index 420c9b6c..a9caf473 100644
--- a/include/Sync.h
+++ b/include/Sync.h
@@ -171,15 +171,42 @@ JoinedRoom::timeline() const
         return timeline_;
 }
 
+class LeftRoom : public Deserializable
+{
+public:
+        inline State state() const;
+        inline Timeline timeline() const;
+
+        void deserialize(const QJsonValue &data) override;
+
+private:
+        State state_;
+        Timeline timeline_;
+};
+
+inline State
+LeftRoom::state() const
+{
+        return state_;
+}
+
+inline Timeline
+LeftRoom::timeline() const
+{
+        return timeline_;
+}
+
 // TODO: Add support for invited and left rooms.
 class Rooms : public Deserializable
 {
 public:
         inline QMap<QString, JoinedRoom> join() const;
+        inline QMap<QString, LeftRoom> leave() const;
         void deserialize(const QJsonValue &data) override;
 
 private:
         QMap<QString, JoinedRoom> join_;
+        QMap<QString, LeftRoom> leave_;
 };
 
 inline QMap<QString, JoinedRoom>
@@ -188,6 +215,12 @@ Rooms::join() const
         return join_;
 }
 
+inline QMap<QString, LeftRoom>
+Rooms::leave() const
+{
+        return leave_;
+}
+
 class SyncResponse : public Deserializable
 {
 public:
diff --git a/include/TimelineViewManager.h b/include/TimelineViewManager.h
index 14f47928..35dcac5a 100644
--- a/include/TimelineViewManager.h
+++ b/include/TimelineViewManager.h
@@ -40,6 +40,10 @@ public:
         void initialize(const Rooms &rooms);
         // Empty initialization.
         void initialize(const QList<QString> &rooms);
+
+        void addRoom(const JoinedRoom &room, const QString &room_id);
+        void addRoom(const QString &room_id);
+
         void sync(const Rooms &rooms);
         void clearAll();
 
diff --git a/include/TopRoomBar.h b/include/TopRoomBar.h
index 6b24cbef..5d8b394e 100644
--- a/include/TopRoomBar.h
+++ b/include/TopRoomBar.h
@@ -29,7 +29,9 @@
 
 #include "Avatar.h"
 #include "FlatButton.h"
+#include "LeaveRoomDialog.h"
 #include "Menu.h"
+#include "OverlayModal.h"
 #include "RoomSettings.h"
 
 static const QString URL_HTML = "<a href=\"\\1\" style=\"color: #333333\">\\1</a>";
@@ -51,9 +53,15 @@ public:
 
         void reset();
 
+signals:
+        void leaveRoom();
+
 protected:
         void paintEvent(QPaintEvent *event) override;
 
+private slots:
+        void closeLeaveRoomDialog(bool leaving);
+
 private:
         QHBoxLayout *topLayout_;
         QVBoxLayout *textLayout_;
@@ -65,9 +73,13 @@ private:
 
         QMenu *menu_;
         QAction *toggleNotifications_;
+        QAction *leaveRoom_;
 
         FlatButton *settingsBtn_;
 
+        OverlayModal *leaveRoomModal;
+        LeaveRoomDialog *leaveRoomDialog_;
+
         Avatar *avatar_;
 
         int buttonSize_;
diff --git a/src/Cache.cc b/src/Cache.cc
index bda81316..5ed77086 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -153,6 +153,16 @@ Cache::insertRoomState(lmdb::txn &txn, const QString &roomid, const RoomState &s
         }
 }
 
+void
+Cache::removeRoom(const QString &roomid)
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, 0);
+
+        lmdb::dbi_del(txn, roomDb_, lmdb::val(roomid.toUtf8(), roomid.toUtf8().size()), nullptr);
+
+        txn.commit();
+}
+
 QMap<QString, RoomState>
 Cache::states()
 {
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 3d3a3876..a6a80e9d 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -114,6 +114,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
         connect(user_info_widget_, SIGNAL(logout()), client_.data(), SLOT(logout()));
         connect(client_.data(), SIGNAL(loggedOut()), this, SLOT(logout()));
 
+        connect(
+          top_bar_, &TopRoomBar::leaveRoom, this, [=]() { client_->leaveRoom(current_room_); });
+
         connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
         connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit);
         connect(
@@ -190,6 +193,14 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
                 SIGNAL(ownAvatarRetrieved(const QPixmap &)),
                 this,
                 SLOT(setOwnAvatar(const QPixmap &)));
+        connect(client_.data(),
+                SIGNAL(joinedRoom(const QString &)),
+                this,
+                SLOT(addRoom(const QString &)));
+        connect(client_.data(),
+                SIGNAL(leftRoom(const QString &)),
+                this,
+                SLOT(removeRoom(const QString &)));
 
         AvatarProvider::init(client);
 }
@@ -293,8 +304,9 @@ ChatPage::syncCompleted(const SyncResponse &response)
                 RoomState room_state;
 
                 // Merge the new updates for rooms that we are tracking.
-                if (state_manager_.contains(it.key()))
+                if (state_manager_.contains(it.key())) {
                         room_state = state_manager_[it.key()];
+                }
 
                 room_state.updateFromEvents(it.value().state().events());
                 room_state.updateFromEvents(it.value().timeline().events());
@@ -307,13 +319,48 @@ ChatPage::syncCompleted(const SyncResponse &response)
                         oldState.update(room_state);
                         state_manager_.insert(it.key(), oldState);
                 } else {
-                        qWarning() << "New rooms cannot be added after initial sync, yet.";
+                        RoomState room_state;
+
+                        // Build the current state from the timeline and state events.
+                        room_state.updateFromEvents(it.value().state().events());
+                        room_state.updateFromEvents(it.value().timeline().events());
+
+                        // Remove redundant memberships.
+                        room_state.removeLeaveMemberships();
+
+                        // Resolve room name and avatar. e.g in case of one-to-one chats.
+                        room_state.resolveName();
+                        room_state.resolveAvatar();
+
+                        updateDisplayNames(room_state);
+
+                        state_manager_.insert(it.key(), room_state);
+                        settingsManager_.insert(
+                          it.key(), QSharedPointer<RoomSettings>(new RoomSettings(it.key())));
+
+                        for (const auto membership : room_state.memberships) {
+                                auto uid = membership.sender();
+                                auto url = membership.content().avatarUrl();
+
+                                if (!url.toString().isEmpty())
+                                        AvatarProvider::setAvatarUrl(uid, url);
+                        }
+
+                        view_manager_->addRoom(it.value(), it.key());
                 }
 
                 if (it.key() == current_room_)
                         changeTopRoomInfo(it.key());
         }
 
+        auto leave = response.rooms().leave();
+
+        for (auto it = leave.constBegin(); it != leave.constEnd(); it++) {
+                if (state_manager_.contains(it.key())) {
+                        removeRoom(it.key());
+                }
+        }
+
         try {
                 cache_->setState(response.nextBatch(), state_manager_);
         } catch (const lmdb::error &e) {
@@ -537,6 +584,38 @@ ChatPage::showQuickSwitcher()
         quickSwitcherModal_->fadeIn();
 }
 
+void
+ChatPage::addRoom(const QString &room_id)
+{
+        if (!state_manager_.contains(room_id)) {
+                RoomState room_state;
+
+                state_manager_.insert(room_id, room_state);
+                settingsManager_.insert(room_id,
+                                        QSharedPointer<RoomSettings>(new RoomSettings(room_id)));
+
+                room_list_->addRoom(settingsManager_[room_id], state_manager_[room_id], room_id);
+
+                this->changeTopRoomInfo(room_id);
+                room_list_->highlightSelectedRoom(room_id);
+        }
+}
+
+void
+ChatPage::removeRoom(const QString &room_id)
+{
+        state_manager_.remove(room_id);
+        settingsManager_.remove(room_id);
+        try {
+                cache_->removeRoom(room_id);
+        } catch (const lmdb::error &e) {
+                qCritical() << "The cache couldn't be updated: " << e.what();
+                // TODO: Notify the user.
+                cache_->unmount();
+        }
+        room_list_->removeRoom(room_id, room_id == current_room_);
+}
+
 ChatPage::~ChatPage()
 {
         sync_timer_->stop();
diff --git a/src/EmojiPanel.cc b/src/EmojiPanel.cc
index c272a478..2730ddb5 100644
--- a/src/EmojiPanel.cc
+++ b/src/EmojiPanel.cc
@@ -34,12 +34,13 @@ EmojiPanel::EmojiPanel(QWidget *parent)
   , animationDuration_{ 100 }
   , categoryIconSize_{ 20 }
 {
-        setStyleSheet(
-          "QWidget {background: #fff; color: #e8e8e8; border: none;}"
-          "QScrollBar:vertical { background-color: #fff; width: 8px; margin: 0px 2px 0 2px; }"
-          "QScrollBar::handle:vertical { background-color: #d6dde3; min-height: 20px; }"
-          "QScrollBar::add-line:vertical { border: none; background: none; }"
-          "QScrollBar::sub-line:vertical { border: none; background: none; }");
+        setStyleSheet("QWidget {background: #fff; color: #e8e8e8; border: none;}"
+                      "QScrollBar:vertical { background-color: #fff; width: 8px; margin: 0px "
+                      "2px 0 2px; }"
+                      "QScrollBar::handle:vertical { background-color: #d6dde3; min-height: "
+                      "20px; }"
+                      "QScrollBar::add-line:vertical { border: none; background: none; }"
+                      "QScrollBar::sub-line:vertical { border: none; background: none; }");
 
         setAttribute(Qt::WA_TranslucentBackground, true);
         setAttribute(Qt::WA_ShowWithoutActivating, true);
diff --git a/src/InputValidator.cc b/src/InputValidator.cc
index 6e343c71..5fd92783 100644
--- a/src/InputValidator.cc
+++ b/src/InputValidator.cc
@@ -20,8 +20,8 @@
 const QRegExp MXID_REGEX("@[A-Za-z0-9._%+-]+:[A-Za-z0-9.-]{1,126}\\.[A-Za-z]{1,63}");
 const QRegExp LOCALPART_REGEX("[A-za-z0-9._%+-]{3,}");
 const QRegExp PASSWORD_REGEX(".{8,}");
-const QRegExp DOMAIN_REGEX(
-  "(?!\\-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,126}(?!\\d+)[a-zA-Z\\d]{1,63}");
+const QRegExp DOMAIN_REGEX("(?!\\-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,"
+                           "126}(?!\\d+)[a-zA-Z\\d]{1,63}");
 
 QRegExpValidator InputValidator::Id(MXID_REGEX);
 QRegExpValidator InputValidator::Localpart(LOCALPART_REGEX);
diff --git a/src/JoinRoomDialog.cc b/src/JoinRoomDialog.cc
new file mode 100644
index 00000000..c3ee289e
--- /dev/null
+++ b/src/JoinRoomDialog.cc
@@ -0,0 +1,49 @@
+#include <QLabel>
+#include <QVBoxLayout>
+
+#include "Config.h"
+#include "JoinRoomDialog.h"
+#include "Theme.h"
+
+JoinRoomDialog::JoinRoomDialog(QWidget *parent)
+  : QFrame(parent)
+{
+        setMaximumSize(400, 400);
+        setStyleSheet("background-color: #fff");
+
+        auto layout = new QVBoxLayout(this);
+        layout->setSpacing(30);
+        layout->setMargin(20);
+
+        auto buttonLayout = new QHBoxLayout();
+        buttonLayout->setSpacing(0);
+        buttonLayout->setMargin(0);
+
+        confirmBtn_ = new FlatButton("JOIN", this);
+        confirmBtn_->setFontSize(conf::btn::fontSize);
+
+        cancelBtn_ = new FlatButton(tr("CANCEL"), this);
+        cancelBtn_->setFontSize(conf::btn::fontSize);
+
+        buttonLayout->addStretch(1);
+        buttonLayout->addWidget(confirmBtn_);
+        buttonLayout->addWidget(cancelBtn_);
+
+        QFont font;
+        font.setPixelSize(conf::headerFontSize);
+
+        auto label = new QLabel(tr("Room alias to join:"), this);
+        label->setFont(font);
+        label->setStyleSheet("color: #333333");
+
+        roomAliasEdit_ = new QLineEdit(this);
+
+        layout->addWidget(label);
+        layout->addWidget(roomAliasEdit_);
+        layout->addLayout(buttonLayout);
+
+        connect(confirmBtn_, &QPushButton::clicked, [=]() {
+                emit closing(true, roomAliasEdit_->text());
+        });
+        connect(cancelBtn_, &QPushButton::clicked, [=]() { emit closing(false, nullptr); });
+}
diff --git a/src/LeaveRoomDialog.cc b/src/LeaveRoomDialog.cc
new file mode 100644
index 00000000..f7669f0d
--- /dev/null
+++ b/src/LeaveRoomDialog.cc
@@ -0,0 +1,44 @@
+#include <QLabel>
+#include <QVBoxLayout>
+
+#include "Config.h"
+#include "LeaveRoomDialog.h"
+#include "Theme.h"
+
+LeaveRoomDialog::LeaveRoomDialog(QWidget *parent)
+  : QFrame(parent)
+{
+        setMaximumSize(400, 400);
+        setStyleSheet("background-color: #fff");
+
+        auto layout = new QVBoxLayout(this);
+        layout->setSpacing(30);
+        layout->setMargin(20);
+
+        auto buttonLayout = new QHBoxLayout();
+        buttonLayout->setSpacing(0);
+        buttonLayout->setMargin(0);
+
+        confirmBtn_ = new FlatButton("LEAVE", this);
+        confirmBtn_->setFontSize(conf::btn::fontSize);
+
+        cancelBtn_ = new FlatButton(tr("CANCEL"), this);
+        cancelBtn_->setFontSize(conf::btn::fontSize);
+
+        buttonLayout->addStretch(1);
+        buttonLayout->addWidget(confirmBtn_);
+        buttonLayout->addWidget(cancelBtn_);
+
+        QFont font;
+        font.setPixelSize(conf::headerFontSize);
+
+        auto label = new QLabel(tr("Are you sure you want to leave?"), this);
+        label->setFont(font);
+        label->setStyleSheet("color: #333333");
+
+        layout->addWidget(label);
+        layout->addLayout(buttonLayout);
+
+        connect(confirmBtn_, &QPushButton::clicked, [=]() { emit closing(true); });
+        connect(cancelBtn_, &QPushButton::clicked, [=]() { emit closing(false); });
+}
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 981a30c2..bd43efd8 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -463,6 +463,40 @@ MatrixClient::onMessagesResponse(QNetworkReply *reply)
 }
 
 void
+MatrixClient::onJoinRoomResponse(QNetworkReply *reply)
+{
+        reply->deleteLater();
+
+        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+        if (status == 0 || status >= 400) {
+                qWarning() << reply->errorString();
+                return;
+        }
+
+        auto data              = reply->readAll();
+        QJsonDocument response = QJsonDocument::fromJson(data);
+        QString room_id        = response.object()["room_id"].toString();
+        emit joinedRoom(room_id);
+}
+
+void
+MatrixClient::onLeaveRoomResponse(QNetworkReply *reply)
+{
+        reply->deleteLater();
+
+        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+        if (status == 0 || status >= 400) {
+                qWarning() << reply->errorString();
+                return;
+        }
+
+        QString room_id = reply->property("room_id").toString();
+        emit leftRoom(room_id);
+}
+
+void
 MatrixClient::onResponse(QNetworkReply *reply)
 {
         switch (static_cast<Endpoint>(reply->property("endpoint").toInt())) {
@@ -508,6 +542,12 @@ MatrixClient::onResponse(QNetworkReply *reply)
         case Endpoint::Messages:
                 onMessagesResponse(reply);
                 break;
+        case Endpoint::JoinRoom:
+                onJoinRoomResponse(reply);
+                break;
+        case Endpoint::LeaveRoom:
+                onLeaveRoomResponse(reply);
+                break;
         default:
                 break;
         }
@@ -571,7 +611,8 @@ void
 MatrixClient::sync() noexcept
 {
         QJsonObject filter{ { "room",
-                              QJsonObject{ { "ephemeral", QJsonObject{ { "limit", 0 } } } } },
+                              QJsonObject{ { "include_leave", true },
+                                           { "ephemeral", QJsonObject{ { "limit", 0 } } } } },
                             { "presence", QJsonObject{ { "limit", 0 } } } };
 
         QUrlQuery query;
@@ -842,3 +883,38 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename)
         reply->setProperty("room_id", roomid);
         reply->setProperty("filename", filename);
 }
+
+void
+MatrixClient::joinRoom(const QString &roomIdOrAlias)
+{
+        QUrlQuery query;
+        query.addQueryItem("access_token", token_);
+
+        QUrl endpoint(server_);
+        endpoint.setPath(clientApiUrl_ + QString("/join/%1").arg(roomIdOrAlias));
+        endpoint.setQuery(query);
+
+        QNetworkRequest request(endpoint);
+        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+
+        QNetworkReply *reply = post(request, "{}");
+        reply->setProperty("endpoint", static_cast<int>(Endpoint::JoinRoom));
+}
+
+void
+MatrixClient::leaveRoom(const QString &roomId)
+{
+        QUrlQuery query;
+        query.addQueryItem("access_token", token_);
+
+        QUrl endpoint(server_);
+        endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/leave").arg(roomId));
+        endpoint.setQuery(query);
+
+        QNetworkRequest request(endpoint);
+        request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+
+        QNetworkReply *reply = post(request, "{}");
+        reply->setProperty("room_id", roomId);
+        reply->setProperty("endpoint", static_cast<int>(Endpoint::LeaveRoom));
+}
diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
index 533fc267..cd15d839 100644
--- a/src/RoomInfoListItem.cc
+++ b/src/RoomInfoListItem.cc
@@ -53,12 +53,15 @@ RoomInfoListItem::RoomInfoListItem(QSharedPointer<RoomSettings> settings,
         menu_ = new Menu(this);
 
         toggleNotifications_ = new QAction(notificationText(), this);
-
         connect(toggleNotifications_, &QAction::triggered, this, [=]() {
                 roomSettings_->toggleNotifications();
         });
 
+        leaveRoom_ = new QAction(tr("Leave room"), this);
+        connect(leaveRoom_, &QAction::triggered, this, [=]() { emit leaveRoom(room_id); });
+
         menu_->addAction(toggleNotifications_);
+        menu_->addAction(leaveRoom_);
 }
 
 QString
diff --git a/src/RoomList.cc b/src/RoomList.cc
index 5c67d98f..9dc7e1c2 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -19,6 +19,7 @@
 #include <QJsonArray>
 #include <QRegularExpression>
 
+#include "MainWindow.h"
 #include "RoomInfoListItem.h"
 #include "RoomList.h"
 #include "Sync.h"
@@ -70,6 +71,36 @@ RoomList::clear()
 }
 
 void
+RoomList::addRoom(const QSharedPointer<RoomSettings> &settings,
+                  const RoomState &state,
+                  const QString &room_id)
+{
+        RoomInfoListItem *room_item = new RoomInfoListItem(settings, state, room_id, scrollArea_);
+        connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom);
+        connect(room_item, &RoomInfoListItem::leaveRoom, this, &RoomList::openLeaveRoomDialog);
+
+        rooms_.insert(room_id, QSharedPointer<RoomInfoListItem>(room_item));
+
+        client_->fetchRoomAvatar(room_id, state.getAvatar());
+
+        contentsLayout_->insertWidget(0, room_item);
+}
+
+void
+RoomList::removeRoom(const QString &room_id, bool reset)
+{
+        rooms_.remove(room_id);
+
+        if (rooms_.isEmpty() || !reset)
+                return;
+
+        auto first_room = rooms_.first();
+        first_room->setPressedState(true);
+
+        emit roomChanged(rooms_.firstKey());
+}
+
+void
 RoomList::updateUnreadMessageCount(const QString &roomid, int count)
 {
         if (!rooms_.contains(roomid)) {
@@ -116,6 +147,7 @@ RoomList::setInitialRooms(const QMap<QString, QSharedPointer<RoomSettings>> &set
                   new RoomInfoListItem(settings[room_id], state, room_id, scrollArea_);
                 connect(
                   room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom);
+                connect(room_item, &RoomInfoListItem::leaveRoom, this, &RoomList::openLeaveRoomDialog);
 
                 rooms_.insert(room_id, QSharedPointer<RoomInfoListItem>(room_item));
 
@@ -133,15 +165,31 @@ RoomList::setInitialRooms(const QMap<QString, QSharedPointer<RoomSettings>> &set
 }
 
 void
+RoomList::openLeaveRoomDialog(const QString &room_id)
+{
+        leaveRoomDialog_ = new LeaveRoomDialog(this);
+        connect(leaveRoomDialog_,
+                &LeaveRoomDialog::closing, this,
+                [=](bool leaving) { closeLeaveRoomDialog(leaving, room_id); });
+
+        leaveRoomModal = new OverlayModal(MainWindow::instance(), leaveRoomDialog_);
+        leaveRoomModal->setDuration(0);
+        leaveRoomModal->setColor(QColor(55, 55, 55, 170));
+
+        leaveRoomModal->fadeIn();
+}
+
+void
 RoomList::sync(const QMap<QString, RoomState> &states)
 {
         for (auto it = states.constBegin(); it != states.constEnd(); it++) {
                 auto room_id = it.key();
                 auto state   = it.value();
 
-                // TODO: Add the new room to the list.
-                if (!rooms_.contains(room_id))
-                        continue;
+                if (!rooms_.contains(room_id)) {
+                        addRoom(
+                          QSharedPointer<RoomSettings>(new RoomSettings(room_id)), state, room_id);
+                }
 
                 auto room = rooms_[room_id];
 
@@ -203,3 +251,23 @@ RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
 
         rooms_.value(roomid)->setDescriptionMessage(info);
 }
+
+void
+RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias)
+{
+        joinRoomModal_->fadeOut();
+
+        if (isJoining) {
+                client_->joinRoom(roomAlias);
+        }
+}
+
+void
+RoomList::closeLeaveRoomDialog(bool leaving, const QString &room_id)
+{
+        leaveRoomModal->fadeOut();
+
+        if (leaving) {
+                client_->leaveRoom(room_id);
+        }
+}
diff --git a/src/Sync.cc b/src/Sync.cc
index 58c423d1..90314352 100644
--- a/src/Sync.cc
+++ b/src/Sync.cc
@@ -90,7 +90,6 @@ Rooms::deserialize(const QJsonValue &data)
 
                 for (auto it = join.constBegin(); it != join.constEnd(); it++) {
                         JoinedRoom tmp_room;
-
                         try {
                                 tmp_room.deserialize(it.value());
                                 join_.insert(it.key(), tmp_room);
@@ -112,7 +111,19 @@ Rooms::deserialize(const QJsonValue &data)
                 if (!object.value("leave").isObject()) {
                         throw DeserializationException("rooms/leave must be a JSON object");
                 }
-                // TODO: Implement leave handling
+                auto leave = object.value("leave").toObject();
+
+                for (auto it = leave.constBegin(); it != leave.constEnd(); it++) {
+                        LeftRoom tmp_room;
+
+                        try {
+                                tmp_room.deserialize(it.value());
+                                leave_.insert(it.key(), tmp_room);
+                        } catch (DeserializationException &e) {
+                                qWarning() << e.what();
+                                qWarning() << "Skipping malformed object for room" << it.key();
+                        }
+                }
         }
 }
 
@@ -185,6 +196,32 @@ JoinedRoom::deserialize(const QJsonValue &data)
 }
 
 void
+LeftRoom::deserialize(const QJsonValue &data)
+{
+        if (!data.isObject())
+                throw DeserializationException("LeftRoom is not a JSON object");
+
+        QJsonObject object = data.toObject();
+
+        if (!object.contains("state"))
+                throw DeserializationException("leave/state is missing");
+
+        if (!object.contains("timeline"))
+                throw DeserializationException("leave/timeline is missing");
+
+        if (!object.value("state").isObject())
+                throw DeserializationException("leave/state should be an object");
+
+        QJsonObject state = object.value("state").toObject();
+
+        if (!state.contains("events"))
+                throw DeserializationException("leave/state/events is missing");
+
+        state_.deserialize(state.value("events"));
+        timeline_.deserialize(object.value("timeline"));
+}
+
+void
 Event::deserialize(const QJsonValue &data)
 {
         if (!data.isObject())
diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index a4d616c3..1969ae5b 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -101,19 +101,7 @@ void
 TimelineViewManager::initialize(const Rooms &rooms)
 {
         for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
-                auto roomid = it.key();
-
-                // Create a history view with the room events.
-                TimelineView *view = new TimelineView(it.value().timeline(), client_, it.key());
-                views_.insert(it.key(), QSharedPointer<TimelineView>(view));
-
-                connect(view,
-                        &TimelineView::updateLastTimelineMessage,
-                        this,
-                        &TimelineViewManager::updateRoomsLastMessage);
-
-                // Add the view in the widget stack.
-                addWidget(view);
+                addRoom(it.value(), it.key());
         }
 }
 
@@ -121,18 +109,40 @@ void
 TimelineViewManager::initialize(const QList<QString> &rooms)
 {
         for (const auto &roomid : rooms) {
-                // Create a history view without any events.
-                TimelineView *view = new TimelineView(client_, roomid);
-                views_.insert(roomid, QSharedPointer<TimelineView>(view));
+                addRoom(roomid);
+        }
+}
+
+void
+TimelineViewManager::addRoom(const JoinedRoom &room, const QString &room_id)
+{
+        // Create a history view with the room events.
+        TimelineView *view = new TimelineView(room.timeline(), client_, room_id);
+        views_.insert(room_id, QSharedPointer<TimelineView>(view));
 
-                connect(view,
-                        &TimelineView::updateLastTimelineMessage,
-                        this,
-                        &TimelineViewManager::updateRoomsLastMessage);
+        connect(view,
+                &TimelineView::updateLastTimelineMessage,
+                this,
+                &TimelineViewManager::updateRoomsLastMessage);
 
-                // Add the view in the widget stack.
-                addWidget(view);
-        }
+        // Add the view in the widget stack.
+        addWidget(view);
+}
+
+void
+TimelineViewManager::addRoom(const QString &room_id)
+{
+        // Create a history view without any events.
+        TimelineView *view = new TimelineView(client_, room_id);
+        views_.insert(room_id, QSharedPointer<TimelineView>(view));
+
+        connect(view,
+                &TimelineView::updateLastTimelineMessage,
+                this,
+                &TimelineViewManager::updateRoomsLastMessage);
+
+        // Add the view in the widget stack.
+        addWidget(view);
 }
 
 void
diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc
index 1805f063..f8a7e600 100644
--- a/src/TopRoomBar.cc
+++ b/src/TopRoomBar.cc
@@ -18,6 +18,7 @@
 #include <QStyleOption>
 
 #include "Config.h"
+#include "MainWindow.h"
 #include "TopRoomBar.h"
 
 TopRoomBar::TopRoomBar(QWidget *parent)
@@ -83,7 +84,21 @@ TopRoomBar::TopRoomBar(QWidget *parent)
                 roomSettings_->toggleNotifications();
         });
 
+        leaveRoom_ = new QAction(tr("Leave room"), this);
+        connect(leaveRoom_, &QAction::triggered, this, [=]() {
+                leaveRoomDialog_ = new LeaveRoomDialog(this);
+                connect(
+                  leaveRoomDialog_, SIGNAL(closing(bool)), this, SLOT(closeLeaveRoomDialog(bool)));
+
+                leaveRoomModal = new OverlayModal(MainWindow::instance(), leaveRoomDialog_);
+                leaveRoomModal->setDuration(100);
+                leaveRoomModal->setColor(QColor(55, 55, 55, 170));
+
+                leaveRoomModal->fadeIn();
+        });
+
         menu_->addAction(toggleNotifications_);
+        menu_->addAction(leaveRoom_);
 
         connect(settingsBtn_, &QPushButton::clicked, this, [=]() {
                 if (roomSettings_->isNotificationsEnabled())
@@ -100,6 +115,16 @@ TopRoomBar::TopRoomBar(QWidget *parent)
 }
 
 void
+TopRoomBar::closeLeaveRoomDialog(bool leaving)
+{
+        leaveRoomModal->fadeOut();
+
+        if (leaving) {
+                emit leaveRoom();
+        }
+}
+
+void
 TopRoomBar::updateRoomAvatarFromName(const QString &name)
 {
         QChar letter = '?';