diff options
-rw-r--r-- | src/dialogs/RoomSettings.cpp | 133 | ||||
-rw-r--r-- | src/dialogs/RoomSettings.h | 55 | ||||
-rw-r--r-- | src/timeline/TimelineItem.cpp | 3 |
3 files changed, 181 insertions, 10 deletions
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 2cba2990..ff3c92a3 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -1,7 +1,10 @@ #include <QApplication> #include <QComboBox> +#include <QFileDialog> +#include <QImageReader> #include <QLabel> #include <QMessageBox> +#include <QMimeDatabase> #include <QPainter> #include <QPixmap> #include <QShowEvent> @@ -356,6 +359,103 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) else avatar_->setImage(avatarImg_); + if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) { + auto filter = new ClickableFilter(this); + avatar_->installEventFilter(filter); + avatar_->setCursor(Qt::PointingHandCursor); + connect(filter, &ClickableFilter::clicked, this, [this]() { + const auto fileName = QFileDialog::getOpenFileName( + this, tr("Select an avatar"), "", tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + const auto format = mime.name().split("/")[0]; + + QFile file{fileName, this}; + if (format != "image") { + displayErrorMessage(tr("The selected media is not an image")); + return; + } + + if (!file.open(QIODevice::ReadOnly)) { + displayErrorMessage( + tr("Error while reading media: %1").arg(file.errorString())); + return; + } + + if (spinner_) { + startLoadingSpinner(); + resetErrorLabel(); + } + + // Events emitted from the http callbacks (different threads) will + // be queued back into the UI thread through this proxy object. + auto proxy = std::make_shared<ThreadProxy>(); + connect(proxy.get(), + &ThreadProxy::error, + this, + &RoomSettings::displayErrorMessage); + connect( + proxy.get(), &ThreadProxy::avatarChanged, this, &RoomSettings::setAvatar); + + const auto bin = file.peek(file.size()); + const auto payload = std::string(bin.data(), bin.size()); + const auto dimensions = QImageReader(&file).size(); + + // First we need to create a new mxc URI + // (i.e upload media to the Matrix content repository) for the new avatar. + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fileName).fileName().toStdString(), + [proxy = std::move(proxy), + dimensions, + payload, + mimetype = mime.name().toStdString(), + size = payload.size(), + room_id = room_id_.toStdString(), + content = std::move(bin)](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit proxy->error(tr("Failed to upload image: %s") + .arg(QString::fromStdString( + err->matrix_error.error))); + return; + } + + using namespace mtx::events; + state::Avatar avatar_event; + avatar_event.image_info.w = dimensions.width(); + avatar_event.image_info.h = dimensions.height(); + avatar_event.image_info.mimetype = mimetype; + avatar_event.image_info.size = size; + avatar_event.url = res.content_uri; + + http::client() + ->send_state_event<state::Avatar, EventType::RoomAvatar>( + room_id, + avatar_event, + [content = std::move(content), + proxy = std::move(proxy)](const mtx::responses::EventId &, + mtx::http::RequestErr err) { + if (err) { + emit proxy->error( + tr("Failed to upload image: %s") + .arg(QString::fromStdString( + err->matrix_error.error))); + return; + } + + emit proxy->avatarChanged(QImage::fromData(content)); + }); + }); + }); + } + auto roomNameLabel = new QLabel(QString::fromStdString(info_.name), this); roomNameLabel->setFont(doubleFont); @@ -537,6 +637,19 @@ RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::strin return false; } +bool +RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const +{ + try { + return cache::client()->hasEnoughPowerLevel( + {EventType::RoomAvatar}, room_id, user_id); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; +} + void RoomSettings::updateAccessRules(const std::string &room_id, const mtx::events::state::JoinRules &join_rule, @@ -597,6 +710,26 @@ RoomSettings::startLoadingSpinner() } void +RoomSettings::displayErrorMessage(const QString &msg) +{ + stopLoadingSpinner(); + + errorLabel_->show(); + errorLabel_->setText(msg); +} + +void +RoomSettings::setAvatar(const QImage &img) +{ + stopLoadingSpinner(); + + avatarImg_ = img; + + if (avatar_) + avatar_->setImage(img); +} + +void RoomSettings::resetErrorLabel() { if (errorLabel_) { diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h index 408804ba..ebc9f04a 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h @@ -1,5 +1,6 @@ #pragma once +#include <QEvent> #include <QFrame> #include <QImage> @@ -19,6 +20,41 @@ class TextField; class TextField; class Toggle; +class ClickableFilter : public QObject +{ + Q_OBJECT + +public: + explicit ClickableFilter(QWidget *parent) + : QObject(parent) + {} + +signals: + void clicked(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) + { + if (event->type() == QEvent::MouseButtonRelease) { + emit clicked(); + return true; + } + + return QObject::eventFilter(obj, event); + } +}; + +/// Convenience class which connects events emmited from threads +/// outside of main with the UI code. +class ThreadProxy : public QObject +{ + Q_OBJECT + +signals: + void error(const QString &msg); + void avatarChanged(const QImage &img); +}; + class EditModal : public QWidget { Q_OBJECT @@ -71,36 +107,39 @@ private: bool canChangeJoinRules(const std::string &room_id, const std::string &user_id) const; //! Whether the user has enough power level to send m.room.name & m.room.topic events. bool canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const; + //! Whether the user has enough power level to send m.room.avatar event. + bool canChangeAvatar(const std::string &room_id, const std::string &user_id) const; void updateAccessRules(const std::string &room_id, const mtx::events::state::JoinRules &, const mtx::events::state::GuestAccess &); void stopLoadingSpinner(); void startLoadingSpinner(); void resetErrorLabel(); + void displayErrorMessage(const QString &msg); - void setAvatar(const QImage &img) { avatarImg_ = img; } + void setAvatar(const QImage &img); void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); void enableEncryption(); - Avatar *avatar_; + Avatar *avatar_ = nullptr; bool usesEncryption_ = false; QHBoxLayout *btnLayout_; - FlatButton *editFieldsBtn_; + FlatButton *editFieldsBtn_ = nullptr; RoomInfo info_; QString room_id_; QImage avatarImg_; - QLabel *errorLabel_; - LoadingIndicator *spinner_; + QLabel *errorLabel_ = nullptr; + LoadingIndicator *spinner_ = nullptr; - QComboBox *accessCombo; - Toggle *encryptionToggle_; - Toggle *keyRequestsToggle_; + QComboBox *accessCombo = nullptr; + Toggle *encryptionToggle_ = nullptr; + Toggle *keyRequestsToggle_ = nullptr; }; } // dialogs diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index f50f9e27..8726ec59 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -639,18 +639,17 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam auto filter = new UserProfileFilter(user_id, userName_); userName_->installEventFilter(filter); + userName_->setCursor(Qt::PointingHandCursor); connect(filter, &UserProfileFilter::hoverOn, this, [this]() { QFont f = userName_->font(); f.setUnderline(true); - userName_->setCursor(Qt::PointingHandCursor); userName_->setFont(f); }); connect(filter, &UserProfileFilter::hoverOff, this, [this]() { QFont f = userName_->font(); f.setUnderline(false); - userName_->setCursor(Qt::ArrowCursor); userName_->setFont(f); }); |