From 7ad45d8d6448c60b4c81c80fa8d6d81afd6e4a02 Mon Sep 17 00:00:00 2001 From: Max Sandholm Date: Sun, 1 Oct 2017 19:49:36 +0300 Subject: [PATCH] React to externally left and joined rooms, and add "leave room" button in room menu (#75) * Initial "join room" feature. * React correctly to remotely joined rooms. * Leaving rooms implemented both locally using the room menu in nheko, and reacting properly when leaving a room remotely from another client. --- CMakeLists.txt | 4 ++ include/Cache.h | 2 + include/ChatPage.h | 2 + include/JoinRoomDialog.h | 22 ++++++++++ include/LeaveRoomDialog.h | 19 ++++++++ include/MatrixClient.h | 8 ++++ include/RoomInfoListItem.h | 2 + include/RoomList.h | 20 +++++++++ include/Sync.h | 33 ++++++++++++++ include/TimelineViewManager.h | 4 ++ include/TopRoomBar.h | 12 +++++ src/Cache.cc | 10 +++++ src/ChatPage.cc | 83 ++++++++++++++++++++++++++++++++++- src/EmojiPanel.cc | 13 +++--- src/InputValidator.cc | 4 +- src/JoinRoomDialog.cc | 49 +++++++++++++++++++++ src/LeaveRoomDialog.cc | 44 +++++++++++++++++++ src/MatrixClient.cc | 78 +++++++++++++++++++++++++++++++- src/RoomInfoListItem.cc | 5 ++- src/RoomList.cc | 74 +++++++++++++++++++++++++++++-- src/Sync.cc | 41 ++++++++++++++++- src/TimelineViewManager.cc | 58 ++++++++++++++---------- src/TopRoomBar.cc | 25 +++++++++++ 23 files changed, 571 insertions(+), 41 deletions(-) create mode 100644 include/JoinRoomDialog.h create mode 100644 include/LeaveRoomDialog.h create mode 100644 src/JoinRoomDialog.cc create mode 100644 src/LeaveRoomDialog.cc 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 +#include + +#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 + +#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_; 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 #include #include #include #include +#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 &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> rooms_; QSharedPointer 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 join() const; + inline QMap leave() const; void deserialize(const QJsonValue &data) override; private: QMap join_; + QMap leave_; }; inline QMap @@ -188,6 +215,12 @@ Rooms::join() const return join_; } +inline QMap +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 &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 = "\\1"; @@ -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 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 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 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(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(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 +#include + +#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 +#include + +#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 @@ -462,6 +462,40 @@ MatrixClient::onMessagesResponse(QNetworkReply *reply) emit messagesRetrieved(room_id, msgs); } +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) { @@ -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(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(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 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 #include +#include "MainWindow.h" #include "RoomInfoListItem.h" #include "RoomList.h" #include "Sync.h" @@ -69,6 +70,36 @@ RoomList::clear() rooms_.clear(); } +void +RoomList::addRoom(const QSharedPointer &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(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) { @@ -116,6 +147,7 @@ RoomList::setInitialRooms(const QMap> &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(room_item)); @@ -132,6 +164,21 @@ RoomList::setInitialRooms(const QMap> &set emit roomChanged(rooms_.firstKey()); } +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 &states) { @@ -139,9 +186,10 @@ RoomList::sync(const QMap &states) 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(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(); + } + } } } @@ -184,6 +195,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) { 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(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + addRoom(it.value(), it.key()); } } @@ -121,20 +109,42 @@ void TimelineViewManager::initialize(const QList &rooms) { for (const auto &roomid : rooms) { - // Create a history view without any events. - TimelineView *view = new TimelineView(client_, roomid); - views_.insert(roomid, QSharedPointer(view)); - - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(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(view)); + + connect(view, + &TimelineView::updateLastTimelineMessage, + this, + &TimelineViewManager::updateRoomsLastMessage); + + // 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(view)); + + connect(view, + &TimelineView::updateLastTimelineMessage, + this, + &TimelineViewManager::updateRoomsLastMessage); + + // Add the view in the widget stack. + addWidget(view); +} + void TimelineViewManager::sync(const Rooms &rooms) { 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 #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()) @@ -99,6 +114,16 @@ TopRoomBar::TopRoomBar(QWidget *parent) setLayout(topLayout_); } +void +TopRoomBar::closeLeaveRoomDialog(bool leaving) +{ + leaveRoomModal->fadeOut(); + + if (leaving) { + emit leaveRoom(); + } +} + void TopRoomBar::updateRoomAvatarFromName(const QString &name) {