From 340c9ab9dea64c2622104ffd28aefaf67e14c8fb Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Wed, 16 May 2018 20:40:42 +0300 Subject: [PATCH] Add menu to modify the name & topic of the room fixes #235 --- include/Config.h | 78 +++++----- include/MatrixClient.h | 63 ++++++++ include/dialogs/RoomSettings.hpp | 35 ++++- resources/icons/ui/edit.svg | 1 + resources/res.qrc | 1 + resources/styles/nheko-dark.qss | 1 + resources/styles/nheko.qss | 1 + src/dialogs/RoomSettings.cpp | 239 +++++++++++++++++++++++++------ 8 files changed, 341 insertions(+), 78 deletions(-) create mode 100644 resources/icons/ui/edit.svg diff --git a/include/Config.h b/include/Config.h index 6e086f54..9cce1929 100644 --- a/include/Config.h +++ b/include/Config.h @@ -9,91 +9,95 @@ namespace conf { // Global settings. -static constexpr int fontSize = 14; -static constexpr int textInputFontSize = 14; -static constexpr int emojiSize = 14; -static constexpr int headerFontSize = 21; -static constexpr int typingNotificationFontSize = 11; +constexpr int fontSize = 14; +constexpr int textInputFontSize = 14; +constexpr int emojiSize = 14; +constexpr int headerFontSize = 21; +constexpr int typingNotificationFontSize = 11; namespace popup { -static constexpr int font = fontSize; -static constexpr int avatar = 28; +constexpr int font = fontSize; +constexpr int avatar = 28; +} + +namespace modals { +constexpr int errorFont = conf::fontSize - 2; } namespace receipts { -static constexpr int font = 12; +constexpr int font = 12; } namespace dialogs { -static constexpr int labelSize = 15; +constexpr int labelSize = 15; } namespace strings { -static const QString url_html = "\\1"; -static const QRegExp url_regex( +const QString url_html = "\\1"; +const QRegExp url_regex( "((www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:])"); } // Window geometry. namespace window { -static constexpr int height = 600; -static constexpr int width = 1066; +constexpr int height = 600; +constexpr int width = 1066; -static constexpr int minHeight = height; -static constexpr int minWidth = 950; +constexpr int minHeight = height; +constexpr int minWidth = 950; } // namespace window namespace textInput { -static constexpr int height = 50; +constexpr int height = 50; } namespace sidebarActions { -static constexpr int height = textInput::height; -static constexpr int iconSize = 28; +constexpr int height = textInput::height; +constexpr int iconSize = 28; } // Button settings. namespace btn { -static constexpr int fontSize = 20; -static constexpr int cornerRadius = 3; +constexpr int fontSize = 20; +constexpr int cornerRadius = 3; } // namespace btn // RoomList specific. namespace roomlist { namespace fonts { -static constexpr int heading = 13; -static constexpr int timestamp = heading; -static constexpr int badge = 10; -static constexpr int bubble = 20; -static constexpr int communityBubble = bubble - 4; +constexpr int heading = 13; +constexpr int timestamp = heading; +constexpr int badge = 10; +constexpr int bubble = 20; +constexpr int communityBubble = bubble - 4; } // namespace fonts } // namespace roomlist namespace userInfoWidget { namespace fonts { -static constexpr int displayName = 16; -static constexpr int userid = 14; +constexpr int displayName = 16; +constexpr int userid = 14; } // namespace fonts } // namespace userInfoWidget namespace topRoomBar { namespace fonts { -static constexpr int roomName = 15; -static constexpr int roomDescription = 14; +constexpr int roomName = 15; +constexpr int roomDescription = 14; } // namespace fonts } // namespace topRoomBar namespace timeline { -static constexpr int msgAvatarTopMargin = 15; -static constexpr int msgTopMargin = 2; -static constexpr int msgLeftMargin = 14; -static constexpr int avatarSize = 36; -static constexpr int headerSpacing = 3; -static constexpr int headerLeftMargin = 15; +constexpr int msgAvatarTopMargin = 15; +constexpr int msgTopMargin = 2; +constexpr int msgLeftMargin = 14; +constexpr int avatarSize = 36; +constexpr int headerSpacing = 3; +constexpr int headerLeftMargin = 15; namespace fonts { -static constexpr int timestamp = 13; -static constexpr int dateSeparator = conf::fontSize; +constexpr int timestamp = 13; +constexpr int dateSeparator = conf::fontSize; } // namespace fonts } // namespace timeline diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 61e14d36..9b0c113d 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -18,10 +18,13 @@ #pragma once #include +#include #include +#include #include #include #include +#include class DownloadMediaProxy : public QObject { @@ -33,6 +36,15 @@ signals: void avatarDownloaded(const QImage &img); }; +class StateEventProxy : public QObject +{ + Q_OBJECT + +signals: + void stateEventSent(); + void stateEventError(const QString &msg); +}; + Q_DECLARE_METATYPE(mtx::responses::Sync) /* @@ -48,6 +60,10 @@ public: // Client API. void initialSync() noexcept; void sync() noexcept; + template + std::shared_ptr sendStateEvent(const EventBody &body, + const QString &roomId, + const QString &stateKey = ""); void sendRoomMessage(mtx::events::MessageType ty, int txnId, const QString &roomid, @@ -221,3 +237,50 @@ init(); MatrixClient * client(); } + +template +std::shared_ptr +MatrixClient::sendStateEvent(const EventBody &body, const QString &roomId, const QString &stateKey) +{ + QUrl endpoint(server_); + endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/state/%2/%3") + .arg(roomId) + .arg(QString::fromStdString(to_string(EventT))) + .arg(stateKey)); + + QNetworkRequest request(QString(endpoint.toEncoded())); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + setupAuth(request); + + auto proxy = std::shared_ptr(new StateEventProxy, + [](auto proxy) { proxy->deleteLater(); }); + + auto serializedBody = nlohmann::json(body).dump(); + auto reply = put(request, QByteArray(serializedBody.data(), serializedBody.size())); + connect(reply, &QNetworkReply::finished, this, [reply, proxy]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + auto data = reply->readAll(); + + if (status == 0 || status >= 400) { + try { + mtx::errors::Error res = nlohmann::json::parse(data); + emit proxy->stateEventError(QString::fromStdString(res.error)); + } catch (const std::exception &e) { + emit proxy->stateEventError(QString::fromStdString(e.what())); + } + + return; + } + + try { + mtx::responses::EventId res = nlohmann::json::parse(data); + emit proxy->stateEventSent(); + } catch (const std::exception &e) { + emit proxy->stateEventError(QString::fromStdString(e.what())); + } + }); + + return proxy; +} diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp index 79ca9375..f4145060 100644 --- a/include/dialogs/RoomSettings.hpp +++ b/include/dialogs/RoomSettings.hpp @@ -12,10 +12,38 @@ class QPixmap; class QLayout; class QLabel; class QComboBox; +class TextField; +class QLabel; template class QSharedPointer; +class EditModal : public QWidget +{ + Q_OBJECT + +public: + EditModal(const QString &roomId, QWidget *parent = nullptr); + + void setFields(const QString &roomName, const QString &roomTopic); + +signals: + void nameChanged(const QString &roomName); + +private: + QString roomId_; + QString initialName_; + QString initialTopic_; + + QLabel *errorField_; + + TextField *nameInput_; + TextField *topicInput_; + + FlatButton *applyBtn_; + FlatButton *cancelBtn_; +}; + class TopSection : public QWidget { Q_OBJECT @@ -25,6 +53,7 @@ class TopSection : public QWidget public: TopSection(const RoomInfo &info, const QImage &img, QWidget *parent = nullptr); QSize sizeHint() const override; + void setRoomName(const QString &name); QColor textColor() const { return textColor_; } void setTextColor(QColor &color) { textColor_ = color; } @@ -56,7 +85,7 @@ protected: void paintEvent(QPaintEvent *event) override; private slots: - void save_and_close(); + void saveSettings(); private: static constexpr int AvatarSize = 64; @@ -67,10 +96,14 @@ private: FlatButton *saveBtn_; FlatButton *cancelBtn_; + FlatButton *editFieldsBtn_; + RoomInfo info_; QString room_id_; QImage avatarImg_; + TopSection *topSection_; + QComboBox *accessCombo; }; diff --git a/resources/icons/ui/edit.svg b/resources/icons/ui/edit.svg new file mode 100644 index 00000000..2313feb1 --- /dev/null +++ b/resources/icons/ui/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/res.qrc b/resources/res.qrc index 77a1ca83..711d32be 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -33,6 +33,7 @@ icons/ui/pause-symbol@2x.png icons/ui/remove-symbol.png icons/ui/remove-symbol@2x.png + icons/ui/edit.svg icons/emoji-categories/people.png icons/emoji-categories/people@2x.png diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 35b4cd10..34538203 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -148,6 +148,7 @@ dialogs--MemberList, dialogs--PreviewUploadOverlay, dialogs--CreateRoom > QLineEdit, dialogs--InviteUsers > QLineEdit, +EditModal, dialogs--JoinRoom > QLineEdit { background-color: #202228; color: #caccd1; diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index dbc8d1e0..125c34ff 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -148,6 +148,7 @@ dialogs--ReadReceipts, dialogs--MemberList, dialogs--JoinRoom, dialogs--PreviewUploadOverlay, +EditModal, QListWidget { background-color: white; color: #333; diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 9bf4cf26..4a7ea4fc 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -1,7 +1,9 @@ #include "Avatar.h" #include "Config.h" #include "FlatButton.h" +#include "MatrixClient.h" #include "Painter.h" +#include "TextField.h" #include "Utils.h" #include "dialogs/RoomSettings.hpp" @@ -15,12 +17,146 @@ using namespace dialogs; +EditModal::EditModal(const QString &roomId, QWidget *parent) + : QWidget(parent) + , roomId_{roomId} +{ + setMinimumWidth(360); + setAutoFillBackground(true); + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + auto layout = new QVBoxLayout(this); + + applyBtn_ = new FlatButton(tr("APPLY"), this); + applyBtn_->setFontSize(conf::btn::fontSize); + cancelBtn_ = new FlatButton(tr("CANCEL"), this); + cancelBtn_->setFontSize(conf::btn::fontSize); + + auto btnLayout = new QHBoxLayout; + btnLayout->setContentsMargins(5, 20, 5, 5); + btnLayout->addWidget(applyBtn_); + btnLayout->addWidget(cancelBtn_); + + nameInput_ = new TextField(this); + nameInput_->setLabel(tr("Name")); + topicInput_ = new TextField(this); + topicInput_->setLabel(tr("Topic")); + + QFont font; + font.setPixelSize(conf::modals::errorFont); + + errorField_ = new QLabel(this); + errorField_->setFont(font); + errorField_->setWordWrap(true); + errorField_->hide(); + + layout->addWidget(nameInput_); + layout->addWidget(topicInput_); + layout->addLayout(btnLayout, 1); + + auto labelLayout = new QHBoxLayout; + labelLayout->setAlignment(Qt::AlignHCenter); + labelLayout->addWidget(errorField_); + layout->addLayout(labelLayout); + + connect(applyBtn_, &QPushButton::clicked, [this]() { + // Check if the values are changed from the originals. + auto newName = nameInput_->text().trimmed(); + auto newTopic = topicInput_->text().trimmed(); + + errorField_->hide(); + + if (newName == initialName_ && newTopic == initialTopic_) { + close(); + return; + } + + using namespace mtx::events; + + if (newName != initialName_ && !newName.isEmpty()) { + state::Name body; + body.name = newName.toStdString(); + + auto proxy = + http::client()->sendStateEvent(body, + roomId_); + connect(proxy.get(), + &StateEventProxy::stateEventSent, + this, + [this, proxy, newName]() { + proxy->deleteLater(); + errorField_->hide(); + emit nameChanged(newName); + close(); + }); + + connect(proxy.get(), + &StateEventProxy::stateEventError, + this, + [this, proxy, newName](const QString &msg) { + proxy->deleteLater(); + errorField_->setText(msg); + errorField_->show(); + }); + } + + if (newTopic != initialTopic_ && !newTopic.isEmpty()) { + state::Topic body; + body.topic = newTopic.toStdString(); + + auto proxy = + http::client()->sendStateEvent( + body, roomId_); + connect(proxy.get(), + &StateEventProxy::stateEventSent, + this, + [this, proxy, newTopic]() { + proxy->deleteLater(); + errorField_->hide(); + close(); + }); + + connect(proxy.get(), + &StateEventProxy::stateEventError, + this, + [this, proxy, newTopic](const QString &msg) { + proxy->deleteLater(); + errorField_->setText(msg); + errorField_->show(); + }); + } + }); + connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close); +} + +void +EditModal::setFields(const QString &roomName, const QString &roomTopic) +{ + initialName_ = roomName; + initialTopic_ = roomTopic; + + nameInput_->setText(roomName); + topicInput_->setText(roomTopic); +} + TopSection::TopSection(const RoomInfo &info, const QImage &img, QWidget *parent) : QWidget{parent} , info_{std::move(info)} { textColor_ = palette().color(QPalette::Text); avatar_ = utils::scaleImageToPixmap(img, AvatarSize); + + QSizePolicy policy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setSizePolicy(policy); +} + +void +TopSection::setRoomName(const QString &name) +{ + info_.name = name.toStdString(); + update(); } QSize @@ -28,14 +164,14 @@ TopSection::sizeHint() const { QFont font; font.setPixelSize(18); - return QSize(200, AvatarSize + QFontMetrics(font).ascent() + 6 * Padding); + return QSize(340, AvatarSize + QFontMetrics(font).ascent()); } RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) : QFrame(parent) , room_id_{std::move(room_id)} { - setMaximumWidth(385); + setMaximumWidth(420); try { info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); @@ -45,8 +181,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) qWarning() << "failed to retrieve room info from cache" << room_id; } + constexpr int SettingsMargin = 2; + auto layout = new QVBoxLayout(this); - layout->setSpacing(30); + layout->setSpacing(15); layout->setMargin(20); saveBtn_ = new FlatButton("SAVE", this); @@ -62,7 +200,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) btnLayout->addWidget(cancelBtn_); auto notifOptionLayout_ = new QHBoxLayout; - notifOptionLayout_->setMargin(5); + notifOptionLayout_->setMargin(SettingsMargin); auto notifLabel = new QLabel(tr("Notifications"), this); auto notifCombo = new QComboBox(this); notifCombo->setDisabled(true); @@ -75,62 +213,92 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) notifOptionLayout_->addWidget(notifCombo, 0, Qt::AlignBottom | Qt::AlignRight); auto accessOptionLayout = new QHBoxLayout(); - accessOptionLayout->setMargin(5); + accessOptionLayout->setMargin(SettingsMargin); auto accessLabel = new QLabel(tr("Room access"), this); - accessCombo = new QComboBox(this); + accessCombo = new QComboBox(this); accessCombo->addItem(tr("Anyone and guests")); accessCombo->addItem(tr("Anyone")); accessCombo->addItem(tr("Invited users")); accessCombo->setDisabled(true); accessLabel->setStyleSheet("font-size: 15px;"); - if(info_.join_rule == JoinRule::Public) - { - if(info_.guest_access) - { + if (info_.join_rule == JoinRule::Public) { + if (info_.guest_access) { accessCombo->setCurrentIndex(0); - } - else - { + } else { accessCombo->setCurrentIndex(1); } - } - else - { + } else { accessCombo->setCurrentIndex(2); } accessOptionLayout->addWidget(accessLabel); accessOptionLayout->addWidget(accessCombo); - layout->addWidget(new TopSection(info_, avatarImg_, this)); - layout->addLayout(notifOptionLayout_); + QFont font; + font.setPixelSize(18); + font.setWeight(70); + + auto menuLabel = new QLabel("Room Settings", this); + menuLabel->setFont(font); + + constexpr int buttonSize = 36; + constexpr int iconSize = buttonSize / 2; + + QIcon editIcon; + editIcon.addFile(":/icons/icons/ui/edit.svg"); + editFieldsBtn_ = new FlatButton(this); + editFieldsBtn_->setFixedSize(buttonSize, buttonSize); + editFieldsBtn_->setCornerRadius(iconSize); + editFieldsBtn_->setIcon(editIcon); + editFieldsBtn_->setIcon(editIcon); + editFieldsBtn_->setIconSize(QSize(iconSize, iconSize)); + + connect(editFieldsBtn_, &QPushButton::clicked, this, [this]() { + auto modal = new EditModal(room_id_, this->parentWidget()); + modal->setFields(QString::fromStdString(info_.name), + QString::fromStdString(info_.topic)); + modal->show(); + connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) { + topSection_->setRoomName(newName); + }); + }); + + topSection_ = new TopSection(info_, avatarImg_, this); + + auto editLayout = new QHBoxLayout; + editLayout->setMargin(0); + editLayout->addWidget(topSection_); + editLayout->addWidget(editFieldsBtn_, 0, Qt::AlignRight | Qt::AlignTop); + + layout->addWidget(menuLabel); + layout->addLayout(editLayout); layout->addLayout(notifOptionLayout_); layout->addLayout(accessOptionLayout); layout->addLayout(btnLayout); - connect(cancelBtn_, &FlatButton::clicked, this, &RoomSettings::closing); - connect(saveBtn_, &FlatButton::clicked, this, &RoomSettings::save_and_close); + connect(cancelBtn_, &QPushButton::clicked, this, &RoomSettings::closing); + connect(saveBtn_, &QPushButton::clicked, this, &RoomSettings::saveSettings); } void -RoomSettings::save_and_close() { +RoomSettings::saveSettings() +{ // TODO: Save access changes to the room - if (accessCombo->currentIndex()<2) { - if(info_.join_rule != JoinRule::Public) { + if (accessCombo->currentIndex() < 2) { + if (info_.join_rule != JoinRule::Public) { // Make join_rule Public } - if(accessCombo->currentIndex()==0) { - if(!info_.guest_access) { + if (accessCombo->currentIndex() == 0) { + if (!info_.guest_access) { // Make guest_access CanJoin } } - } - else { - if(info_.join_rule != JoinRule::Invite) { + } else { + if (info_.join_rule != JoinRule::Invite) { // Make join_rule invite } - if(info_.guest_access) { + if (info_.guest_access) { // Make guest_access forbidden } } @@ -152,27 +320,18 @@ TopSection::paintEvent(QPaintEvent *) Painter p(this); PainterHighQualityEnabler hq(p); - constexpr int textPadding = 23; constexpr int textStartX = AvatarSize + 5 * Padding; const int availableTextWidth = width() - textStartX; constexpr int nameFont = 15; constexpr int membersFont = 14; - constexpr int labelFont = 18; - - QFont font; - font.setPixelSize(labelFont); - font.setWeight(70); - - p.setFont(font); - p.setPen(textColor()); - p.drawTextLeft(Padding, Padding, "Room settings"); - p.translate(0, textPadding + QFontMetrics(p.font()).ascent()); p.save(); + p.setPen(textColor()); p.translate(textStartX, 2 * Padding); // Draw the name. + QFont font; font.setPixelSize(membersFont); const auto members = QString("%1 members").arg(info_.member_count);