diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index ee824ba0..5d2baba1 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -33,12 +33,53 @@ ApplicationWindow { spacing: 10 Avatar { - url: "" + url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/") height: 130 width: 130 - displayName: "" - userid: "" Layout.alignment: Qt.AlignHCenter + onClicked: { + if(roomSettings.canChangeAvatar) { + roomSettings.updateAvatar(); + } + } + } + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + running: roomSettings.isLoading + visible: roomSettings.isLoading + } + + Text { + id: errorText + text: "Error Text" + color: "red" + visible: opacity > 0 + opacity: 0 + Layout.alignment: Qt.AlignHCenter + } + + SequentialAnimation { + id: hideErrorAnimation + running: false + PauseAnimation { + duration: 4000 + } + NumberAnimation { + target: errorText + property: 'opacity' + to: 0 + duration: 1000 + } + } + + Connections{ + target: roomSettings + onDisplayError: { + errorText.text = errorMessage + errorText.opacity = 1 + hideErrorAnimation.restart() + } } ColumnLayout { diff --git a/src/dialogs/RoomSettingsOld.cpp b/src/dialogs/RoomSettingsOld.cpp index 7eb34c20..bc34715e 100644 --- a/src/dialogs/RoomSettingsOld.cpp +++ b/src/dialogs/RoomSettingsOld.cpp @@ -143,10 +143,10 @@ EditModal::applyClicked() } using namespace mtx::events; - auto proxy = std::make_shared(); - connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent); - connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent); - connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error); + auto proxy = std::make_shared(); + connect(proxy.get(), &ThreadProxya::topicEventSent, this, &EditModal::topicEventSent); + connect(proxy.get(), &ThreadProxya::nameEventSent, this, &EditModal::nameEventSent); + connect(proxy.get(), &ThreadProxya::error, this, &EditModal::error); if (newName != initialName_ && !newName.isEmpty()) { state::Name body; @@ -810,9 +810,9 @@ RoomSettingsOld::updateAvatar() // 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(); - connect(proxy.get(), &ThreadProxy::error, this, &RoomSettingsOld::displayErrorMessage); - connect(proxy.get(), &ThreadProxy::avatarChanged, this, &RoomSettingsOld::setAvatar); + auto proxy = std::make_shared(); + connect(proxy.get(), &ThreadProxya::error, this, &RoomSettingsOld::displayErrorMessage); + connect(proxy.get(), &ThreadProxya::avatarChanged, this, &RoomSettingsOld::setAvatar); const auto bin = file.peek(file.size()); const auto payload = std::string(bin.data(), bin.size()); diff --git a/src/dialogs/RoomSettingsOld.h b/src/dialogs/RoomSettingsOld.h index e517676a..ad8dd5bd 100644 --- a/src/dialogs/RoomSettingsOld.h +++ b/src/dialogs/RoomSettingsOld.h @@ -40,7 +40,7 @@ protected: /// Convenience class which connects events emmited from threads /// outside of main with the UI code. -class ThreadProxy : public QObject +class ThreadProxya : public QObject { Q_OBJECT diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp index 49e48e40..3ff1d5d5 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp @@ -1,5 +1,9 @@ #include "RoomSettings.h" +#include +#include +#include +#include #include #include @@ -84,6 +88,18 @@ RoomSettings::roomVersion() const return QString::fromStdString(info_.version); } +bool +RoomSettings::isLoading() const +{ + return isLoading_; +} + +QString +RoomSettings::roomAvatarUrl() +{ + return QString::fromStdString(info_.avatar_url); +} + int RoomSettings::memberCount() const { @@ -96,7 +112,6 @@ RoomSettings::retrieveRoomInfo() try { usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString()); info_ = cache::singleRoomInfo(roomid_.toStdString()); - // setAvatar(); } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", roomid_.toStdString()); @@ -143,9 +158,9 @@ RoomSettings::enableEncryption() room_id, err->matrix_error.error, status_code); - //emit enableEncryptionError( - // tr("Failed to enable encryption: %1") - // .arg(QString::fromStdString(err->matrix_error.error))); + emit displayError( + tr("Failed to enable encryption: %1") + .arg(QString::fromStdString(err->matrix_error.error))); usesEncryption_ = false; emit encryptionChanged(); return; @@ -172,6 +187,33 @@ RoomSettings::canChangeJoinRules() const return false; } +bool +RoomSettings::canChangeNameAndTopic() const +{ + try { + return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic}, + roomid_.toStdString(), + utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; +} + +bool +RoomSettings::canChangeAvatar() const +{ + try { + return cache::hasEnoughPowerLevel( + {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; +} + bool RoomSettings::isEncryptionEnabled() const { @@ -269,8 +311,8 @@ RoomSettings::updateAccessRules(const std::string &room_id, const mtx::events::state::JoinRules &join_rule, const mtx::events::state::GuestAccess &guest_access) { - // startLoadingSpinner(); - // resetErrorLabel(); + isLoading_ = true; + emit loadingChanged(); http::client()->send_state_event( room_id, @@ -281,8 +323,9 @@ RoomSettings::updateAccessRules(const std::string &room_id, nhlog::net()->warn("failed to send m.room.join_rule: {} {}", static_cast(err->status_code), err->matrix_error.error); - // emit showErrorMessage(QString::fromStdString(err->matrix_error.error)); - + emit displayError(QString::fromStdString(err->matrix_error.error)); + isLoading_ = false; + emit loadingChanged(); return; } @@ -294,13 +337,115 @@ RoomSettings::updateAccessRules(const std::string &room_id, nhlog::net()->warn("failed to send m.room.guest_access: {} {}", static_cast(err->status_code), err->matrix_error.error); - // emit showErrorMessage( - // QString::fromStdString(err->matrix_error.error)); + emit displayError( + QString::fromStdString(err->matrix_error.error)); + } + isLoading_ = false; + emit loadingChanged(); + }); + }); +} + +void +RoomSettings::stopLoading() +{ + isLoading_ = false; + emit loadingChanged(); +} + +void +RoomSettings::avatarChanged() +{ + retrieveRoomInfo(); + emit avatarUrlChanged(); +} + +void +RoomSettings::updateAvatar() +{ + const QString picturesFolder = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString fileName = QFileDialog::getOpenFileName( + nullptr, tr("Select an avatar"), picturesFolder, 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") { + emit displayError(tr("The selected file is not an image")); + return; + } + + if (!file.open(QIODevice::ReadOnly)) { + emit displayError(tr("Error while reading file: %1").arg(file.errorString())); + return; + } + + isLoading_ = true; + emit loadingChanged(); + + // 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(); + connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError); + connect(proxy.get(), &ThreadProxy::avatarChanged, this, &RoomSettings::avatarChanged); + connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading); + + 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 = roomid_.toStdString(), + content = std::move(bin)](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit proxy->stopLoading(); + 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( + 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 signal that stops loading spinner and reset error label + emit proxy->stopLoading(); + emit proxy->avatarChanged(); }); }); } \ No newline at end of file diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h index f5cc043c..09295a58 100644 --- a/src/ui/RoomSettings.h +++ b/src/ui/RoomSettings.h @@ -7,16 +7,34 @@ #include "CacheStructs.h" +/// 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(); + void nameEventSent(const QString &); + void topicEventSent(); + void stopLoading(); +}; + class RoomSettings : public QObject { - Q_OBJECT + Q_OBJECT Q_PROPERTY(QString roomName READ roomName CONSTANT) Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(int memberCount READ memberCount CONSTANT) Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged) Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged) + Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT) + Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT) + Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT) Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged) Q_PROPERTY(bool respondsToKeyRequests READ respondsToKeyRequests NOTIFY keyRequestsChanged) @@ -26,24 +44,38 @@ public: QString roomName() const; QString roomId() const; QString roomVersion() const; + QString roomAvatarUrl(); int memberCount() const; int notifications(); int accessJoinRules(); bool respondsToKeyRequests(); + bool isLoading() const; //! Whether the user has enough power level to send m.room.join_rules events. bool canChangeJoinRules() const; + //! Whether the user has enough power level to send m.room.name & m.room.topic events. + bool canChangeNameAndTopic() const; + //! Whether the user has enough power level to send m.room.avatar event. + bool canChangeAvatar() const; bool isEncryptionEnabled() const; Q_INVOKABLE void changeNotifications(int currentIndex); Q_INVOKABLE void changeAccessRules(int index); Q_INVOKABLE void changeKeyRequestsPreference(bool isOn); Q_INVOKABLE void enableEncryption(); + Q_INVOKABLE void updateAvatar(); signals: void notificationsChanged(); void accessJoinRulesChanged(); void keyRequestsChanged(); void encryptionChanged(); + void avatarUrlChanged(); + void loadingChanged(); + void displayError(const QString &errorMessage); + +public slots: + void avatarChanged(); + void stopLoading(); private: void retrieveRoomInfo(); @@ -54,6 +86,7 @@ private: private: QString roomid_; bool usesEncryption_ = false; + bool isLoading_ = false; RoomInfo info_; int notifications_ = 0; int accessRules_ = 0;