diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index c023f2357..3cbca8dbf 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -98,6 +98,7 @@ set(client_SRCS sharelinkwidget.cpp sharemanager.cpp shareusergroupwidget.cpp + profilepagewidget.cpp sharee.cpp sslbutton.cpp sslerrordialog.cpp @@ -115,7 +116,6 @@ set(client_SRCS guiutility.cpp elidedlabel.cpp headerbanner.cpp - iconjob.cpp iconutils.cpp remotewipe.cpp userstatusselectormodel.cpp diff --git a/src/gui/profilepagewidget.cpp b/src/gui/profilepagewidget.cpp new file mode 100644 index 000000000..5bed47d8a --- /dev/null +++ b/src/gui/profilepagewidget.cpp @@ -0,0 +1,46 @@ +#include "profilepagewidget.h" +#include "guiutility.h" +#include "ocsprofileconnector.h" + +namespace OCC { + +ProfilePageMenu::ProfilePageMenu(AccountPtr account, const QString &shareWithUserId, QWidget *parent) + : QWidget(parent) + , _profileConnector(account) +{ + connect(&_profileConnector, &OcsProfileConnector::hovercardFetched, this, &ProfilePageMenu::onHovercardFetched); + connect(&_profileConnector, &OcsProfileConnector::iconLoaded, this, &ProfilePageMenu::onIconLoaded); + _profileConnector.fetchHovercard(shareWithUserId); +} + +ProfilePageMenu::~ProfilePageMenu() = default; + +void ProfilePageMenu::exec(const QPoint &globalPosition) +{ + _menu.exec(globalPosition); +} + +void ProfilePageMenu::onHovercardFetched() +{ + _menu.clear(); + + const auto hovercardActions = _profileConnector.hovercard()._actions; + for (const auto &hovercardAction : hovercardActions) { + const auto action = _menu.addAction(hovercardAction._icon, hovercardAction._title); + const auto link = hovercardAction._link; + connect(action, &QAction::triggered, action, [link](bool) { Utility::openBrowser(link); }); + } +} + +void ProfilePageMenu::onIconLoaded(const std::size_t &hovercardActionIndex) +{ + const auto hovercardActions = _profileConnector.hovercard()._actions; + const auto menuActions = _menu.actions(); + if (hovercardActionIndex >= hovercardActions.size() + || hovercardActionIndex >= static_cast(menuActions.size())) { + return; + } + const auto menuAction = menuActions[static_cast(hovercardActionIndex)]; + menuAction->setIcon(hovercardActions[hovercardActionIndex]._icon); +} +} diff --git a/src/gui/profilepagewidget.h b/src/gui/profilepagewidget.h new file mode 100644 index 000000000..637dbd460 --- /dev/null +++ b/src/gui/profilepagewidget.h @@ -0,0 +1,30 @@ +#pragma once + +#include "ocsprofileconnector.h" + +#include +#include +#include +#include + +#include + +namespace OCC { + +class ProfilePageMenu : public QWidget +{ + Q_OBJECT +public: + explicit ProfilePageMenu(AccountPtr account, const QString &shareWithUserId, QWidget *parent = nullptr); + ~ProfilePageMenu() override; + + void exec(const QPoint &globalPosition); + +private: + void onHovercardFetched(); + void onIconLoaded(const std::size_t &hovercardActionIndex); + + OcsProfileConnector _profileConnector; + QMenu _menu; +}; +} diff --git a/src/gui/shareusergroupwidget.cpp b/src/gui/shareusergroupwidget.cpp index 0c37c4565..9c9bc3fc0 100644 --- a/src/gui/shareusergroupwidget.cpp +++ b/src/gui/shareusergroupwidget.cpp @@ -12,7 +12,9 @@ * for more details. */ +#include "ocsprofileconnector.h" #include "sharee.h" +#include "tray/usermodel.h" #include "ui_shareusergroupwidget.h" #include "ui_shareuserline.h" #include "shareusergroupwidget.h" @@ -36,7 +38,9 @@ #include #include #include -#include +#include +#include +#include #include #include #include @@ -48,15 +52,37 @@ #include #include #include +#include +#include #include namespace { - const char *passwordIsSetPlaceholder = "●●●●●●●●"; +const char *passwordIsSetPlaceholder = "●●●●●●●●"; + } namespace OCC { +AvatarEventFilter::AvatarEventFilter(QObject *parent) + : QObject(parent) +{ +} + + +bool AvatarEventFilter::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) { + const auto contextMenuEvent = dynamic_cast(event); + if (!contextMenuEvent) { + return false; + } + emit contextMenu(contextMenuEvent->globalPos()); + return true; + } + return QObject::eventFilter(obj, event); +} + ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account, const QString &sharePath, const QString &localPath, @@ -465,16 +491,14 @@ void ShareUserGroupWidget::activateShareeLineEdit() _ui->shareeLineEdit->setFocus(); } -ShareUserLine::ShareUserLine(AccountPtr account, - QSharedPointer share, - SharePermissions maxSharingPermissions, - bool isFile, - QWidget *parent) +ShareUserLine::ShareUserLine(AccountPtr account, QSharedPointer share, + SharePermissions maxSharingPermissions, bool isFile, QWidget *parent) : QWidget(parent) , _ui(new Ui::ShareUserLine) , _account(account) , _share(share) , _isFile(isFile) + , _profilePageMenu(account, share->getShareWith()->shareWith()) { Q_ASSERT(_share); _ui->setupUi(this); @@ -618,11 +642,22 @@ ShareUserLine::ShareUserLine(AccountPtr account, _permissionReshare->setVisible(false); } + const auto avatarEventFilter = new AvatarEventFilter(_ui->avatar); + connect(avatarEventFilter, &AvatarEventFilter::contextMenu, this, &ShareUserLine::onAvatarContextMenu); + _ui->avatar->installEventFilter(avatarEventFilter); + loadAvatar(); customizeStyle(); } +void ShareUserLine::onAvatarContextMenu(const QPoint &globalPosition) +{ + if (_share->getShareType() == Share::TypeUser) { + _profilePageMenu.exec(globalPosition); + } +} + void ShareUserLine::loadAvatar() { const int avatarSize = 36; diff --git a/src/gui/shareusergroupwidget.h b/src/gui/shareusergroupwidget.h index c63e56d74..599ab521b 100644 --- a/src/gui/shareusergroupwidget.h +++ b/src/gui/shareusergroupwidget.h @@ -19,6 +19,7 @@ #include "sharemanager.h" #include "sharepermissions.h" #include "sharee.h" +#include "profilepagewidget.h" #include "QProgressIndicator.h" #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include class QAction; @@ -44,6 +46,21 @@ class SyncResult; class Share; class ShareManager; +class AvatarEventFilter : public QObject +{ + Q_OBJECT + +public: + explicit AvatarEventFilter(QObject *parent = nullptr); + +signals: + void clicked(); + void contextMenu(const QPoint &globalPosition); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; +}; + /** * @brief The ShareDialog (user/group) class * @ingroup gui @@ -166,6 +183,8 @@ private slots: void slotConfirmPasswordClicked(); + void onAvatarContextMenu(const QPoint &globalPosition); + private: void displayPermissions(); void loadAvatar(); @@ -197,6 +216,8 @@ private: QSharedPointer _share; bool _isFile; + ProfilePageMenu _profilePageMenu; + // _permissionEdit is a checkbox QAction *_permissionReshare; QAction *_deleteShareButton; diff --git a/src/gui/shareuserline.ui b/src/gui/shareuserline.ui index e79614a06..5067a3120 100644 --- a/src/gui/shareuserline.ui +++ b/src/gui/shareuserline.ui @@ -6,8 +6,8 @@ 0 0 - 980 - 239 + 899 + 310 @@ -302,32 +302,518 @@ - + 255 + 255 + 255 + + + + + + + 65 + 70 + 84 + + + + + + + 95 + 103 + 127 + + + + + + + 49 + 49 + 49 + + + + + + + 0 0 0 + + + + 25 + 25 + 25 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 64 + 69 + 82 + + + + + + + 56 + 60 + 74 + + + + + + + 0 + 0 + 0 + + + + + + + 82 + 148 + 226 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 157 + 255 + + + + + + + 158 + 79 + 255 + + + + + + + 60 + 67 + 79 + + + + + + + 0 + 0 + 0 + + + + + + + 238 + 252 + 255 + + + + + + + 255 + 255 + 255 + + + - + 255 + 255 + 255 + + + + + + + 65 + 70 + 84 + + + + + + + 95 + 103 + 127 + + + + + + + 49 + 49 + 49 + + + + + + + 0 0 0 + + + + 25 + 25 + 25 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 64 + 69 + 82 + + + + + + + 56 + 60 + 74 + + + + + + + 0 + 0 + 0 + + + + + + + 82 + 149 + 225 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 157 + 255 + + + + + + + 158 + 79 + 255 + + + + + + + 60 + 67 + 79 + + + + + + + 0 + 0 + 0 + + + + + + + 238 + 252 + 255 + + + + + + + 255 + 255 + 255 + + + + + + 255 + 255 + 255 + + + + - 123 - 121 - 134 + 65 + 70 + 84 + + + + + + + 95 + 103 + 127 + + + + + + + 49 + 49 + 49 + + + + + + + 0 + 0 + 0 + + + + + + + 25 + 25 + 25 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 64 + 69 + 82 + + + + + + + 56 + 60 + 74 + + + + + + + 0 + 0 + 0 + + + + + + + 82 + 148 + 226 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 157 + 255 + + + + + + + 158 + 79 + 255 + + + + + + + 60 + 67 + 79 + + + + + + + 0 + 0 + 0 + + + + + + + 238 + 252 + 255 + + + + + + + 255 + 255 + 255 diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 0ec0ccc0e..d58e27d21 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -29,6 +29,7 @@ set(libsync_SRCS configfile.cpp abstractnetworkjob.cpp networkjobs.cpp + iconjob.cpp owncloudpropagator.cpp nextcloudtheme.cpp abstractpropagateremotedeleteencrypted.cpp @@ -58,6 +59,7 @@ set(libsync_SRCS datetimeprovider.cpp ocsuserstatusconnector.cpp userstatusconnector.cpp + ocsprofileconnector.cpp creds/dummycredentials.cpp creds/abstractcredentials.cpp creds/credentialscommon.cpp diff --git a/src/gui/iconjob.cpp b/src/libsync/iconjob.cpp similarity index 89% rename from src/gui/iconjob.cpp rename to src/libsync/iconjob.cpp index 2eaed5d0b..0c9017125 100644 --- a/src/gui/iconjob.cpp +++ b/src/libsync/iconjob.cpp @@ -31,11 +31,15 @@ IconJob::IconJob(const QUrl &url, QObject *parent) : void IconJob::finished(QNetworkReply *reply) { - if (reply->error() != QNetworkReply::NoError) - return; - reply->deleteLater(); deleteLater(); + + const auto networkError = reply->error(); + if (networkError != QNetworkReply::NoError) { + emit error(networkError); + return; + } + emit jobFinished(reply->readAll()); } } diff --git a/src/gui/iconjob.h b/src/libsync/iconjob.h similarity index 89% rename from src/gui/iconjob.h rename to src/libsync/iconjob.h index efbbeb3bd..719ea3be4 100644 --- a/src/gui/iconjob.h +++ b/src/libsync/iconjob.h @@ -15,6 +15,8 @@ #ifndef ICONJOB_H #define ICONJOB_H +#include "owncloudlib.h" + #include #include #include @@ -27,7 +29,7 @@ namespace OCC { * @brief Job to fetch a icon * @ingroup gui */ -class IconJob : public QObject +class OWNCLOUDSYNC_EXPORT IconJob : public QObject { Q_OBJECT public: @@ -35,6 +37,7 @@ public: signals: void jobFinished(QByteArray iconData); + void error(QNetworkReply::NetworkError errorType); private slots: void finished(QNetworkReply *reply); diff --git a/src/libsync/ocsprofileconnector.cpp b/src/libsync/ocsprofileconnector.cpp new file mode 100644 index 000000000..5bda93cc4 --- /dev/null +++ b/src/libsync/ocsprofileconnector.cpp @@ -0,0 +1,168 @@ +#include "ocsprofileconnector.h" +#include "accountfwd.h" +#include "common/result.h" +#include "networkjobs.h" +#include "iconjob.h" +#include "theme.h" +#include "account.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +Q_LOGGING_CATEGORY(lcOcsProfileConnector, "nextcloud.gui.ocsprofileconnector", QtInfoMsg) + +OCC::HovercardAction jsonToAction(const QJsonObject &jsonActionObject) +{ + const auto iconUrl = jsonActionObject.value(QStringLiteral("icon")).toString(QStringLiteral("no-icon")); + QPixmap iconPixmap; + OCC::HovercardAction hovercardAction{ + jsonActionObject.value(QStringLiteral("title")).toString(QStringLiteral("No title")), iconUrl, + jsonActionObject.value(QStringLiteral("hyperlink")).toString(QStringLiteral("no-link"))}; + if (QPixmapCache::find(iconUrl, &iconPixmap)) { + hovercardAction._icon = iconPixmap; + } + return hovercardAction; +} + +OCC::Hovercard jsonToHovercard(const QJsonArray &jsonDataArray) +{ + OCC::Hovercard hovercard; + hovercard._actions.reserve(jsonDataArray.size()); + for (const auto &jsonEntry : jsonDataArray) { + Q_ASSERT(jsonEntry.isObject()); + if (!jsonEntry.isObject()) { + continue; + } + hovercard._actions.push_back(jsonToAction(jsonEntry.toObject())); + } + return hovercard; +} + +OCC::Optional createPixmapFromSvgData(const QByteArray &iconData) +{ + QSvgRenderer svgRenderer; + if (!svgRenderer.load(iconData)) { + return {}; + } + QSize imageSize{16, 16}; + if (OCC::Theme::isHidpi()) { + imageSize = QSize{32, 32}; + } + QImage scaledSvg(imageSize, QImage::Format_ARGB32); + scaledSvg.fill("transparent"); + QPainter svgPainter{&scaledSvg}; + svgRenderer.render(&svgPainter); + return QPixmap::fromImage(scaledSvg); +} + +OCC::Optional iconDataToPixmap(const QByteArray iconData) +{ + if (!iconData.startsWith("serverVersionInt() < Account::makeServerVersion(23, 0, 0)) { + qInfo(lcOcsProfileConnector) << "Server version" << _account->serverVersion() + << "does not support profile page"; + emit error(); + return; + } + const QString url = QStringLiteral("/ocs/v2.php/hovercard/v1/%1").arg(userId); + const auto job = new JsonApiJob(_account, url, this); + connect(job, &JsonApiJob::jsonReceived, this, &OcsProfileConnector::onHovercardFetched); + job->start(); +} + +void OcsProfileConnector::onHovercardFetched(const QJsonDocument &json, int statusCode) +{ + qCDebug(lcOcsProfileConnector) << "Hovercard fetched:" << json; + + if (statusCode != 200) { + qCInfo(lcOcsProfileConnector) << "Fetching of hovercard finished with status code" << statusCode; + return; + } + const auto jsonData = json.object().value("ocs").toObject().value("data").toObject().value("actions"); + Q_ASSERT(jsonData.isArray()); + _currentHovercard = jsonToHovercard(jsonData.toArray()); + fetchIcons(); + emit hovercardFetched(); +} + +void OcsProfileConnector::setHovercardActionIcon(const std::size_t index, const QPixmap &pixmap) +{ + auto &hovercardAction = _currentHovercard._actions[index]; + QPixmapCache::insert(hovercardAction._iconUrl.toString(), pixmap); + hovercardAction._icon = pixmap; + emit iconLoaded(index); +} + +void OcsProfileConnector::loadHovercardActionIcon(const std::size_t hovercardActionIndex, const QByteArray &iconData) +{ + if (hovercardActionIndex >= _currentHovercard._actions.size()) { + // Note: Probably could do more checking, like checking if the url is still the same. + return; + } + const auto icon = iconDataToPixmap(iconData); + if (icon.isValid()) { + setHovercardActionIcon(hovercardActionIndex, icon.get()); + return; + } + qCWarning(lcOcsProfileConnector) << "Could not load Svg icon from data" << iconData; +} + +void OcsProfileConnector::startFetchIconJob(const std::size_t hovercardActionIndex) +{ + const auto hovercardAction = _currentHovercard._actions[hovercardActionIndex]; + const auto iconJob = new IconJob{hovercardAction._iconUrl, this}; + connect(iconJob, &IconJob::jobFinished, + [this, hovercardActionIndex](QByteArray iconData) { loadHovercardActionIcon(hovercardActionIndex, iconData); }); + connect(iconJob, &IconJob::error, this, [](QNetworkReply::NetworkError errorType) { + qCWarning(lcOcsProfileConnector) << "Could not fetch icon:" << errorType; + }); +} + +void OcsProfileConnector::fetchIcons() +{ + for (auto hovercardActionIndex = 0u; hovercardActionIndex < _currentHovercard._actions.size(); + ++hovercardActionIndex) { + startFetchIconJob(hovercardActionIndex); + } +} + +const Hovercard &OcsProfileConnector::hovercard() const +{ + return _currentHovercard; +} +} diff --git a/src/libsync/ocsprofileconnector.h b/src/libsync/ocsprofileconnector.h new file mode 100644 index 000000000..69cd60116 --- /dev/null +++ b/src/libsync/ocsprofileconnector.h @@ -0,0 +1,58 @@ +#pragma once + +#include "accountfwd.h" +#include "owncloudlib.h" + +#include +#include +#include +#include + +namespace OCC { + +struct OWNCLOUDSYNC_EXPORT HovercardAction +{ +public: + HovercardAction(); + HovercardAction(QString title, QUrl iconUrl, QUrl link); + + QString _title; + QUrl _iconUrl; + QPixmap _icon; + QUrl _link; +}; + +struct OWNCLOUDSYNC_EXPORT Hovercard +{ + std::vector _actions; +}; + +class OWNCLOUDSYNC_EXPORT OcsProfileConnector : public QObject +{ + Q_OBJECT +public: + explicit OcsProfileConnector(AccountPtr account, QObject *parent = nullptr); + + void fetchHovercard(const QString &userId); + const Hovercard &hovercard() const; + +signals: + void error(); + void hovercardFetched(); + void iconLoaded(const std::size_t hovercardActionIndex); + +private: + void onHovercardFetched(const QJsonDocument &json, int statusCode); + + void fetchIcons(); + void startFetchIconJob(const std::size_t hovercardActionIndex); + void setHovercardActionIcon(const std::size_t index, const QPixmap &pixmap); + void loadHovercardActionIcon(const std::size_t hovercardActionIndex, const QByteArray &iconData); + + AccountPtr _account; + Hovercard _currentHovercard; +}; +} + +Q_DECLARE_METATYPE(OCC::HovercardAction) +Q_DECLARE_METATYPE(OCC::Hovercard)