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 <QLabel>
+#include <QWidget>
+
+#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 <QSharedPointer>
#include <QVBoxLayout>
+#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<OverlayModal> leaveRoomModal_;
QSharedPointer<dialogs::LeaveRoom> leaveRoomDialog_;
+ QSharedPointer<OverlayModal> inviteUsersModal_;
+ QSharedPointer<dialogs::InviteUsers> 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 <QFrame>
+#include <QLabel>
+#include <QListWidgetItem>
+#include <QStringList>
+
+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
--- /dev/null
+++ b/resources/icons/ui/remove-symbol.png
Binary files differdiff --git a/resources/icons/ui/remove-symbol@2x.png b/resources/icons/ui/remove-symbol@2x.png
new file mode 100644
index 00000000..aa37086b
--- /dev/null
+++ b/resources/icons/ui/remove-symbol@2x.png
Binary files differdiff --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 @@
<file>icons/ui/play-sign@2x.png</file>
<file>icons/ui/pause-symbol.png</file>
<file>icons/ui/pause-symbol@2x.png</file>
+ <file>icons/ui/remove-symbol.png</file>
+ <file>icons/ui/remove-symbol@2x.png</file>
<file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file>
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<MatrixClient> 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<MatrixClient> 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 <QHBoxLayout>
+
+#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
@@ -861,6 +861,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)
{
QSettings settings;
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 <http://www.gnu.org/licenses/>.
*/
+#include <QDebug>
#include <QStyleOption>
#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<dialogs::InviteUsers>(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<OverlayModal>(
+ 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 <QDebug>
+#include <QIcon>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QStyleOption>
+#include <QTimer>
+#include <QVBoxLayout>
+
+#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<ids::User>(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<InviteeItem *>(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();
|