diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 13cde2ff9b86f..d3e943063f26f 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -98,6 +98,7 @@ set(client_SRCS systray.cpp thumbnailjob.cpp userinfo.cpp + userstatus.cpp accountstate.cpp addcertificatedialog.cpp authenticationdialog.cpp diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index 90c05fefed675..ac5a21c98d3d8 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -44,6 +44,8 @@ AccountState::AccountState(AccountPtr account) , _waitingForNewCredentials(false) , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay , _remoteWipe(new RemoteWipe(_account)) + , _userStatus(new UserStatus(this)) + , _isDesktopNotificationsAllowed(true) { qRegisterMetaType("AccountState*"); @@ -125,6 +127,21 @@ void AccountState::setState(State state) emit stateChanged(_state); } +UserStatus::Status AccountState::status() const +{ + return _userStatus->status(); +} + +QString AccountState::statusMessage() const +{ + return _userStatus->message(); +} + +QUrl AccountState::statusIcon() const +{ + return _userStatus->icon(); +} + QString AccountState::stateString(State state) { switch (state) { @@ -205,6 +222,16 @@ void AccountState::setNavigationAppsEtagResponseHeader(const QByteArray &value) _navigationAppsEtagResponseHeader = value; } +bool AccountState::isDesktopNotificationsAllowed() const +{ + return _isDesktopNotificationsAllowed; +} + +void AccountState::setDesktopNotificationsAllowed(const bool isAllowed) +{ + _isDesktopNotificationsAllowed = isAllowed; +} + void AccountState::checkConnectivity() { if (isSignedOut() || _waitingForNewCredentials) { @@ -422,6 +449,12 @@ void AccountState::fetchNavigationApps(){ job->getNavigationApps(); } +void AccountState::fetchUserStatus() +{ + connect(_userStatus, &UserStatus::fetchUserStatusFinished, this, &AccountState::statusChanged); + _userStatus->fetchUserStatus(_account); +} + void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){ if(statusCode == 200){ qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value; diff --git a/src/gui/accountstate.h b/src/gui/accountstate.h index 4c6bba62d8e7c..59966f1313181 100644 --- a/src/gui/accountstate.h +++ b/src/gui/accountstate.h @@ -21,6 +21,7 @@ #include #include "connectionvalidator.h" #include "creds/abstractcredentials.h" +#include "userstatus.h" #include class QSettings; @@ -161,6 +162,32 @@ class AccountState : public QObject, public QSharedData ///Asks for user credentials void handleInvalidCredentials(); + /** Returns the user status (Online, Dnd, Away, Offline, Invisible) + * https://gist.github.com/georgehrke/55a0412007f13be1551d1f9436a39675 + */ + UserStatus::Status status() const; + + /** Returns the user status Message (emoji + text) + */ + QString statusMessage() const; + + /** Returns the user status icon url + */ + QUrl statusIcon() const; + + /** Returns the notifications status retrieved by the notificatons endpoint + * https://github.com/nextcloud/desktop/issues/2318#issuecomment-680698429 + */ + bool isDesktopNotificationsAllowed() const; + + /** Set desktop notifications status retrieved by the notificatons endpoint + */ + void setDesktopNotificationsAllowed(const bool isAllowed); + + /** Fetch the user status (status, icon, message) + */ + void fetchUserStatus(); + public slots: /// Triggers a ping to the server to update state and /// connection status and errors. @@ -174,6 +201,7 @@ public slots: void stateChanged(State state); void isConnectedChanged(); void hasFetchedNavigationApps(); + void statusChanged(); protected Q_SLOTS: void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors); @@ -223,6 +251,8 @@ protected Q_SLOTS: */ AccountAppList _apps; + UserStatus *_userStatus; + bool _isDesktopNotificationsAllowed; }; class AccountApp : public QObject diff --git a/src/gui/tray/NotificationHandler.cpp b/src/gui/tray/NotificationHandler.cpp index 98e5c1aa7875a..46700ec1e23b8 100644 --- a/src/gui/tray/NotificationHandler.cpp +++ b/src/gui/tray/NotificationHandler.cpp @@ -48,6 +48,8 @@ void ServerNotificationHandler::slotFetchNotifications() this, &ServerNotificationHandler::slotNotificationsReceived); QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived, this, &ServerNotificationHandler::slotEtagResponseHeaderReceived); + QObject::connect(_notificationJob.data(), &JsonApiJob::allowDesktopNotificationsChanged, + this, &ServerNotificationHandler::slotAllowDesktopNotificationsChanged); _notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue(_accountState)); _notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader()); _notificationJob->start(); @@ -62,6 +64,14 @@ void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray } } +void ServerNotificationHandler::slotAllowDesktopNotificationsChanged(const bool isAllowed) +{ + auto *account = qvariant_cast(sender()->property(propertyAccountStateC)); + if (account != nullptr) { + account->setDesktopNotificationsAllowed(isAllowed); + } +} + void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData) { iconCache.insert(sender()->property("activityId").toInt(),iconData); diff --git a/src/gui/tray/NotificationHandler.h b/src/gui/tray/NotificationHandler.h index 69e286e783e33..61fbb51ec5dfd 100644 --- a/src/gui/tray/NotificationHandler.h +++ b/src/gui/tray/NotificationHandler.h @@ -26,6 +26,7 @@ private slots: void slotNotificationsReceived(const QJsonDocument &json, int statusCode); void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode); void slotIconDownloaded(QByteArray iconData); + void slotAllowDesktopNotificationsChanged(const bool isAllowed); private: QPointer _notificationJob; diff --git a/src/gui/tray/UserLine.qml b/src/gui/tray/UserLine.qml index 654aa079c47c7..ce752fef1cbe1 100644 --- a/src/gui/tray/UserLine.qml +++ b/src/gui/tray/UserLine.qml @@ -35,7 +35,7 @@ MenuItem { anchors.fill: parent hoverEnabled: true onContainsMouseChanged: { - accountStateIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white") + accountStatusIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white") } onClicked: { if (!isCurrentUser) { @@ -71,8 +71,8 @@ MenuItem { Layout.preferredHeight: (userLineLayout.height -16) Layout.preferredWidth: (userLineLayout.height -16) Rectangle { - id: accountStateIndicatorBackground - width: accountStateIndicator.sourceSize.width + 2 + id: accountStatusIndicatorBackground + width: accountStatusIndicator.sourceSize.width + 2 height: width anchors.bottom: accountAvatar.bottom anchors.right: accountAvatar.right @@ -80,18 +80,16 @@ MenuItem { radius: width*0.5 } Image { - id: accountStateIndicator - source: model.isConnected - ? Style.stateOnlineImageSource - : Style.stateOfflineImageSource + id: accountStatusIndicator + source: model.statusIcon cache: false - x: accountStateIndicatorBackground.x + 1 - y: accountStateIndicatorBackground.y + 1 + x: accountStatusIndicatorBackground.x + 1 + y: accountStatusIndicatorBackground.y + 1 sourceSize.width: Style.accountAvatarStateIndicatorSize sourceSize.height: Style.accountAvatarStateIndicatorSize Accessible.role: Accessible.Indicator - Accessible.name: model.isConnected ? qsTr("Account connected") : qsTr("Account not connected") + Accessible.name: model.isStatusOnline ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb") } } @@ -109,6 +107,14 @@ MenuItem { font.pixelSize: 12 font.bold: true } + Label { + id: userStatusMessage + width: 128 + text: statusMessage + elide: Text.ElideRight + color: "black" + font.pixelSize: 10 + } Label { id: accountServer width: 128 @@ -223,13 +229,4 @@ MenuItem { } } } - - Connections { - target: UserModel - onRefreshCurrentUserGui: { - accountStateIndicator.source = model.isConnected - ? Style.stateOnlineImageSource - : Style.stateOfflineImageSource - } - } } // MenuItem userLine diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp index 8a1233be7928c..89345c9328ab7 100644 --- a/src/gui/tray/UserModel.cpp +++ b/src/gui/tray/UserModel.cpp @@ -53,6 +53,7 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent) connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog); connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged); + connect(_account.data(), &AccountState::statusChanged, this, &User::statusChanged); connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest); } @@ -87,7 +88,7 @@ void User::slotBuildNotificationDisplay(const ActivityList &list) // Assemble a tray notification for the NEW notification ConfigFile cfg; - if (cfg.optionalServerNotifications()) { + if (cfg.optionalServerNotifications() && isDesktopNotificationsAllowed()) { if (AccountManager::instance()->accounts().count() == 1) { emit guiLog(activity._subject, ""); } else { @@ -185,6 +186,8 @@ void User::slotRefreshImmediately() { void User::slotRefresh() { + slotRefreshUserStatus(); + if (checkPushNotificationsAreReady()) { // we are relying on WebSocket push notifications - ignore refresh attempts from UI _timeSinceLastCheck[_account.data()].invalidate(); @@ -216,6 +219,13 @@ void User::slotRefreshActivities() _activityModel->slotRefreshActivity(); } +void User::slotRefreshUserStatus() { + // TODO: check for _account->account()->capabilities().userStatus() + if (_account.data() && _account.data()->isConnected()) { + _account.data()->fetchUserStatus(); + } +} + void User::slotRefreshNotifications() { // start a server notification handler if no notification requests @@ -557,6 +567,21 @@ QString User::server(bool shortened) const return serverUrl; } +UserStatus::Status User::status() const +{ + return _account->status(); +} + +QString User::statusMessage() const +{ + return _account->statusMessage(); +} + +QUrl User::statusIcon() const +{ + return _account->statusIcon(); +} + QImage User::avatar() const { return AvatarJob::makeCircularAvatar(_account->account()->avatar()); @@ -606,6 +631,12 @@ bool User::isConnected() const return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected); } + +bool User::isDesktopNotificationsAllowed() const +{ + return _account.data()->isDesktopNotificationsAllowed(); +} + void User::removeAccount() const { AccountManager::instance()->deleteAccount(_account.data()); @@ -667,6 +698,16 @@ Q_INVOKABLE bool UserModel::isUserConnected(const int &id) return _users[id]->isConnected(); } +Q_INVOKABLE QUrl UserModel::statusIcon(const int &id) +{ + if (id < 0 || id >= _users.size()) { + return {}; + } + + return _users[id]->statusIcon(); +} + + QImage UserModel::avatarById(const int &id) { if (id < 0 || id >= _users.size()) @@ -703,6 +744,11 @@ void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent) emit dataChanged(index(row, 0), index(row, 0), {UserModel::AvatarRole}); }); + connect(u, &User::statusChanged, this, [this, row] { + emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole, + UserModel::StatusMessageRole}); + }); + _users << u; if (isCurrent) { _currentUserId = _users.indexOf(_users.last()); @@ -841,6 +887,10 @@ QVariant UserModel::data(const QModelIndex &index, int role) const return _users[index.row()]->name(); } else if (role == ServerRole) { return _users[index.row()]->server(); + } else if (role == StatusIconRole) { + return _users[index.row()]->statusIcon(); + } else if (role == StatusMessageRole) { + return _users[index.row()]->statusMessage(); } else if (role == AvatarRole) { return _users[index.row()]->avatarUrl(); } else if (role == IsCurrentUserRole) { @@ -858,6 +908,8 @@ QHash UserModel::roleNames() const QHash roles; roles[NameRole] = "name"; roles[ServerRole] = "server"; + roles[StatusIconRole] = "statusIcon"; + roles[StatusMessageRole] = "statusMessage"; roles[AvatarRole] = "avatar"; roles[IsCurrentUserRole] = "isCurrentUser"; roles[IsConnectedRole] = "isConnected"; diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h index 6302d13407243..527dd19490c00 100644 --- a/src/gui/tray/UserModel.h +++ b/src/gui/tray/UserModel.h @@ -19,6 +19,8 @@ class User : public QObject Q_OBJECT Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString server READ server CONSTANT) + Q_PROPERTY(QUrl statusIcon READ statusIcon NOTIFY statusChanged) + Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusChanged) Q_PROPERTY(bool hasLocalFolder READ hasLocalFolder NOTIFY hasLocalFolderChanged) Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged) Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged) @@ -45,6 +47,10 @@ class User : public QObject void logout() const; void removeAccount() const; QString avatarUrl() const; + bool isDesktopNotificationsAllowed() const; + UserStatus::Status status() const; + QString statusMessage() const; + QUrl statusIcon() const; signals: void guiLog(const QString &, const QString &); @@ -53,6 +59,7 @@ class User : public QObject void serverHasTalkChanged(); void avatarChanged(); void accountStateChanged(int state); + void statusChanged(); public slots: void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); @@ -67,6 +74,7 @@ public slots: void slotRefreshNotifications(); void slotRefreshActivities(); void slotRefresh(); + void slotRefreshUserStatus(); void slotRefreshImmediately(); void setNotificationRefreshInterval(std::chrono::milliseconds interval); void slotRebuildNavigationAppList(); @@ -132,6 +140,7 @@ class UserModel : public QAbstractListModel Q_INVOKABLE bool currentUserHasLocalFolder(); int currentUserId() const; Q_INVOKABLE bool isUserConnected(const int &id); + Q_INVOKABLE QUrl statusIcon(const int &id); Q_INVOKABLE void switchCurrentUser(const int &id); Q_INVOKABLE void login(const int &id); Q_INVOKABLE void logout(const int &id); @@ -142,6 +151,8 @@ class UserModel : public QAbstractListModel enum UserRoles { NameRole = Qt::UserRole + 1, ServerRole, + StatusIconRole, + StatusMessageRole, AvatarRole, IsCurrentUserRole, IsConnectedRole, diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index f46bc61001313..f27bdc3505eb4 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -35,8 +35,8 @@ Window { } onVisibleChanged: { - currentAccountStateIndicator.source = "" - currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId) + folderStateIndicator.source = "" + folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId) ? Style.stateOnlineImageSource : Style.stateOfflineImageSource @@ -49,8 +49,8 @@ Window { Connections { target: UserModel onRefreshCurrentUserGui: { - currentAccountStateIndicator.source = "" - currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId) + folderStateIndicator.source = "" + folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId) ? Style.stateOnlineImageSource : Style.stateOfflineImageSource } @@ -328,7 +328,7 @@ Window { Accessible.name: qsTr("Current user avatar") Rectangle { - id: currentAccountStateIndicatorBackground + id: currentAccountStatusIndicatorBackground width: Style.accountAvatarStateIndicatorSize + 2 height: width anchors.bottom: currentAccountAvatar.bottom @@ -348,18 +348,16 @@ Window { } Image { - id: currentAccountStateIndicator - source: UserModel.isUserConnected(UserModel.currentUserId) - ? Style.stateOnlineImageSource - : Style.stateOfflineImageSource + id: currentAccountStatusIndicator + source: UserModel.currentUser.statusIcon cache: false - x: currentAccountStateIndicatorBackground.x + 1 - y: currentAccountStateIndicatorBackground.y + 1 + x: currentAccountStatusIndicatorBackground.x + 1 + y: currentAccountStatusIndicatorBackground.y + 1 sourceSize.width: Style.accountAvatarStateIndicatorSize sourceSize.height: Style.accountAvatarStateIndicatorSize Accessible.role: Accessible.Indicator - Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected") + Accessible.name: UserModel.isUserStatusOnline(UserModel.currentUserId()) ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb") } } @@ -379,9 +377,9 @@ Window { font.bold: true } Label { - id: currentAccountServer + id: currentUserStatus width: Style.currentAccountLabelWidth - text: UserModel.currentUser.server + text: UserModel.currentUser.statusMessage elide: Text.ElideRight color: Style.ncTextColor font.pixelSize: Style.subLinePixelSize @@ -423,6 +421,42 @@ Window { Accessible.onPressAction: openLocalFolderButton.clicked() } + Rectangle { + id: folderStateIndicatorBackground + width: Style.folderStateIndicatorSize + height: width + anchors.top: openLocalFolderButton.verticalCenter + anchors.left: openLocalFolderButton.horizontalCenter + color: Style.ncBlue + radius: width*0.5 + } + + Rectangle { + id: folderStateRectangle + width: Style.folderStateIndicatorSize + height: width + anchors.bottom: openLocalFolderButton.bottom + anchors.right: openLocalFolderButton.right + color: openLocalFolderButton.containsMouse ? "white" : "transparent" + opacity: 0.2 + radius: width*0.5 + } + + Image { + id: folderStateIndicator + source: UserModel.isUserConnected(UserModel.currentUserId) + ? Style.stateOnlineImageSource + : Style.stateOfflineImageSource + cache: false + x: folderStateIndicatorBackground.x + y: folderStateIndicatorBackground.y + sourceSize.width: Style.folderStateIndicatorSize + sourceSize.height: Style.folderStateIndicatorSize + + Accessible.role: Accessible.Indicator + Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected") + } + HeaderButton { id: trayWindowTalkButton diff --git a/src/gui/userstatus.cpp b/src/gui/userstatus.cpp new file mode 100644 index 0000000000000..dc6866889e065 --- /dev/null +++ b/src/gui/userstatus.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) by Camila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "userstatus.h" +#include "account.h" +#include "accountstate.h" +#include "networkjobs.h" +#include "folderman.h" +#include "creds/abstractcredentials.h" +#include "theme.h" + +#include +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcUserStatus, "nextcloud.gui.userstatus", QtInfoMsg) + +UserStatus::UserStatus(QObject *parent) + : QObject(parent) + , _message("") +{ +} + +UserStatus::Status UserStatus::stringToEnum(const QString &status) const +{ + // api should return invisible, dnd,... toLower() it is to make sure + // it matches _preDefinedStatus, otherwise the default is online (0) + const auto statusEnum = _preDefinedStatus.value(status.isEmpty()? "online" : status.toLower(), 0); + return static_cast(statusEnum); +} + +void UserStatus::fetchUserStatus(AccountPtr account) +{ + if (_job) { + _job->deleteLater(); + } + + _job = new JsonApiJob(account, QStringLiteral("/ocs/v2.php/apps/user_status/api/v1/user_status"), this); + connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserStatus::slotFetchUserStatusFinished); + _job->start(); +} + +void UserStatus::slotFetchUserStatusFinished(const QJsonDocument &json, const int statusCode) +{ + const QJsonObject defaultValues + { + {"icon", ""}, + {"message", ""}, + {"status", "online"} + }; + + if (statusCode != 200) { + qCInfo(lcUserStatus) << "Slot fetch UserStatus finished with status code" << statusCode; + qCInfo(lcUserStatus) << "Using then default values as if user has not set any status" << defaultValues; + } + const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject(defaultValues); + const auto emoji = retrievedData.value("icon").toString(); + const auto message = retrievedData.value("message").toString(); + auto statusString = retrievedData.value("status").toString(); + _status = stringToEnum(statusString); + + // to display it to the user like 'Invisible' instead of 'invisible' + statusString.replace(0, 1, statusString.at(0).toUpper()); + + const auto visibleStatusText = message.isEmpty() + ? _status == DoNotDisturb? tr("Do not disturb") + : tr(qPrintable(statusString)) + : message; + + _message = QString("%1 %2").arg(emoji, visibleStatusText); + emit fetchUserStatusFinished(); +} + +UserStatus::Status UserStatus::status() const +{ + return _status; +} + +QString UserStatus::message() const +{ + return _message; +} + +QUrl UserStatus::icon() const +{ + switch (_status) { + case Online: + return Theme::instance()->statusOnlineImageSource(); + case Away: + return Theme::instance()->statusAwayImageSource(); + case DoNotDisturb: + return Theme::instance()->statusDoNotDisturbImageSource(); + case Invisible: + case Offline: + return Theme::instance()->statusInvisibleImageSource(); + default: + return Theme::instance()->statusOnlineImageSource(); + } +} + +} // namespace OCC diff --git a/src/gui/userstatus.h b/src/gui/userstatus.h new file mode 100644 index 0000000000000..ef35eda514960 --- /dev/null +++ b/src/gui/userstatus.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) by Camila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef USERSTATUS_H +#define USERSTATUS_H + +#include +#include "accountfwd.h" + +namespace OCC { + +class JsonApiJob; + +class UserStatus : public QObject +{ + Q_OBJECT + +public: + explicit UserStatus(QObject *parent = nullptr); + enum Status { + Online, + DoNotDisturb, + Away, + Offline, + Invisible + }; + Q_ENUM(Status); + void fetchUserStatus(AccountPtr account); + Status status() const; + QString message() const; + QUrl icon() const; + +private slots: + void slotFetchUserStatusFinished(const QJsonDocument &json, const int statusCode); + +signals: + void fetchUserStatusFinished(); + +private: + Status stringToEnum(const QString &status) const; + + // it needs to match the Status enum + const QHash _preDefinedStatus{{"online", 0}, + {"dnd", 1}, //DoNotDisturb + {"away", 2}, + {"offline", 3}, + {"invisible", 4}}; + + QPointer _job; // the currently running job + Status _status{Status::Online}; + QString _message; +}; + + +} // namespace OCC + +#endif //USERSTATUS_H diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 2f980a7489300..4b991348e8f6e 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -177,6 +177,11 @@ bool Capabilities::chunkingNg() const return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0"; } +bool Capabilities::userStatus() const +{ + return _capabilities.contains("notifications") && _capabilities["notifications"].toMap().contains("user-status"); +} + PushNotificationTypes Capabilities::availablePushNotifications() const { if (!_capabilities.contains("notify_push")) { diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 06ecaf4f1c49e..11f7848756085 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -56,6 +56,7 @@ class OWNCLOUDSYNC_EXPORT Capabilities bool sharePublicLinkMultiple() const; bool shareResharing() const; bool chunkingNg() const; + bool userStatus() const; /// Returns which kind of push notfications are available PushNotificationTypes availablePushNotifications() const; diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index f701e2dc1c94d..1330ff518a976 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -871,6 +871,11 @@ bool JsonApiJob::finished() if(reply()->rawHeaderList().contains("ETag")) emit etagResponseHeaderReceived(reply()->rawHeader("ETag"), statusCode); + const auto desktopNotificationsAllowed = reply()->rawHeader(QByteArray("X-Nextcloud-User-Status")); + if(!desktopNotificationsAllowed.isEmpty()) { + emit allowDesktopNotificationsChanged(desktopNotificationsAllowed == "online"); + } + QJsonParseError error; auto json = QJsonDocument::fromJson(jsonStr.toUtf8(), &error); // empty or invalid response and status code is != 304 because jsonStr is expected to be empty diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index c9fd92fb28792..0fb0029250db8 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -420,6 +420,12 @@ public slots: * @param statusCode - the OCS status code: 100 (!) for success */ void etagResponseHeaderReceived(const QByteArray &value, int statusCode); + + /** + * @brief desktopNotificationStatusReceived - signal to report if notifications are allowed + * @param status - set desktop notifications allowed status + */ + void allowDesktopNotificationsChanged(const bool isAllowed); private: QUrlQuery _additionalParams; diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index b77a6665593e7..c94933cca7b80 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -139,6 +139,26 @@ QUrl Theme::stateOfflineImageSource() const return imagePathToUrl(themeImagePath("state-offline", 16)); } +QUrl Theme::statusOnlineImageSource() const +{ + return imagePathToUrl(themeImagePath("user-status-online", 16)); +} + +QUrl Theme::statusDoNotDisturbImageSource() const +{ + return imagePathToUrl(themeImagePath("user-status-dnd", 16)); +} + +QUrl Theme::statusAwayImageSource() const +{ + return imagePathToUrl(themeImagePath("user-status-away", 16)); +} + +QUrl Theme::statusInvisibleImageSource() const +{ + return imagePathToUrl(themeImagePath("user-status-invisible", 16)); +} + QString Theme::version() const { return MIRALL_VERSION_STRING; diff --git a/src/libsync/theme.h b/src/libsync/theme.h index a06828b1fcb40..7dcdb55bf0d56 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -42,6 +42,11 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject Q_PROPERTY(QString appName READ appName CONSTANT) Q_PROPERTY(QUrl stateOnlineImageSource READ stateOnlineImageSource CONSTANT) Q_PROPERTY(QUrl stateOfflineImageSource READ stateOfflineImageSource CONSTANT) + Q_PROPERTY(QUrl stateOnlineImageSource READ stateOnlineImageSource CONSTANT) + Q_PROPERTY(QUrl statusOnlineImageSource READ statusOnlineImageSource CONSTANT) + Q_PROPERTY(QUrl statusDoNotDisturbImageSource READ statusDoNotDisturbImageSource CONSTANT) + Q_PROPERTY(QUrl statusAwayImageSource READ statusAwayImageSource CONSTANT) + Q_PROPERTY(QUrl statusInvisibleImageSource READ statusInvisibleImageSource CONSTANT) #ifndef TOKEN_AUTH_ONLY Q_PROPERTY(QIcon folderDisabledIcon READ folderDisabledIcon CONSTANT) Q_PROPERTY(QIcon folderOfflineIcon READ folderOfflineIcon CONSTANT) @@ -122,6 +127,30 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject * @return QUrl full path to an icon */ QUrl stateOfflineImageSource() const; + + /** + * @brief Returns full path to an online user status icon + * @return QUrl full path to an icon + */ + QUrl statusOnlineImageSource() const; + + /** + * @brief Returns full path to an do not disturb user status icon + * @return QUrl full path to an icon + */ + QUrl statusDoNotDisturbImageSource() const; + + /** + * @brief Returns full path to an away user status icon + * @return QUrl full path to an icon + */ + QUrl statusAwayImageSource() const; + + /** + * @brief Returns full path to an invisible user status icon + * @return QUrl full path to an icon + */ + QUrl statusInvisibleImageSource() const; /** * @brief configFileName diff --git a/theme.qrc b/theme.qrc index 620a406027b98..4402bd2c2c970 100644 --- a/theme.qrc +++ b/theme.qrc @@ -195,5 +195,9 @@ theme/share.svg theme/reply.svg theme/magnifying-glass.svg + theme/colored/user-status-online.svg + theme/colored/user-status-invisible.svg + theme/colored/user-status-away.svg + theme/colored/user-status-dnd.svg diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 332d8133dfc61..520de312dd75b 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -33,6 +33,7 @@ QtObject { property int accountAvatarSize: (trayWindowHeaderHeight - 16) property int accountAvatarStateIndicatorSize: 16 + property int folderStateIndicatorSize: 16 property int accountLabelWidth: 128 property int accountDropDownCaretSize: 20 diff --git a/theme/colored/user-status-away.svg b/theme/colored/user-status-away.svg new file mode 100644 index 0000000000000..775961d0e48c2 --- /dev/null +++ b/theme/colored/user-status-away.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/colored/user-status-dnd.svg b/theme/colored/user-status-dnd.svg new file mode 100644 index 0000000000000..3b44ca23928e6 --- /dev/null +++ b/theme/colored/user-status-dnd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/colored/user-status-invisible.svg b/theme/colored/user-status-invisible.svg new file mode 100644 index 0000000000000..d69bd696bdb60 --- /dev/null +++ b/theme/colored/user-status-invisible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/colored/user-status-online.svg b/theme/colored/user-status-online.svg new file mode 100644 index 0000000000000..95c807a0fc375 --- /dev/null +++ b/theme/colored/user-status-online.svg @@ -0,0 +1 @@ + \ No newline at end of file