From: Dominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.com> Date: Mon, 13 Jan 2020 13:35:58 +0000 (+0100) Subject: Activity refresh in background X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~222^2^2~413^2~42 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=e4b19d0cb5f83ca566859933ce6143f5c877f4d7;p=nextcloud-desktop.git Activity refresh in background Signed-off-by: Dominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.com> --- diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b919758de..4ccf3abaa 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 index 000000000..f1594b4dd --- /dev/null +++ b/src/gui/tray/NotificationHandler.cpp @@ -0,0 +1,152 @@ +#include "NotificationHandler.h" + +#include "accountstate.h" +#include "capabilities.h" +#include "networkjobs.h" + +#include "iconjob.h" + +#include +#include + +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 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)); + _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(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(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 index 000000000..01caef428 --- /dev/null +++ b/src/gui/tray/NotificationHandler.h @@ -0,0 +1,36 @@ +#ifndef NOTIFICATIONHANDLER_H +#define NOTIFICATIONHANDLER_H + +#include + +#include "UserModel.h" + +class QJsonDocument; + +namespace OCC { + +class ServerNotificationHandler : public QObject +{ + Q_OBJECT +public: + explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr); + static QMap 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 _notificationJob; + AccountState *_accountState; +}; +} + +#endif // NOTIFICATIONHANDLER_H \ No newline at end of file diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp index f78d90d9d..2d2da8ec6 100644 --- a/src/gui/tray/UserModel.cpp +++ b/src/gui/tray/UserModel.cpp @@ -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 #include @@ -10,13 +15,18 @@ #include #include +// 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, QObject* parent) +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(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(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(); } /*-------------------------------------------------------------------------------------*/ diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h index 7ab4cf5b5..f7d5092f8 100644 --- a/src/gui/tray/UserModel.h +++ b/src/gui/tray/UserModel.h @@ -3,12 +3,14 @@ #include #include +#include #include #include #include "ActivityListModel.h" #include "accountmanager.h" #include "folderman.h" +#include 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 _timeSinceLastCheck; + + QElapsedTimer _guiLogTimer; + QSet _guiLoggedNotifications; + + // number of currently running notification requests. If non zero, + // no query for notifications is started. + int _notificationRequestsRunning; }; class UserModel : public QAbstractListModel diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index 0991304b6..7b38106bc 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -61,6 +61,7 @@ Window { trayWindow.setX( systrayBackend.calcTrayWindowX()); trayWindow.setY( systrayBackend.calcTrayWindowY()); systrayBackend.setOpened(); + userModelBackend.fetchCurrentActivityModel(); } onHideWindow: { trayWindow.hide();