瀏覽代碼

Merge pull request #2505 from nextcloud/status-feature

Status feature.
Camila 4 年之前
父節點
當前提交
e8669ad4b6

+ 1 - 0
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

+ 33 - 0
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 *>("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;

+ 30 - 0
src/gui/accountstate.h

@@ -21,6 +21,7 @@
 #include <QPointer>
 #include "connectionvalidator.h"
 #include "creds/abstractcredentials.h"
+#include "userstatus.h"
 #include <memory>
 
 class QSettings;
@@ -161,6 +162,32 @@ public:
     ///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 @@ signals:
     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 @@ private:
      */
     AccountAppList _apps;
 
+    UserStatus *_userStatus;
+    bool _isDesktopNotificationsAllowed;
 };
 
 class AccountApp : public QObject

+ 10 - 0
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 *>(_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<AccountState *>(sender()->property(propertyAccountStateC));
+    if (account != nullptr) {
+       account->setDesktopNotificationsAllowed(isAllowed);
+    }
+}
+
 void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
 {
     iconCache.insert(sender()->property("activityId").toInt(),iconData);

+ 1 - 0
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<JsonApiJob> _notificationJob;

+ 16 - 19
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

+ 53 - 1
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<int, QByteArray> UserModel::roleNames() const
     QHash<int, QByteArray> roles;
     roles[NameRole] = "name";
     roles[ServerRole] = "server";
+    roles[StatusIconRole] = "statusIcon";
+    roles[StatusMessageRole] = "statusMessage";
     roles[AvatarRole] = "avatar";
     roles[IsCurrentUserRole] = "isCurrentUser";
     roles[IsConnectedRole] = "isConnected";

+ 11 - 0
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 @@ public:
     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 @@ signals:
     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 @@ public:
     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 @@ public:
     enum UserRoles {
         NameRole = Qt::UserRole + 1,
         ServerRole,
+        StatusIconRole,
+        StatusMessageRole,
         AvatarRole,
         IsCurrentUserRole,
         IsConnectedRole,

+ 48 - 14
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
 

+ 114 - 0
src/gui/userstatus.cpp

@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) by Camila <hello@camila.codes>
+ *
+ * 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 <QTimer>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+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<Status>(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

+ 68 - 0
src/gui/userstatus.h

@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) by Camila <hello@camila.codes>
+ *
+ * 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 <QPointer>
+#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<QString, int> _preDefinedStatus{{"online", 0},
+                                               {"dnd", 1}, //DoNotDisturb
+                                               {"away", 2},
+                                               {"offline", 3},
+                                               {"invisible", 4}};
+
+    QPointer<JsonApiJob> _job; // the currently running job
+    Status _status{Status::Online};
+    QString _message;
+};
+
+
+} // namespace OCC
+
+#endif //USERSTATUS_H

+ 5 - 0
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")) {

+ 1 - 0
src/libsync/capabilities.h

@@ -56,6 +56,7 @@ public:
     bool sharePublicLinkMultiple() const;
     bool shareResharing() const;
     bool chunkingNg() const;
+    bool userStatus() const;
 
     /// Returns which kind of push notfications are available
     PushNotificationTypes availablePushNotifications() const;

+ 5 - 0
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

+ 6 - 0
src/libsync/networkjobs.h

@@ -420,6 +420,12 @@ signals:
      * @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;

+ 20 - 0
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;

+ 29 - 0
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 @@ public:
      * @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

+ 4 - 0
theme.qrc

@@ -195,5 +195,9 @@
         <file>theme/share.svg</file>
         <file>theme/reply.svg</file>
         <file>theme/magnifying-glass.svg</file>
+        <file>theme/colored/user-status-online.svg</file>
+        <file>theme/colored/user-status-invisible.svg</file>
+        <file>theme/colored/user-status-away.svg</file>
+        <file>theme/colored/user-status-dnd.svg</file>
     </qresource>
 </RCC>

+ 1 - 0
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

+ 1 - 0
theme/colored/user-status-away.svg

@@ -0,0 +1 @@
+<svg width="24" height="24" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" fill="none"/><path d="m10.615 2.1094c-4.8491 0.68106-8.6152 4.8615-8.6152 9.8906 0 5.5 4.5 10 10 10 5.0292 0 9.2096-3.7661 9.8906-8.6152-1.4654 1.601-3.5625 2.6152-5.8906 2.6152-4.4 0-8-3.6-8-8 0-2.3281 1.0143-4.4252 2.6152-5.8906z" fill="#f4a331"/></svg>

+ 1 - 0
theme/colored/user-status-dnd.svg

@@ -0,0 +1 @@
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#ed484c"/><path d="m8 10h8c1.108 0 2 0.892 2 2s-0.892 2-2 2h-8c-1.108 0-2-0.892-2-2s0.892-2 2-2z" fill="#fdffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" style="paint-order:stroke markers fill"/></svg>

+ 1 - 0
theme/colored/user-status-invisible.svg

@@ -0,0 +1 @@
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 4a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6z" fill="#000"/></svg>

+ 1 - 0
theme/colored/user-status-online.svg

@@ -0,0 +1 @@
+<svg width="24" height="24" enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m8 16h8v-8h-8v8zm4-14c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#49B382"/></svg>