diff --git a/CMakeLists.txt b/CMakeLists.txt index e3203cc7..1a83f002 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ set(SRC_FILES src/dialogs/MemberList.cpp src/dialogs/LeaveRoom.cpp src/dialogs/Logout.cpp + src/dialogs/UserProfile.cpp src/dialogs/ReadReceipts.cpp src/dialogs/ReCaptcha.cpp src/dialogs/RoomSettings.cpp @@ -265,6 +266,7 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/MemberList.h src/dialogs/LeaveRoom.h src/dialogs/Logout.h + src/dialogs/UserProfile.h src/dialogs/ReadReceipts.h src/dialogs/ReCaptcha.h src/dialogs/RoomSettings.h diff --git a/resources/icons/ui/black-bubble-speech.png b/resources/icons/ui/black-bubble-speech.png new file mode 100644 index 00000000..350189b1 Binary files /dev/null and b/resources/icons/ui/black-bubble-speech.png differ diff --git a/resources/icons/ui/black-bubble-speech@2x.png b/resources/icons/ui/black-bubble-speech@2x.png new file mode 100644 index 00000000..ca5ba09d Binary files /dev/null and b/resources/icons/ui/black-bubble-speech@2x.png differ diff --git a/resources/icons/ui/cursor.png b/resources/icons/ui/cursor.png index cc7a2477..9b3addee 100644 Binary files a/resources/icons/ui/cursor.png and b/resources/icons/ui/cursor.png differ diff --git a/resources/icons/ui/cursor@2x.png b/resources/icons/ui/cursor@2x.png index d02d29fb..89a26cad 100644 Binary files a/resources/icons/ui/cursor@2x.png and b/resources/icons/ui/cursor@2x.png differ diff --git a/resources/icons/ui/do-not-disturb-rounded-sign.png b/resources/icons/ui/do-not-disturb-rounded-sign.png new file mode 100644 index 00000000..996cccd6 Binary files /dev/null and b/resources/icons/ui/do-not-disturb-rounded-sign.png differ diff --git a/resources/icons/ui/do-not-disturb-rounded-sign@2x.png b/resources/icons/ui/do-not-disturb-rounded-sign@2x.png new file mode 100644 index 00000000..24070444 Binary files /dev/null and b/resources/icons/ui/do-not-disturb-rounded-sign@2x.png differ diff --git a/resources/icons/ui/paper-clip-outline.png b/resources/icons/ui/paper-clip-outline.png index a30fd07b..579f564e 100644 Binary files a/resources/icons/ui/paper-clip-outline.png and b/resources/icons/ui/paper-clip-outline.png differ diff --git a/resources/icons/ui/paper-clip-outline@2x.png b/resources/icons/ui/paper-clip-outline@2x.png index 6cb3d6a5..e9f12cd3 100644 Binary files a/resources/icons/ui/paper-clip-outline@2x.png and b/resources/icons/ui/paper-clip-outline@2x.png differ diff --git a/resources/icons/ui/round-remove-button.png b/resources/icons/ui/round-remove-button.png new file mode 100644 index 00000000..b78e177c Binary files /dev/null and b/resources/icons/ui/round-remove-button.png differ diff --git a/resources/icons/ui/round-remove-button@2x.png b/resources/icons/ui/round-remove-button@2x.png new file mode 100644 index 00000000..6bc98e53 Binary files /dev/null and b/resources/icons/ui/round-remove-button@2x.png differ diff --git a/resources/icons/ui/smile.png b/resources/icons/ui/smile.png index 161d46d4..17b3f103 100644 Binary files a/resources/icons/ui/smile.png and b/resources/icons/ui/smile.png differ diff --git a/resources/icons/ui/smile@2x.png b/resources/icons/ui/smile@2x.png index acc439e9..b1bb5098 100644 Binary files a/resources/icons/ui/smile@2x.png and b/resources/icons/ui/smile@2x.png differ diff --git a/resources/icons/ui/volume-off-indicator.png b/resources/icons/ui/volume-off-indicator.png new file mode 100644 index 00000000..0ff3d30e Binary files /dev/null and b/resources/icons/ui/volume-off-indicator.png differ diff --git a/resources/icons/ui/volume-off-indicator@2x.png b/resources/icons/ui/volume-off-indicator@2x.png new file mode 100644 index 00000000..25ebbbbe Binary files /dev/null and b/resources/icons/ui/volume-off-indicator@2x.png differ diff --git a/resources/res.qrc b/resources/res.qrc index 71463e65..8484b299 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -1,5 +1,14 @@ + icons/ui/volume-off-indicator.png + icons/ui/volume-off-indicator@2x.png + icons/ui/black-bubble-speech.png + icons/ui/black-bubble-speech@2x.png + icons/ui/do-not-disturb-rounded-sign.png + icons/ui/do-not-disturb-rounded-sign@2x.png + icons/ui/round-remove-button.png + icons/ui/round-remove-button@2x.png + icons/ui/double-tick-indicator.png icons/ui/double-tick-indicator@2x.png icons/ui/lock.png diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index c95eca2f..2729343c 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -147,6 +147,7 @@ dialogs--ReadReceipts, dialogs--JoinRoom, dialogs--MemberList, dialogs--PreviewUploadOverlay, +dialogs--UserProfile, dialogs--CreateRoom > QLineEdit, dialogs--InviteUsers > QLineEdit, EditModal, diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 674d1722..163832bd 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -149,10 +149,11 @@ dialogs--ReadReceipts, dialogs--MemberList, dialogs--JoinRoom, dialogs--PreviewUploadOverlay, +dialogs--UserProfile, EditModal, QListWidget { background-color: white; - color: #333; + color: #495057; } TopSection { diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index dbfc1945..57b61c75 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -50,6 +50,8 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); mtx::http::ThumbOpts opts; + opts.width = 256; + opts.height = 256; opts.mxc_url = avatarUrl.toStdString(); http::client()->get_thumbnail( diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index fdca98c3..35bfba86 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -309,6 +309,18 @@ MainWindow::hasActiveUser() settings.contains("auth/user_id"); } +void +MainWindow::openUserProfile(const QString &user_id, const QString &room_id) +{ + userProfileDialog_ = QSharedPointer(new dialogs::UserProfile(this)); + userProfileDialog_->init(user_id, room_id); + + userProfileModal_ = + QSharedPointer(new OverlayModal(this, userProfileDialog_.data())); + + userProfileModal_->show(); +} + void MainWindow::openRoomSettings(const QString &room_id) { @@ -382,6 +394,7 @@ MainWindow::showOverlayProgressBar() progressModal_ = QSharedPointer(new OverlayModal(this, spinner_.data()), [](OverlayModal *modal) { modal->deleteLater(); }); + progressModal_->setContentAlignment(Qt::AlignCenter); progressModal_->setColor(QColor(30, 30, 30)); progressModal_->setDismissible(false); progressModal_->show(); diff --git a/src/MainWindow.h b/src/MainWindow.h index 92040191..3d9a94d3 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -28,6 +28,7 @@ #include "RegisterPage.h" #include "UserSettingsPage.h" #include "WelcomePage.h" +#include "dialogs/UserProfile.h" class ChatPage; class LoadingIndicator; @@ -71,6 +72,7 @@ public: void openLogoutDialog(std::function callback); void openRoomSettings(const QString &room_id = ""); void openMemberListDialog(const QString &room_id = ""); + void openUserProfile(const QString &user_id, const QString &room_id); protected: void closeEvent(QCloseEvent *event) override; @@ -171,4 +173,7 @@ private: QSharedPointer memberListModal_; //! Member list dialog. QSharedPointer memberListDialog_; + + QSharedPointer userProfileModal_; + QSharedPointer userProfileDialog_; }; diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp new file mode 100644 index 00000000..1da293a5 --- /dev/null +++ b/src/dialogs/UserProfile.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "AvatarProvider.h" +#include "Cache.h" +#include "Utils.h" +#include "dialogs/UserProfile.h" +#include "ui/Avatar.h" +#include "ui/FlatButton.h" + +using namespace dialogs; + +constexpr int BUTTON_SIZE = 36; + +DeviceItem::DeviceItem(QWidget *parent, QString deviceName) + : QWidget(parent) + , name_(deviceName) +{} + +UserProfile::UserProfile(QWidget *parent) + : QWidget(parent) +{ + QIcon banIcon, kickIcon, ignoreIcon, startChatIcon; + + banIcon.addFile(":/icons/icons/ui/do-not-disturb-rounded-sign.png"); + banBtn_ = new FlatButton(this); + banBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + banBtn_->setCornerRadius(BUTTON_SIZE / 2); + banBtn_->setIcon(banIcon); + banBtn_->setIconSize(QSize(BUTTON_SIZE / 2, BUTTON_SIZE / 2)); + banBtn_->setToolTip(tr("Ban the user from the room")); + + ignoreIcon.addFile(":/icons/icons/ui/volume-off-indicator.png"); + ignoreBtn_ = new FlatButton(this); + ignoreBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + ignoreBtn_->setCornerRadius(BUTTON_SIZE / 2); + ignoreBtn_->setIcon(ignoreIcon); + ignoreBtn_->setIconSize(QSize(BUTTON_SIZE / 2, BUTTON_SIZE / 2)); + ignoreBtn_->setToolTip(tr("Ignore messages from this user")); + + kickIcon.addFile(":/icons/icons/ui/round-remove-button.png"); + kickBtn_ = new FlatButton(this); + kickBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + kickBtn_->setCornerRadius(BUTTON_SIZE / 2); + kickBtn_->setIcon(kickIcon); + kickBtn_->setIconSize(QSize(BUTTON_SIZE / 2, BUTTON_SIZE / 2)); + kickBtn_->setToolTip(tr("Kick the user from the room")); + + startChatIcon.addFile(":/icons/icons/ui/black-bubble-speech.png"); + startChat_ = new FlatButton(this); + startChat_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + startChat_->setCornerRadius(BUTTON_SIZE / 2); + startChat_->setIcon(startChatIcon); + startChat_->setIconSize(QSize(BUTTON_SIZE / 2, BUTTON_SIZE / 2)); + startChat_->setToolTip(tr("Start a conversation")); + + // Button line + auto btnLayout = new QHBoxLayout; + btnLayout->addWidget(startChat_); + btnLayout->addWidget(ignoreBtn_); + + // TODO: check if the user has enough power level given the room_id + // in which the profile was opened. + btnLayout->addWidget(kickBtn_); + btnLayout->addWidget(banBtn_); + btnLayout->setSpacing(8); + btnLayout->setMargin(0); + + avatar_ = new Avatar(this); + avatar_->setLetter("X"); + avatar_->setSize(148); + + QFont font; + font.setPointSizeF(font.pointSizeF() * 2); + + userIdLabel_ = new QLabel(this); + displayNameLabel_ = new QLabel(this); + displayNameLabel_->setFont(font); + + auto textLayout = new QVBoxLayout; + textLayout->addWidget(displayNameLabel_); + textLayout->addWidget(userIdLabel_); + textLayout->setAlignment(displayNameLabel_, Qt::AlignCenter | Qt::AlignTop); + textLayout->setAlignment(userIdLabel_, Qt::AlignCenter | Qt::AlignTop); + textLayout->setSpacing(4); + textLayout->setMargin(0); + + auto vlayout = new QVBoxLayout{this}; + vlayout->addWidget(avatar_); + vlayout->addLayout(textLayout); + vlayout->addLayout(btnLayout); + + vlayout->setAlignment(avatar_, Qt::AlignCenter | Qt::AlignTop); + vlayout->setAlignment(userIdLabel_, Qt::AlignCenter | Qt::AlignTop); + + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + setMinimumWidth(340); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + vlayout->setSpacing(15); + vlayout->setContentsMargins(20, 40, 20, 20); +} + +void +UserProfile::init(const QString &userId, const QString &roomId) +{ + auto displayName = Cache::displayName(roomId, userId); + + userIdLabel_->setText(userId); + displayNameLabel_->setText(displayName); + avatar_->setLetter(utils::firstChar(displayName)); + + AvatarProvider::resolve( + roomId, userId, this, [this](const QImage &img) { avatar_->setImage(img); }); + + QSettings settings; + auto localUser = settings.value("auth/user_id").toString(); + + if (localUser == userId) { + qDebug() << "the local user should have edit rights on avatar & display name"; + // TODO: click on display name & avatar to change. + } + + try { + bool hasMemberRights = + cache::client()->hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, + roomId.toStdString(), + localUser.toStdString()); + if (!hasMemberRights) { + kickBtn_->hide(); + banBtn_->hide(); + } + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } +} + +void +UserProfile::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/dialogs/UserProfile.h b/src/dialogs/UserProfile.h new file mode 100644 index 00000000..8d5b9c5f --- /dev/null +++ b/src/dialogs/UserProfile.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +class Avatar; +class FlatButton; +class QLabel; +class QListWidget; + +namespace dialogs { + +class DeviceItem : public QWidget +{ + Q_OBJECT + +public: + explicit DeviceItem(QWidget *parent, QString deviceName); + +private: + QString name_; + + // Toggle *verifyToggle_; +}; + +class UserProfile : public QWidget +{ + Q_OBJECT +public: + explicit UserProfile(QWidget *parent = nullptr); + + void init(const QString &userId, const QString &roomId); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + Avatar *avatar_; + + QString displayName_; + QString userId_; + + QLabel *userIdLabel_; + QLabel *displayNameLabel_; + + FlatButton *banBtn_; + FlatButton *kickBtn_; + FlatButton *ignoreBtn_; + FlatButton *startChat_; + + QListWidget *devices_; +}; +} // dialogs diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 696db8de..71b3156c 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -23,6 +23,7 @@ #include "ChatPage.h" #include "Config.h" #include "Logging.h" +#include "MainWindow.h" #include "Olm.h" #include "ui/Avatar.h" #include "ui/Painter.h" @@ -561,6 +562,13 @@ TimelineItem::generateBody(const QString &body) // The username/timestamp is displayed along with the message body. void TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) +{ + generateUserName(user_id, displayname); + generateBody(body); +} + +void +TimelineItem::generateUserName(const QString &user_id, const QString &displayname) { auto sender = displayname; @@ -598,7 +606,9 @@ TimelineItem::generateBody(const QString &user_id, const QString &displayname, c userName_->setFont(f); }); - generateBody(body); + connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { + MainWindow::instance()->openUserProfile(user_id, room_id_); + }); } void @@ -742,10 +752,7 @@ TimelineItem::addAvatar() auto userid = descriptionMsg_.userid; auto displayName = Cache::displayName(room_id_, userid); - QFontMetrics fm(usernameFont_); - userName_ = new QLabel(this); - userName_->setFont(usernameFont_); - userName_->setText(fm.elidedText(displayName, Qt::ElideRight, 500)); + generateUserName(userid, displayName); setupAvatarLayout(displayName); diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index 874c00df..db000078 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -137,13 +137,13 @@ public: signals: void hoverOff(); void hoverOn(); + void clicked(); protected: bool eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::MouseButtonRelease) { - // QMouseEvent *mouseEvent = static_cast(event); - // TODO: Open user profile + emit clicked(); return true; } else if (event->type() == QEvent::HoverLeave) { emit hoverOff(); @@ -274,6 +274,7 @@ private: void generateBody(const QString &body); void generateBody(const QString &user_id, const QString &displayname, const QString &body); void generateTimestamp(const QDateTime &time); + void generateUserName(const QString &userid, const QString &displayname); void setupAvatarLayout(const QString &userName); void setupSimpleLayout(); diff --git a/src/ui/OverlayModal.cpp b/src/ui/OverlayModal.cpp index 6aa16b07..41e07a91 100644 --- a/src/ui/OverlayModal.cpp +++ b/src/ui/OverlayModal.cpp @@ -25,11 +25,11 @@ OverlayModal::OverlayModal(QWidget *parent, QWidget *content) , content_{content} , color_{QColor(30, 30, 30, 170)} { - auto layout = new QVBoxLayout(); - layout->addWidget(content); - layout->setAlignment(Qt::AlignCenter); - - setLayout(layout); + layout_ = new QVBoxLayout(this); + layout_->addWidget(content); + layout_->setSpacing(0); + layout_->setContentsMargins(10, 40, 10, 20); + setContentAlignment(Qt::AlignTop | Qt::AlignHCenter); content->setFocus(); } diff --git a/src/ui/OverlayModal.h b/src/ui/OverlayModal.h index a761e3ed..acba7a7a 100644 --- a/src/ui/OverlayModal.h +++ b/src/ui/OverlayModal.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "OverlayWidget.h" @@ -31,6 +32,8 @@ public: void setColor(QColor color) { color_ = color; } void setDismissible(bool state) { isDismissible_ = state; } + void setContentAlignment(QFlags flag) { layout_->setAlignment(flag); } + protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; @@ -38,6 +41,8 @@ protected: private: QWidget *content_; + QVBoxLayout *layout_; + QColor color_; //! Decides whether or not the modal can be removed by clicking into it.