tray/ActivityData.cpp
tray/ActivityListModel.cpp
tray/UserModel.cpp
+ tray/NotificationHandler.cpp
creds/credentialsfactory.cpp
creds/httpcredentialsgui.cpp
creds/oauth.cpp
--- /dev/null
+#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
--- /dev/null
+#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
+#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, 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);
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)
_currentUserId = _users.indexOf(_users.last());
}
endInsertRows();
+ ConfigFile cfg;
+ _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
}
}
emit newUserSelected();
}
-Q_INVOKABLE void UserModel::login(const int &id) {
+Q_INVOKABLE void UserModel::login(const int &id)
+{
_users[id]->login();
emit refreshCurrentUserGui();
}
void UserModel::fetchCurrentActivityModel()
{
- if (_users[currentUserId()]->isConnected()) {
- _users[currentUserId()]->getActivityModel()->fetchMore(QModelIndex());
- }
+ _users[currentUserId()]->slotRefresh();
}
/*-------------------------------------------------------------------------------------*/
#include <QAbstractListModel>
#include <QImage>
+#include <QDateTime>
#include <QStringList>
#include <QQuickImageProvider>
#include "ActivityListModel.h"
#include "accountmanager.h"
#include "folderman.h"
+#include <chrono>
namespace OCC {
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
trayWindow.setX( systrayBackend.calcTrayWindowX());\r
trayWindow.setY( systrayBackend.calcTrayWindowY());\r
systrayBackend.setOpened();\r
+ userModelBackend.fetchCurrentActivityModel();\r
}\r
onHideWindow: {\r
trayWindow.hide();\r