Activity refresh in background
authorDominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.com>
Mon, 13 Jan 2020 13:35:58 +0000 (14:35 +0100)
committerDominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.com>
Mon, 13 Jan 2020 13:35:58 +0000 (14:35 +0100)
Signed-off-by: Dominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.com>
src/gui/CMakeLists.txt
src/gui/tray/NotificationHandler.cpp [new file with mode: 0644]
src/gui/tray/NotificationHandler.h [new file with mode: 0644]
src/gui/tray/UserModel.cpp
src/gui/tray/UserModel.h
src/gui/tray/Window.qml

index b919758deb4d121c63e709721d51e31e34ed703b..4ccf3abaaee9b497a1ee0510f2be6cef246dba11 100644 (file)
@@ -104,6 +104,7 @@ set(client_SRCS
     tray/ActivityData.cpp
     tray/ActivityListModel.cpp
     tray/UserModel.cpp
+    tray/NotificationHandler.cpp
     creds/credentialsfactory.cpp
     creds/httpcredentialsgui.cpp
     creds/oauth.cpp
diff --git a/src/gui/tray/NotificationHandler.cpp b/src/gui/tray/NotificationHandler.cpp
new file mode 100644 (file)
index 0000000..f1594b4
--- /dev/null
@@ -0,0 +1,152 @@
+#include "NotificationHandler.h"
+
+#include "accountstate.h"
+#include "capabilities.h"
+#include "networkjobs.h"
+
+#include "iconjob.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcServerNotification, "nextcloud.gui.servernotification", QtInfoMsg)
+
+const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/api/v2/notifications");
+const char propertyAccountStateC[] = "oc_account_state";
+const int successStatusCode = 200;
+const int notModifiedStatusCode = 304;
+QMap<int, QIcon> ServerNotificationHandler::iconCache;
+
+ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
+    : QObject(parent)
+    , _accountState(accountState)
+{
+}
+
+void ServerNotificationHandler::slotFetchNotifications()
+{
+    // check connectivity and credentials
+    if (!(_accountState && _accountState->isConnected() && _accountState->account() && _accountState->account()->credentials() && _accountState->account()->credentials()->ready())) {
+        deleteLater();
+        return;
+    }
+    // check if the account has notifications enabled. If the capabilities are
+    // not yet valid, its assumed that notifications are available.
+    if (_accountState->account()->capabilities().isValid()) {
+        if (!_accountState->account()->capabilities().notificationsAvailable()) {
+            qCInfo(lcServerNotification) << "Account" << _accountState->account()->displayName() << "does not have notifications enabled.";
+            deleteLater();
+            return;
+        }
+    }
+
+    // if the previous notification job has finished, start next.
+    _notificationJob = new JsonApiJob(_accountState->account(), notificationsPath, this);
+    QObject::connect(_notificationJob.data(), &JsonApiJob::jsonReceived,
+        this, &ServerNotificationHandler::slotNotificationsReceived);
+    QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived,
+        this, &ServerNotificationHandler::slotEtagResponseHeaderReceived);
+    _notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(_accountState));
+    _notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader());
+    _notificationJob->start();
+}
+
+void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode)
+{
+    if (statusCode == successStatusCode) {
+        qCWarning(lcServerNotification) << "New Notification ETag Response Header received " << value;
+        AccountState *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+        account->setNotificationsEtagResponseHeader(value);
+    }
+}
+
+void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
+{
+    QPixmap pixmap;
+    pixmap.loadFromData(iconData);
+    iconCache.insert(sender()->property("activityId").toInt(), QIcon(pixmap));
+}
+
+void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
+{
+    if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
+        qCWarning(lcServerNotification) << "Notifications failed with status code " << statusCode;
+        deleteLater();
+        return;
+    }
+
+    if (statusCode == notModifiedStatusCode) {
+        qCWarning(lcServerNotification) << "Status code " << statusCode << " Not Modified - No new notifications.";
+        deleteLater();
+        return;
+    }
+
+    auto notifies = json.object().value("ocs").toObject().value("data").toArray();
+
+    AccountState *ai = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+
+    ActivityList list;
+
+    foreach (auto element, notifies) {
+        Activity a;
+        auto json = element.toObject();
+        a._type = Activity::NotificationType;
+        a._accName = ai->account()->displayName();
+        a._id = json.value("notification_id").toInt();
+
+        //need to know, specially for remote_share
+        a._objectType = json.value("object_type").toString();
+        a._status = 0;
+
+        a._subject = json.value("subject").toString();
+        a._message = json.value("message").toString();
+
+        if (!json.value("icon").toString().isEmpty()) {
+            IconJob *iconJob = new IconJob(QUrl(json.value("icon").toString()));
+            iconJob->setProperty("activityId", a._id);
+            connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
+        }
+
+        QUrl link(json.value("link").toString());
+        if (!link.isEmpty()) {
+            if (link.host().isEmpty()) {
+                link.setScheme(ai->account()->url().scheme());
+                link.setHost(ai->account()->url().host());
+            }
+            if (link.port() == -1) {
+                link.setPort(ai->account()->url().port());
+            }
+        }
+        a._link = link;
+        a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
+
+        auto actions = json.value("actions").toArray();
+        foreach (auto action, actions) {
+            auto actionJson = action.toObject();
+            ActivityLink al;
+            al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
+            al._link = actionJson.value("link").toString();
+            al._verb = actionJson.value("type").toString().toUtf8();
+            al._isPrimary = actionJson.value("primary").toBool();
+
+            a._links.append(al);
+        }
+
+        // Add another action to dismiss notification on server
+        // https://github.com/owncloud/notifications/blob/master/docs/ocs-endpoint-v1.md#deleting-a-notification-for-a-user
+        ActivityLink al;
+        al._label = tr("Dismiss");
+        al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
+        al._verb = "DELETE";
+        al._isPrimary = false;
+        a._links.append(al);
+
+        list.append(a);
+    }
+    emit newNotificationList(list);
+
+    deleteLater();
+}
+}
\ No newline at end of file
diff --git a/src/gui/tray/NotificationHandler.h b/src/gui/tray/NotificationHandler.h
new file mode 100644 (file)
index 0000000..01caef4
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef NOTIFICATIONHANDLER_H
+#define NOTIFICATIONHANDLER_H
+
+#include <QtCore>
+
+#include "UserModel.h"
+
+class QJsonDocument;
+
+namespace OCC {
+
+class ServerNotificationHandler : public QObject
+{
+    Q_OBJECT
+public:
+    explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
+    static QMap<int, QIcon> iconCache;
+
+signals:
+    void newNotificationList(ActivityList);
+
+public slots:
+    void slotFetchNotifications();
+
+private slots:
+    void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
+    void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
+    void slotIconDownloaded(QByteArray iconData);
+
+private:
+    QPointer<JsonApiJob> _notificationJob;
+    AccountState *_accountState;
+};
+}
+
+#endif // NOTIFICATIONHANDLER_H
\ No newline at end of file
index f78d90d9dea382bc2d3acf526e0fdb36600ea401..2d2da8ec61bbf2602773da52b762904d1e2d3da2 100644 (file)
@@ -1,7 +1,12 @@
+#include "NotificationHandler.h"
+#include "UserModel.h"
+
 #include "accountmanager.h"
 #include "owncloudgui.h"
-#include "UserModel.h"
 #include "syncengine.h"
+#include "ocsjob.h"
+#include "configfile.h"
+#include "notificationconfirmjob.h"
 
 #include <QDesktopServices>
 #include <QIcon>
 #include <QPainter>
 #include <QPushButton>
 
+// time span in milliseconds which has to be between two
+// refreshes of the notifications
+#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
+
 namespace OCC {
 
-User::User(AccountStatePtr &account, const bool &isCurrent, QObjectparent)
+User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
     : QObject(parent)
     , _account(account)
     , _isCurrentUser(isCurrent)
     , _activityModel(new ActivityListModel(_account.data()))
+    , _notificationRequestsRunning(0)
 {
     connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
         this, &User::slotProgressInfo);
@@ -24,6 +34,183 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject* parent)
         this, &User::slotItemCompleted);
     connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
         this, &User::slotAddError);
+
+    connect(&_notificationCheckTimer, &QTimer::timeout,
+        this, &User::slotRefresh);
+
+    connect(_account.data(), &AccountState::stateChanged,
+            [=]() { if (isConnected()) {slotRefresh();} });
+}
+
+void User::slotBuildNotificationDisplay(const ActivityList &list)
+{
+    // Whether a new notification was added to the list
+    bool newNotificationShown = false;
+
+    _activityModel->clearNotifications();
+
+    foreach (auto activity, list) {
+        if (_blacklistedNotifications.contains(activity)) {
+            qCInfo(lcActivity) << "Activity in blacklist, skip";
+            continue;
+        }
+
+        // handle gui logs. In order to NOT annoy the user with every fetching of the
+        // notifications the notification id is stored in a Set. Only if an id
+        // is not in the set, it qualifies for guiLog.
+        // Important: The _guiLoggedNotifications set must be wiped regularly which
+        // will repeat the gui log.
+
+        // after one hour, clear the gui log notification store
+        if (_guiLogTimer.elapsed() > 60 * 60 * 1000) {
+            _guiLoggedNotifications.clear();
+        }
+
+        if (!_guiLoggedNotifications.contains(activity._id)) {
+            newNotificationShown = true;
+            _guiLoggedNotifications.insert(activity._id);
+
+            // Assemble a tray notification for the NEW notification
+            ConfigFile cfg;
+            if (cfg.optionalServerNotifications()) {
+                if (AccountManager::instance()->accounts().count() == 1) {
+                    emit guiLog(activity._subject, "");
+                } else {
+                    emit guiLog(activity._subject, activity._accName);
+                }
+            }
+        }
+
+        _activityModel->addNotificationToActivityList(activity);
+    }
+
+    // restart the gui log timer now that we show a new notification
+    if (newNotificationShown) {
+        _guiLogTimer.start();
+    }
+}
+
+void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
+{
+    qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
+    _notificationCheckTimer.start(interval.count());
+}
+
+void User::slotRefresh()
+{
+    // QElapsedTimer isn't actually constructed as invalid.
+    if (!_timeSinceLastCheck.contains(_account.data())) {
+        _timeSinceLastCheck[_account.data()].invalidate();
+    }
+    QElapsedTimer &timer = _timeSinceLastCheck[_account.data()];
+
+    // Fetch Activities only if visible and if last check is longer than 15 secs ago
+    if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
+        qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
+        return;
+    }
+    if (_account.data() && _account.data()->isConnected()) {
+        if (!timer.isValid()) {
+            this->slotRefreshActivities();
+        }
+        this->slotRefreshNotifications();
+        timer.start();
+    }
+}
+
+void User::slotRefreshActivities()
+{
+    _activityModel->slotRefreshActivity();
+}
+
+void User::slotRefreshNotifications()
+{
+    // start a server notification handler if no notification requests
+    // are running
+    if (_notificationRequestsRunning == 0) {
+        ServerNotificationHandler *snh = new ServerNotificationHandler(_account.data());
+        connect(snh, &ServerNotificationHandler::newNotificationList,
+            this, &User::slotBuildNotificationDisplay);
+
+        snh->slotFetchNotifications();
+    } else {
+        qCWarning(lcActivity) << "Notification request counter not zero.";
+    }
+}
+
+void User::slotNotificationRequestFinished(int statusCode)
+{
+    int row = sender()->property("activityRow").toInt();
+
+    // the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
+    if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
+        qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
+    } else {
+        // to do use the model to rebuild the list or remove the item
+        qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
+        _activityModel->removeActivityFromActivityList(row);
+    }
+}
+
+void User::slotEndNotificationRequest(int replyCode)
+{
+    _notificationRequestsRunning--;
+    slotNotificationRequestFinished(replyCode);
+}
+
+void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
+{
+    qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
+
+    const QStringList validVerbs = QStringList() << "GET"
+                                                 << "PUT"
+                                                 << "POST"
+                                                 << "DELETE";
+
+    if (validVerbs.contains(verb)) {
+        AccountStatePtr acc = AccountManager::instance()->account(accountName);
+        if (acc) {
+            NotificationConfirmJob *job = new NotificationConfirmJob(acc->account());
+            QUrl l(link);
+            job->setLinkAndVerb(l, verb);
+            job->setProperty("activityRow", QVariant::fromValue(row));
+            connect(job, &AbstractNetworkJob::networkError,
+                this, &User::slotNotifyNetworkError);
+            connect(job, &NotificationConfirmJob::jobFinished,
+                this, &User::slotNotifyServerFinished);
+            job->start();
+
+            // count the number of running notification requests. If this member var
+            // is larger than zero, no new fetching of notifications is started
+            _notificationRequestsRunning++;
+        }
+    } else {
+        qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
+    }
+}
+
+void User::slotNotifyNetworkError(QNetworkReply *reply)
+{
+    NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
+    if (!job) {
+        return;
+    }
+
+    int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+    slotEndNotificationRequest(resultCode);
+    qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
+}
+
+void User::slotNotifyServerFinished(const QString &reply, int replyCode)
+{
+    NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
+    if (!job) {
+        return;
+    }
+
+    slotEndNotificationRequest(replyCode);
+    qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
 }
 
 void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
@@ -390,6 +577,8 @@ void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
             _currentUserId = _users.indexOf(_users.last());
         }
         endInsertRows();
+        ConfigFile cfg;
+        _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
     }
 }
 
@@ -430,7 +619,8 @@ Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
     emit newUserSelected();
 }
 
-Q_INVOKABLE void UserModel::login(const int &id) {
+Q_INVOKABLE void UserModel::login(const int &id)
+{
     _users[id]->login();
     emit refreshCurrentUserGui();
 }
@@ -524,9 +714,7 @@ bool UserModel::currentUserHasActivities()
 
 void UserModel::fetchCurrentActivityModel()
 {
-    if (_users[currentUserId()]->isConnected()) {
-        _users[currentUserId()]->getActivityModel()->fetchMore(QModelIndex());
-    }
+    _users[currentUserId()]->slotRefresh();
 }
 
 /*-------------------------------------------------------------------------------------*/
index 7ab4cf5b503c676e6d5a1f938bcec7bbd59e3ba0..f7d5092f8b9d87ddfabe3704daa1a416bb776248 100644 (file)
@@ -3,12 +3,14 @@
 
 #include <QAbstractListModel>
 #include <QImage>
+#include <QDateTime>
 #include <QStringList>
 #include <QQuickImageProvider>
 
 #include "ActivityListModel.h"
 #include "accountmanager.h"
 #include "folderman.h"
+#include <chrono>
 
 namespace OCC {
 
@@ -36,15 +38,39 @@ public:
     void logout() const;
     void removeAccount() const;
 
+signals:
+    void guiLog(const QString &, const QString &);
+
 public slots:
     void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
     void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
     void slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category);
+    void slotNotificationRequestFinished(int statusCode);
+    void slotNotifyNetworkError(QNetworkReply *reply);
+    void slotEndNotificationRequest(int replyCode);
+    void slotNotifyServerFinished(const QString &reply, int replyCode);
+    void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
+    void slotBuildNotificationDisplay(const ActivityList &list);
+    void slotRefreshNotifications();
+    void slotRefreshActivities();
+    void slotRefresh();
+    void setNotificationRefreshInterval(std::chrono::milliseconds interval);
 
 private:
     AccountStatePtr _account;
     bool _isCurrentUser;
     ActivityListModel *_activityModel;
+    ActivityList _blacklistedNotifications;
+
+    QTimer _notificationCheckTimer;
+    QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
+
+    QElapsedTimer _guiLogTimer;
+    QSet<int> _guiLoggedNotifications;
+
+    // number of currently running notification requests. If non zero,
+    // no query for notifications is started.
+    int _notificationRequestsRunning;
 };
 
 class UserModel : public QAbstractListModel
index 0991304b6c1869e86e53b081a0ac2921572bcec2..7b38106bc5baaabd10e463f6f84565e1e57656c2 100644 (file)
@@ -61,6 +61,7 @@ Window {
             trayWindow.setX( systrayBackend.calcTrayWindowX());\r
             trayWindow.setY( systrayBackend.calcTrayWindowY());\r
             systrayBackend.setOpened();\r
+            userModelBackend.fetchCurrentActivityModel();\r
         }\r
         onHideWindow: {\r
             trayWindow.hide();\r