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.
This commit is contained in:
Max Sandholm 2017-10-01 19:49:36 +03:00 committed by mujx
parent ea296321c9
commit 7ad45d8d64
23 changed files with 571 additions and 41 deletions

View file

@ -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()
{

View file

@ -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();

View file

@ -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);

View file

@ -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);

49
src/JoinRoomDialog.cc Normal file
View file

@ -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); });
}

44
src/LeaveRoomDialog.cc Normal file
View file

@ -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); });
}

View file

@ -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<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));
}

View file

@ -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

View file

@ -19,6 +19,7 @@
#include <QJsonArray>
#include <QRegularExpression>
#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<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)
{
@ -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));
@ -132,6 +164,21 @@ RoomList::setInitialRooms(const QMap<QString, QSharedPointer<RoomSettings>> &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<QString, RoomState> &states)
{
@ -139,9 +186,10 @@ RoomList::sync(const QMap<QString, RoomState> &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<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);
}
}

View file

@ -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)
{

View file

@ -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,20 +109,42 @@ 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));
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<TimelineView>(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<TimelineView>(view));
connect(view,
&TimelineView::updateLastTimelineMessage,
this,
&TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack.
addWidget(view);
}
void
TimelineViewManager::sync(const Rooms &rooms)
{

View file

@ -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())
@ -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)
{