diff --git a/CMakeLists.txt b/CMakeLists.txt index 1591c36f..b6affe75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ endif() set(SRC_FILES # Dialogs src/dialogs/ImageOverlay.cc + src/dialogs/InviteUsers.cc src/dialogs/JoinRoom.cc src/dialogs/LeaveRoom.cc src/dialogs/Logout.cc @@ -185,6 +186,7 @@ set(SRC_FILES src/Cache.cc src/ChatPage.cc src/Deserializable.cc + src/InviteeItem.cc src/InputValidator.cc src/Login.cc src/LoginPage.cc @@ -218,6 +220,7 @@ include_directories(${LMDB_INCLUDE_DIR}) qt5_wrap_cpp(MOC_HEADERS # Dialogs include/dialogs/ImageOverlay.h + include/dialogs/InviteUsers.h include/dialogs/JoinRoom.h include/dialogs/LeaveRoom.h include/dialogs/Logout.h @@ -259,6 +262,7 @@ qt5_wrap_cpp(MOC_HEADERS include/ChatPage.h include/LoginPage.h include/MainWindow.h + include/InviteeItem.h include/MatrixClient.h include/QuickSwitcher.h include/RegisterPage.h diff --git a/include/InviteeItem.h b/include/InviteeItem.h new file mode 100644 index 00000000..f0bdbdf0 --- /dev/null +++ b/include/InviteeItem.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "mtx.hpp" + +class FlatButton; + +class InviteeItem : public QWidget +{ + Q_OBJECT + +public: + InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr); + + QString userID() { return user_; } + +signals: + void removeItem(); + +private: + QString user_; + + QLabel *name_; + FlatButton *removeUserBtn_; +}; diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 397ba11d..ee493da6 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -60,6 +60,7 @@ public: void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000); void removeTypingNotification(const QString &roomid); void readEvent(const QString &room_id, const QString &event_id); + void inviteUser(const QString &room_id, const QString &user); QUrl getHomeServer() { return server_; }; int transactionId() { return txn_id_; }; @@ -84,6 +85,7 @@ signals: void versionError(const QString &error); void loggedOut(); + void invitedUser(const QString &room_id, const QString &user); void loginSuccess(const QString &userid, const QString &homeserver, const QString &token); void registerSuccess(const QString &userid, diff --git a/include/TopRoomBar.h b/include/TopRoomBar.h index 7bd10356..471662a4 100644 --- a/include/TopRoomBar.h +++ b/include/TopRoomBar.h @@ -26,6 +26,7 @@ #include #include +#include "dialogs/InviteUsers.h" #include "dialogs/LeaveRoom.h" class Avatar; @@ -56,6 +57,7 @@ public: signals: void leaveRoom(); + void inviteUsers(QStringList users); protected: void paintEvent(QPaintEvent *event) override; @@ -76,12 +78,16 @@ private: QMenu *menu_; QAction *toggleNotifications_; QAction *leaveRoom_; + QAction *inviteUsers_; FlatButton *settingsBtn_; QSharedPointer leaveRoomModal_; QSharedPointer leaveRoomDialog_; + QSharedPointer inviteUsersModal_; + QSharedPointer inviteUsersDialog_; + Avatar *avatar_; int buttonSize_; diff --git a/include/dialogs/InviteUsers.h b/include/dialogs/InviteUsers.h new file mode 100644 index 00000000..236a2558 --- /dev/null +++ b/include/dialogs/InviteUsers.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +class FlatButton; +class TextField; +class QListWidget; + +namespace dialogs { + +class InviteUsers : public QFrame +{ + Q_OBJECT +public: + explicit InviteUsers(QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void closing(bool isLeaving, QStringList invitees); + +private slots: + void removeInvitee(QListWidgetItem *item); + +private: + void addUser(); + QStringList invitedUsers() const; + + FlatButton *confirmBtn_; + FlatButton *cancelBtn_; + + TextField *inviteeInput_; + QLabel *errorLabel_; + + QListWidget *inviteeList_; +}; +} // dialogs diff --git a/resources/icons/ui/remove-symbol.png b/resources/icons/ui/remove-symbol.png new file mode 100644 index 00000000..0b610853 Binary files /dev/null and b/resources/icons/ui/remove-symbol.png differ diff --git a/resources/icons/ui/remove-symbol@2x.png b/resources/icons/ui/remove-symbol@2x.png new file mode 100644 index 00000000..aa37086b Binary files /dev/null and b/resources/icons/ui/remove-symbol@2x.png differ diff --git a/resources/res.qrc b/resources/res.qrc index d15dd04c..83415e9b 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -30,6 +30,8 @@ icons/ui/play-sign@2x.png icons/ui/pause-symbol.png icons/ui/pause-symbol@2x.png + icons/ui/remove-symbol.png + icons/ui/remove-symbol@2x.png 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 5704fee1..42ee6740 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -79,11 +79,17 @@ Avatar { dialogs--Logout, dialogs--LeaveRoom, +dialogs--InviteUsers, dialogs--JoinRoom { background-color: #383c4a; color: #caccd1; } +QListWidget { + background-color: #383c4a; + color: #caccd1; +} + WelcomePage, LoginPage, RegisterPage { diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 8ffe625b..6c592ac8 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -81,11 +81,17 @@ Avatar { dialogs--Logout, dialogs--LeaveRoom, +dialogs--InviteUsers, dialogs--JoinRoom { background-color: white; color: #333; } +QListWidget { + background-color: white; + color: #333; +} + WelcomePage, LoginPage, RegisterPage { diff --git a/resources/styles/system.qss b/resources/styles/system.qss index cc54402f..f3bf619d 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -89,3 +89,8 @@ ScrollBar { qproperty-handleColor: palette(text); qproperty-backgroundColor: palette(window); } + +QListWidget { + background-color: palette(window); + color: palette(text); +} diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 25b8fe66..dfae487d 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -109,6 +109,13 @@ ChatPage::ChatPage(QSharedPointer client, QWidget *parent) connect( top_bar_, &TopRoomBar::leaveRoom, this, [=]() { client_->leaveRoom(current_room_); }); + connect(top_bar_, &TopRoomBar::inviteUsers, this, [=](QStringList users) { + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 1000, this, [=]() { + client_->inviteUser(current_room_, users.at(ii)); + }); + } + }); connect(room_list_, &RoomList::roomChanged, this, [=](const QString &roomid) { QStringList users; @@ -258,6 +265,9 @@ ChatPage::ChatPage(QSharedPointer client, QWidget *parent) connect(client_.data(), &MatrixClient::joinedRoom, this, [=]() { emit showNotification("You joined the room."); }); + connect(client_.data(), &MatrixClient::invitedUser, this, [=](QString, QString user) { + emit showNotification(QString("Invited user %1").arg(user)); + }); connect(client_.data(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom); showContentTimer_ = new QTimer(this); diff --git a/src/InviteeItem.cc b/src/InviteeItem.cc new file mode 100644 index 00000000..c544032c --- /dev/null +++ b/src/InviteeItem.cc @@ -0,0 +1,37 @@ +#include + +#include "FlatButton.h" +#include "InviteeItem.h" +#include "Theme.h" + +constexpr int SidePadding = 10; +constexpr int IconSize = 13; + +InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent) + : QWidget{parent} + , user_{QString::fromStdString(user.toString())} +{ + auto topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0); + + QFont font; + font.setPixelSize(15); + + name_ = new QLabel(user_, this); + name_->setFont(font); + + QIcon removeUserIcon; + removeUserIcon.addFile(":/icons/icons/ui/remove-symbol.png"); + + removeUserBtn_ = new FlatButton(this); + removeUserBtn_->setIcon(removeUserIcon); + removeUserBtn_->setIconSize(QSize(IconSize, IconSize)); + removeUserBtn_->setFixedSize(QSize(IconSize, IconSize)); + removeUserBtn_->setRippleStyle(ui::RippleStyle::NoRipple); + + topLayout_->addWidget(name_); + topLayout_->addWidget(removeUserBtn_); + + connect(removeUserBtn_, &FlatButton::clicked, this, &InviteeItem::removeItem); +} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index 2878c4bb..b18b6e4a 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -860,6 +860,36 @@ MatrixClient::leaveRoom(const QString &roomId) }); } +void +MatrixClient::inviteUser(const QString &roomId, const QString &user) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/invite").arg(roomId)); + endpoint.setQuery(query); + + QNetworkRequest request(endpoint); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + + QJsonObject body{{"user_id", user}}; + auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); + + connect(reply, &QNetworkReply::finished, this, [this, reply, roomId, user]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + // TODO: Handle failure. + qWarning() << reply->errorString(); + return; + } + + emit invitedUser(roomId, user); + }); +} void MatrixClient::sendTypingNotification(const QString &roomid, int timeoutInMillis) { diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc index 51a3af68..9ad30064 100644 --- a/src/TopRoomBar.cc +++ b/src/TopRoomBar.cc @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include "Avatar.h" @@ -88,6 +89,33 @@ TopRoomBar::TopRoomBar(QWidget *parent) roomSettings_->toggleNotifications(); }); + inviteUsers_ = new QAction(tr("Invite users"), this); + connect(inviteUsers_, &QAction::triggered, this, [=]() { + if (inviteUsersDialog_.isNull()) { + inviteUsersDialog_ = + QSharedPointer(new dialogs::InviteUsers(this)); + + connect(inviteUsersDialog_.data(), + &dialogs::InviteUsers::closing, + this, + [=](bool isSending, QStringList invitees) { + inviteUsersModal_->fadeOut(); + + if (isSending && !invitees.isEmpty()) + emit inviteUsers(invitees); + }); + } + + if (inviteUsersModal_.isNull()) { + inviteUsersModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), inviteUsersDialog_.data())); + inviteUsersModal_->setDuration(0); + inviteUsersModal_->setColor(QColor(30, 30, 30, 170)); + } + + inviteUsersModal_->fadeIn(); + }); + leaveRoom_ = new QAction(tr("Leave room"), this); connect(leaveRoom_, &QAction::triggered, this, [=]() { if (leaveRoomDialog_.isNull()) { @@ -111,6 +139,7 @@ TopRoomBar::TopRoomBar(QWidget *parent) }); menu_->addAction(toggleNotifications_); + menu_->addAction(inviteUsers_); menu_->addAction(leaveRoom_); connect(settingsBtn_, &QPushButton::clicked, this, [=]() { @@ -171,9 +200,9 @@ TopRoomBar::paintEvent(QPaintEvent *event) QPainter painter(this); style()->drawPrimitive(QStyle::PE_Widget, &option, &painter, this); - // Number of pixels that we can move sidebar splitter per frame. If label contains text - // which fills entire it's width then label starts blocking it's layout from shrinking. - // Making label little bit shorter leaves some space for it to shrink. + // Number of pixels that we can move sidebar splitter per frame. If label contains + // text which fills entire it's width then label starts blocking it's layout from + // shrinking. Making label little bit shorter leaves some space for it to shrink. const auto perFrameResize = 20; QString elidedText = diff --git a/src/dialogs/InviteUsers.cc b/src/dialogs/InviteUsers.cc new file mode 100644 index 00000000..22042453 --- /dev/null +++ b/src/dialogs/InviteUsers.cc @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" +#include "FlatButton.h" +#include "TextField.h" + +#include "InviteeItem.h" +#include "dialogs/InviteUsers.h" + +#include "mtx.hpp" + +using namespace dialogs; + +InviteUsers::InviteUsers(QWidget *parent) + : QFrame(parent) +{ + setMaximumSize(400, 350); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(30); + layout->setMargin(20); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(0); + buttonLayout->setMargin(0); + + confirmBtn_ = new FlatButton("INVITE", 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); + + inviteeInput_ = new TextField(this); + inviteeInput_->setLabel(tr("User ID to invite")); + + inviteeList_ = new QListWidget; + inviteeList_->setFrameStyle(QFrame::NoFrame); + inviteeList_->setSelectionMode(QAbstractItemView::NoSelection); + inviteeList_->setAttribute(Qt::WA_MacShowFocusRect, 0); + inviteeList_->setSpacing(5); + + errorLabel_ = new QLabel(this); + errorLabel_->setAlignment(Qt::AlignCenter); + font.setPixelSize(12); + errorLabel_->setFont(font); + + layout->addWidget(inviteeInput_); + layout->addWidget(errorLabel_); + layout->addWidget(inviteeList_); + layout->addLayout(buttonLayout); + + connect(inviteeInput_, &TextField::returnPressed, this, &InviteUsers::addUser); + connect(confirmBtn_, &QPushButton::clicked, [=]() { + emit closing(true, invitedUsers()); + + inviteeInput_->clear(); + inviteeList_->clear(); + errorLabel_->hide(); + }); + + connect(cancelBtn_, &QPushButton::clicked, [=]() { + QStringList emptyList; + emit closing(false, emptyList); + + inviteeInput_->clear(); + inviteeList_->clear(); + errorLabel_->hide(); + }); +} + +void +InviteUsers::addUser() +{ + auto user_id = inviteeInput_->text(); + + try { + namespace ids = mtx::identifiers; + auto user = ids::parse(user_id.toStdString()); + + auto item = new QListWidgetItem(inviteeList_); + auto invitee = new InviteeItem(user, this); + + item->setSizeHint(invitee->minimumSizeHint()); + item->setFlags(Qt::NoItemFlags); + item->setTextAlignment(Qt::AlignCenter); + + inviteeList_->setItemWidget(item, invitee); + + connect(invitee, &InviteeItem::removeItem, this, [this, item]() { + emit removeInvitee(item); + }); + + errorLabel_->hide(); + inviteeInput_->clear(); + } catch (std::exception &e) { + errorLabel_->setText(e.what()); + errorLabel_->show(); + } +} + +void +InviteUsers::removeInvitee(QListWidgetItem *item) +{ + int row = inviteeList_->row(item); + auto widget = inviteeList_->takeItem(row); + + inviteeList_->removeItemWidget(widget); +} + +void +InviteUsers::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +QStringList +InviteUsers::invitedUsers() const +{ + QStringList users; + + for (int ii = 0; ii < inviteeList_->count(); ++ii) { + auto item = inviteeList_->item(ii); + auto widget = inviteeList_->itemWidget(item); + auto invitee = qobject_cast(widget); + + if (invitee) + users << invitee->userID(); + else + qDebug() << "Cast InviteeItem failed"; + } + + return users; +} diff --git a/src/ui/SnackBar.cc b/src/ui/SnackBar.cc index 39566e99..fb415fcd 100644 --- a/src/ui/SnackBar.cc +++ b/src/ui/SnackBar.cc @@ -18,7 +18,9 @@ SnackBar::SnackBar(QWidget *parent) offset_ = STARTING_OFFSET; position_ = SnackBarPosition::Top; - QFont font("Open Sans", 14, QFont::Medium); + QFont font("Open Sans"); + font.setPixelSize(14); + font.setWeight(50); setFont(font); showTimer_ = new QTimer();