Merge branch 'notifications'

This commit is contained in:
Klaas Freitag 2016-04-04 10:40:33 +02:00
commit 6b0d535120
30 changed files with 1559 additions and 318 deletions

View file

@ -22,5 +22,6 @@
<file>resources/account.png</file> <file>resources/account.png</file>
<file>resources/more.png</file> <file>resources/more.png</file>
<file>resources/delete.png</file> <file>resources/delete.png</file>
<file>resources/bell.png</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -25,3 +25,6 @@ You can change the following configuration settings (must be under the ``[ownClo
- ``chunkSize`` (default: ``5242880``) -- Specifies the chunk size of uploaded files in bytes. - ``chunkSize`` (default: ``5242880``) -- Specifies the chunk size of uploaded files in bytes.
- ``promptDeleteAllFiles`` (default: ``true``) -- If a UI prompt should ask for confirmation if it was detected that all files and folders were deleted. - ``promptDeleteAllFiles`` (default: ``true``) -- If a UI prompt should ask for confirmation if it was detected that all files and folders were deleted.
- ``notificationRefreshInterval`` (default``300,000``) -- Specifies the default interval of checking for new server notifications in milliseconds.

BIN
resources/bell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

View file

@ -32,6 +32,7 @@ set(client_UI
owncloudsetuppage.ui owncloudsetuppage.ui
addcertificatedialog.ui addcertificatedialog.ui
proxyauthdialog.ui proxyauthdialog.ui
notificationwidget.ui
wizard/owncloudadvancedsetuppage.ui wizard/owncloudadvancedsetuppage.ui
wizard/owncloudconnectionmethoddialog.ui wizard/owncloudconnectionmethoddialog.ui
wizard/owncloudhttpcredspage.ui wizard/owncloudhttpcredspage.ui
@ -62,6 +63,8 @@ set(client_SRCS
owncloudgui.cpp owncloudgui.cpp
owncloudsetupwizard.cpp owncloudsetupwizard.cpp
protocolwidget.cpp protocolwidget.cpp
activitydata.cpp
activitylistmodel.cpp
activitywidget.cpp activitywidget.cpp
activityitemdelegate.cpp activityitemdelegate.cpp
selectivesyncdialog.cpp selectivesyncdialog.cpp
@ -85,6 +88,9 @@ set(client_SRCS
proxyauthdialog.cpp proxyauthdialog.cpp
synclogdialog.cpp synclogdialog.cpp
tooltipupdater.cpp tooltipupdater.cpp
notificationwidget.cpp
notificationconfirmjob.cpp
servernotificationhandler.cpp
creds/credentialsfactory.cpp creds/credentialsfactory.cpp
creds/httpcredentialsgui.cpp creds/httpcredentialsgui.cpp
creds/shibbolethcredentials.cpp creds/shibbolethcredentials.cpp

35
src/gui/activitydata.cpp Normal file
View file

@ -0,0 +1,35 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 <QtCore>
#include "activitydata.h"
namespace OCC
{
bool operator<( const Activity& rhs, const Activity& lhs ) {
return rhs._dateTime.toMSecsSinceEpoch() > lhs._dateTime.toMSecsSinceEpoch();
}
bool operator==( const Activity& rhs, const Activity& lhs ) {
return (rhs._type == lhs._type && rhs._id== lhs._id && rhs._accName == lhs._accName);
}
Activity::Identifier Activity::ident() const {
return Identifier( _id, _accName );
}
}

89
src/gui/activitydata.h Normal file
View file

@ -0,0 +1,89 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 ACTIVITYDATA_H
#define ACTIVITYDATA_H
#include <QtCore>
namespace OCC {
/**
* @brief The ActivityLink class describes actions of an activity
*
* These are part of notifications which are mapped into activities.
*/
class ActivityLink
{
public:
QString _label;
QString _link;
QByteArray _verb;
bool _isPrimary;
};
/* ==================================================================== */
/**
* @brief Activity Structure
* @ingroup gui
*
* contains all the information describing a single activity.
*/
class Activity
{
public:
typedef QPair<qlonglong, QString> Identifier;
enum Type {
ActivityType,
NotificationType
};
Type _type;
qlonglong _id;
QString _subject;
QString _message;
QString _file;
QUrl _link;
QDateTime _dateTime;
QString _accName;
QVector <ActivityLink> _links;
/**
* @brief Sort operator to sort the list youngest first.
* @param val
* @return
*/
Identifier ident() const;
};
bool operator==( const Activity& rhs, const Activity& lhs );
bool operator<( const Activity& rhs, const Activity& lhs );
/* ==================================================================== */
/**
* @brief The ActivityList
* @ingroup gui
*
* A QList based list of Activities
*/
typedef QList<Activity> ActivityList;
}
#endif // ACTIVITYDATA_H

View file

@ -0,0 +1,228 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 <QtCore>
#include <QAbstractListModel>
#include <QWidget>
#include <QIcon>
#include "account.h"
#include "accountstate.h"
#include "accountmanager.h"
#include "folderman.h"
#include "accessmanager.h"
#include "activityitemdelegate.h"
#include "activitydata.h"
#include "activitylistmodel.h"
namespace OCC {
ActivityListModel::ActivityListModel(QWidget *parent)
:QAbstractListModel(parent)
{
}
QVariant ActivityListModel::data(const QModelIndex &index, int role) const
{
Activity a;
if (!index.isValid())
return QVariant();
a = _finalList.at(index.row());
AccountStatePtr ast = AccountManager::instance()->account(a._accName);
QStringList list;
switch (role) {
case ActivityItemDelegate::PathRole:
list = FolderMan::instance()->findFileInLocalFolders(a._file, ast->account());
if( list.count() > 0 ) {
return QVariant(list.at(0));
}
// File does not exist anymore? Let's try to open its path
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(a._file).path(), ast->account());
if( list.count() > 0 ) {
return QVariant(list.at(0));
}
return QVariant();
break;
case ActivityItemDelegate::ActionIconRole:
return QVariant(); // FIXME once the action can be quantified, display on Icon
break;
case ActivityItemDelegate::UserIconRole:
return QIcon(QLatin1String(":/client/resources/account.png"));
break;
case Qt::ToolTipRole:
case ActivityItemDelegate::ActionTextRole:
return a._subject;
break;
case ActivityItemDelegate::LinkRole:
return a._link;
break;
case ActivityItemDelegate::AccountRole:
return a._accName;
break;
case ActivityItemDelegate::PointInTimeRole:
return Utility::timeAgoInWords(a._dateTime);
break;
case ActivityItemDelegate::AccountConnectedRole:
return (ast && ast->isConnected());
break;
default:
return QVariant();
}
return QVariant();
}
int ActivityListModel::rowCount(const QModelIndex&) const
{
return _finalList.count();
}
// current strategy: Fetch 100 items per Account
// ATTENTION: This method is const and thus it is not possible to modify
// the _activityLists hash or so. Doesn't make it easier...
bool ActivityListModel::canFetchMore(const QModelIndex& ) const
{
if( _activityLists.count() == 0 ) return true;
for(auto i = _activityLists.begin() ; i != _activityLists.end(); ++i) {
AccountState *ast = i.key();
if( ast && ast->isConnected() ) {
ActivityList activities = i.value();
if( activities.count() == 0 &&
! _currentlyFetching.contains(ast) ) {
return true;
}
}
}
return false;
}
void ActivityListModel::startFetchJob(AccountState* s)
{
if( !s->isConnected() ) {
return;
}
JsonApiJob *job = new JsonApiJob(s->account(), QLatin1String("ocs/v1.php/cloud/activity"), this);
QObject::connect(job, SIGNAL(jsonReceived(QVariantMap, int)),
this, SLOT(slotActivitiesReceived(QVariantMap, int)));
job->setProperty("AccountStatePtr", QVariant::fromValue<AccountState*>(s));
QList< QPair<QString,QString> > params;
params.append(qMakePair(QString::fromLatin1("page"), QString::fromLatin1("0")));
params.append(qMakePair(QString::fromLatin1("pagesize"), QString::fromLatin1("100")));
job->addQueryParams(params);
_currentlyFetching.insert(s);
qDebug() << Q_FUNC_INFO << "Start fetching activities for " << s->account()->displayName();
job->start();
}
void ActivityListModel::slotActivitiesReceived(const QVariantMap& json, int statusCode)
{
auto activities = json.value("ocs").toMap().value("data").toList();
ActivityList list;
AccountState* ast = qvariant_cast<AccountState*>(sender()->property("AccountStatePtr"));
_currentlyFetching.remove(ast);
foreach( auto activ, activities ) {
auto json = activ.toMap();
Activity a;
a._type = Activity::ActivityType;
a._accName = ast->account()->displayName();
a._id = json.value("id").toLongLong();
a._subject = json.value("subject").toString();
a._message = json.value("message").toString();
a._file = json.value("file").toString();
a._link = json.value("link").toUrl();
a._dateTime = json.value("date").toDateTime();
list.append(a);
}
_activityLists[ast] = list;
emit activityJobStatusCode(ast, statusCode);
combineActivityLists();
}
void ActivityListModel::combineActivityLists()
{
ActivityList resultList;
foreach( ActivityList list, _activityLists.values() ) {
resultList.append(list);
}
std::sort( resultList.begin(), resultList.end() );
beginResetModel();
_finalList.clear();
endResetModel();
beginInsertRows(QModelIndex(), 0, resultList.count());
_finalList = resultList;
endInsertRows();
}
void ActivityListModel::fetchMore(const QModelIndex &)
{
QList<AccountStatePtr> accounts = AccountManager::instance()->accounts();
foreach (const AccountStatePtr& asp, accounts) {
if( !_activityLists.contains(asp.data()) && asp->isConnected() ) {
_activityLists[asp.data()] = ActivityList();
startFetchJob(asp.data());
}
}
}
void ActivityListModel::slotRefreshActivity(AccountState *ast)
{
if(ast && _activityLists.contains(ast)) {
_activityLists.remove(ast);
}
startFetchJob(ast);
}
void ActivityListModel::slotRemoveAccount(AccountState *ast )
{
if( _activityLists.contains(ast) ) {
int i = 0;
const QString accountToRemove = ast->account()->displayName();
QMutableListIterator<Activity> it(_finalList);
while (it.hasNext()) {
Activity activity = it.next();
if( activity._accName == accountToRemove ) {
beginRemoveRows(QModelIndex(), i, i+1);
it.remove();
endRemoveRows();
}
}
_activityLists.remove(ast);
_currentlyFetching.remove(ast);
}
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 ACTIVITYLISTMODEL_H
#define ACTIVITYLISTMODEL_H
#include <QtCore>
#include "activitydata.h"
namespace OCC {
class AccountState;
/**
* @brief The ActivityListModel
* @ingroup gui
*
* Simple list model to provide the list view with data.
*/
class ActivityListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit ActivityListModel(QWidget *parent=0);
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex& parent = QModelIndex()) const Q_DECL_OVERRIDE;
bool canFetchMore(const QModelIndex& ) const Q_DECL_OVERRIDE;
void fetchMore(const QModelIndex&) Q_DECL_OVERRIDE;
ActivityList activityList() { return _finalList; }
public slots:
void slotRefreshActivity(AccountState* ast);
void slotRemoveAccount( AccountState *ast );
private slots:
void slotActivitiesReceived(const QVariantMap& json, int statusCode);
signals:
void activityJobStatusCode(AccountState* ast, int statusCode);
private:
void startFetchJob(AccountState* s);
void combineActivityLists();
QMap<AccountState*, ActivityList> _activityLists;
ActivityList _finalList;
QSet<AccountState*> _currentlyFetching;
};
}
#endif // ACTIVITYLISTMODEL_H

View file

@ -16,8 +16,8 @@
#include <QtWidgets> #include <QtWidgets>
#endif #endif
#include "activitylistmodel.h"
#include "activitywidget.h" #include "activitywidget.h"
#include "configfile.h"
#include "syncresult.h" #include "syncresult.h"
#include "logger.h" #include "logger.h"
#include "utility.h" #include "utility.h"
@ -33,234 +33,28 @@
#include "activityitemdelegate.h" #include "activityitemdelegate.h"
#include "protocolwidget.h" #include "protocolwidget.h"
#include "QProgressIndicator.h" #include "QProgressIndicator.h"
#include "notificationwidget.h"
#include "notificationconfirmjob.h"
#include "servernotificationhandler.h"
#include "theme.h"
#include "ocsjob.h"
#include "ui_activitywidget.h" #include "ui_activitywidget.h"
#include <climits> #include <climits>
// time span in milliseconds which has to be between two
// refreshes of the notifications
#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
namespace OCC { namespace OCC {
void ActivityList::setAccountName( const QString& name )
{
_accountName = name;
}
QString ActivityList::accountName() const
{
return _accountName;
}
/* ==================================================================== */
ActivityListModel::ActivityListModel(QWidget *parent)
:QAbstractListModel(parent)
{
}
QVariant ActivityListModel::data(const QModelIndex &index, int role) const
{
Activity a;
if (!index.isValid())
return QVariant();
a = _finalList.at(index.row());
AccountStatePtr ast = AccountManager::instance()->account(a._accName);
QStringList list;
if (role == Qt::EditRole)
return QVariant();
switch (role) {
case ActivityItemDelegate::PathRole:
list = FolderMan::instance()->findFileInLocalFolders(a._file, ast->account());
if( list.count() > 0 ) {
return QVariant(list.at(0));
}
// File does not exist anymore? Let's try to open its path
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(a._file).path(), ast->account());
if( list.count() > 0 ) {
return QVariant(list.at(0));
}
return QVariant();
break;
case ActivityItemDelegate::ActionIconRole:
return QVariant(); // FIXME once the action can be quantified, display on Icon
break;
case ActivityItemDelegate::UserIconRole:
return QIcon(QLatin1String(":/client/resources/account.png"));
break;
case Qt::ToolTipRole:
case ActivityItemDelegate::ActionTextRole:
return a._subject;
break;
case ActivityItemDelegate::LinkRole:
return a._link;
break;
case ActivityItemDelegate::AccountRole:
return a._accName;
break;
case ActivityItemDelegate::PointInTimeRole:
return Utility::timeAgoInWords(a._dateTime);
break;
case ActivityItemDelegate::AccountConnectedRole:
return (ast && ast->isConnected());
break;
default:
return QVariant();
}
return QVariant();
}
int ActivityListModel::rowCount(const QModelIndex&) const
{
return _finalList.count();
}
// current strategy: Fetch 100 items per Account
// ATTENTION: This method is const and thus it is not possible to modify
// the _activityLists hash or so. Doesn't make it easier...
bool ActivityListModel::canFetchMore(const QModelIndex& ) const
{
if( _activityLists.count() == 0 ) return true;
QMap<AccountState*, ActivityList>::const_iterator i = _activityLists.begin();
while (i != _activityLists.end()) {
AccountState *ast = i.key();
if( ast && ast->isConnected() ) {
ActivityList activities = i.value();
if( activities.count() == 0 &&
! _currentlyFetching.contains(ast) ) {
return true;
}
}
++i;
}
return false;
}
void ActivityListModel::startFetchJob(AccountState* s)
{
if( !s->isConnected() ) {
return;
}
JsonApiJob *job = new JsonApiJob(s->account(), QLatin1String("ocs/v1.php/cloud/activity"), this);
QObject::connect(job, SIGNAL(jsonReceived(QVariantMap, int)),
this, SLOT(slotActivitiesReceived(QVariantMap, int)));
job->setProperty("AccountStatePtr", QVariant::fromValue<AccountState*>(s));
QList< QPair<QString,QString> > params;
params.append(qMakePair(QString::fromLatin1("page"), QString::fromLatin1("0")));
params.append(qMakePair(QString::fromLatin1("pagesize"), QString::fromLatin1("100")));
job->addQueryParams(params);
_currentlyFetching.insert(s);
qDebug() << "Start fetching activities for " << s->account()->displayName();
job->start();
}
void ActivityListModel::slotActivitiesReceived(const QVariantMap& json, int statusCode)
{
auto activities = json.value("ocs").toMap().value("data").toList();
qDebug() << "*** activities" << activities;
ActivityList list;
AccountState* ast = qvariant_cast<AccountState*>(sender()->property("AccountStatePtr"));
_currentlyFetching.remove(ast);
list.setAccountName( ast->account()->displayName());
foreach( auto activ, activities ) {
auto json = activ.toMap();
Activity a;
a._accName = ast->account()->displayName();
a._id = json.value("id").toLongLong();
a._subject = json.value("subject").toString();
a._message = json.value("message").toString();
a._file = json.value("file").toString();
a._link = json.value("link").toUrl();
a._dateTime = json.value("date").toDateTime();
list.append(a);
}
_activityLists[ast] = list;
emit activityJobStatusCode(ast, statusCode);
combineActivityLists();
}
void ActivityListModel::combineActivityLists()
{
ActivityList resultList;
foreach( ActivityList list, _activityLists.values() ) {
resultList.append(list);
}
std::sort( resultList.begin(), resultList.end() );
beginInsertRows(QModelIndex(), 0, resultList.count()-1);
_finalList = resultList;
endInsertRows();
}
void ActivityListModel::fetchMore(const QModelIndex &)
{
QList<AccountStatePtr> accounts = AccountManager::instance()->accounts();
foreach (AccountStatePtr asp, accounts) {
bool newItem = false;
if( !_activityLists.contains(asp.data()) && asp->isConnected() ) {
_activityLists[asp.data()] = ActivityList();
newItem = true;
}
if( newItem ) {
startFetchJob(asp.data());
}
}
}
void ActivityListModel::slotRefreshActivity(AccountState *ast)
{
if(ast && _activityLists.contains(ast)) {
qDebug() << "**** Refreshing Activity list for" << ast->account()->displayName();
_activityLists.remove(ast);
}
startFetchJob(ast);
}
void ActivityListModel::slotRemoveAccount(AccountState *ast )
{
if( _activityLists.contains(ast) ) {
int i = 0;
const QString accountToRemove = ast->account()->displayName();
QMutableListIterator<Activity> it(_finalList);
while (it.hasNext()) {
Activity activity = it.next();
if( activity._accName == accountToRemove ) {
beginRemoveRows(QModelIndex(), i, i+1);
it.remove();
endRemoveRows();
}
}
_activityLists.remove(ast);
_currentlyFetching.remove(ast);
}
}
/* ==================================================================== */ /* ==================================================================== */
ActivityWidget::ActivityWidget(QWidget *parent) : ActivityWidget::ActivityWidget(QWidget *parent) :
QWidget(parent), QWidget(parent),
_ui(new Ui::ActivityWidget) _ui(new Ui::ActivityWidget),
_notificationRequestsRunning(0)
{ {
_ui->setupUi(this); _ui->setupUi(this);
@ -276,6 +70,16 @@ ActivityWidget::ActivityWidget(QWidget *parent) :
_ui->_activityList->setAlternatingRowColors(true); _ui->_activityList->setAlternatingRowColors(true);
_ui->_activityList->setModel(_model); _ui->_activityList->setModel(_model);
_ui->_notifyLabel->hide();
_ui->_notifyScroll->hide();
// Create a widget container for the notifications. The ui file defines
// a scroll area that get a widget with a layout as children
QWidget *w = new QWidget(this);
_notificationsLayout = new QVBoxLayout(this);
w->setLayout(_notificationsLayout);
_ui->_notifyScroll->setWidget(w);
showLabels(); showLabels();
connect(_model, SIGNAL(activityJobStatusCode(AccountState*,int)), connect(_model, SIGNAL(activityJobStatusCode(AccountState*,int)),
@ -289,6 +93,9 @@ ActivityWidget::ActivityWidget(QWidget *parent) :
connect( _ui->_activityList, SIGNAL(activated(QModelIndex)), this, connect( _ui->_activityList, SIGNAL(activated(QModelIndex)), this,
SLOT(slotOpenFile(QModelIndex))); SLOT(slotOpenFile(QModelIndex)));
connect( &_removeTimer, SIGNAL(timeout()), this, SLOT(slotCheckToCleanWidgets()) );
_removeTimer.setInterval(1000);
} }
ActivityWidget::~ActivityWidget() ActivityWidget::~ActivityWidget()
@ -296,11 +103,26 @@ ActivityWidget::~ActivityWidget()
delete _ui; delete _ui;
} }
void ActivityWidget::slotRefresh(AccountState *ptr) void ActivityWidget::slotRefreshActivities(AccountState *ptr)
{ {
_model->slotRefreshActivity(ptr); _model->slotRefreshActivity(ptr);
} }
void ActivityWidget::slotRefreshNotifications(AccountState *ptr)
{
// start a server notification handler if no notification requests
// are running
if( _notificationRequestsRunning == 0 ) {
ServerNotificationHandler *snh = new ServerNotificationHandler;
connect(snh, SIGNAL(newNotificationList(ActivityList)), this,
SLOT(slotBuildNotificationDisplay(ActivityList)));
snh->slotFetchNotifications(ptr);
} else {
qDebug() << Q_FUNC_INFO << "========> notification request counter not zero.";
}
}
void ActivityWidget::slotRemoveAccount( AccountState *ptr ) void ActivityWidget::slotRemoveAccount( AccountState *ptr )
{ {
_model->slotRemoveAccount(ptr); _model->slotRemoveAccount(ptr);
@ -312,6 +134,8 @@ void ActivityWidget::showLabels()
_ui->_headerLabel->setTextFormat(Qt::RichText); _ui->_headerLabel->setTextFormat(Qt::RichText);
_ui->_headerLabel->setText(t); _ui->_headerLabel->setText(t);
_ui->_notifyLabel->setText(tr("Action Required: Notifications"));
t.clear(); t.clear();
QSetIterator<QString> i(_accountsWithoutActivities); QSetIterator<QString> i(_accountsWithoutActivities);
while (i.hasNext() ) { while (i.hasNext() ) {
@ -389,7 +213,7 @@ void ActivityWidget::storeActivityList( QTextStream& ts )
void ActivityWidget::slotOpenFile(QModelIndex indx) void ActivityWidget::slotOpenFile(QModelIndex indx)
{ {
qDebug() << indx.isValid() << indx.data(ActivityItemDelegate::PathRole).toString() << QFile::exists(indx.data(ActivityItemDelegate::PathRole).toString()); qDebug() << Q_FUNC_INFO << indx.isValid() << indx.data(ActivityItemDelegate::PathRole).toString() << QFile::exists(indx.data(ActivityItemDelegate::PathRole).toString());
if( indx.isValid() ) { if( indx.isValid() ) {
QString fullPath = indx.data(ActivityItemDelegate::PathRole).toString(); QString fullPath = indx.data(ActivityItemDelegate::PathRole).toString();
@ -399,6 +223,264 @@ void ActivityWidget::slotOpenFile(QModelIndex indx)
} }
} }
// GUI: Display the notifications.
// All notifications in list are coming from the same account
// but in the _widgetForNotifId hash widgets for all accounts are
// collected.
void ActivityWidget::slotBuildNotificationDisplay(const ActivityList& list)
{
QHash<QString, int> accNotified;
QString listAccountName;
foreach( auto activity, list ) {
if( _blacklistedNotifications.contains(activity)) {
qDebug() << Q_FUNC_INFO << "Activity in blacklist, skip";
continue;
}
NotificationWidget *widget = 0;
if( _widgetForNotifId.contains( activity.ident()) ) {
widget = _widgetForNotifId[activity.ident()];
} else {
widget = new NotificationWidget(this);
connect(widget, SIGNAL(sendNotificationRequest(QString, QString, QByteArray)),
this, SLOT(slotSendNotificationRequest(QString, QString, QByteArray)));
connect(widget, SIGNAL(requestCleanupAndBlacklist(Activity)),
this, SLOT(slotRequestCleanupAndBlacklist(Activity)));
_notificationsLayout->addWidget(widget);
// _ui->_notifyScroll->setMinimumHeight( widget->height());
_ui->_notifyScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
_widgetForNotifId[activity.ident()] = widget;
}
widget->setActivity( activity );
// remember the list account name for the strayCat handling below.
listAccountName = activity._accName;
// 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)) {
QString host = activity._accName;
// store the name of the account that sends the notification to be
// able to add it to the tray notification
// remove the user name from the account as that is not accurate here.
int indx = host.indexOf(QChar('@'));
if( indx>-1 ) {
host.remove(0, 1+indx);
}
if( !host.isEmpty() ) {
if( accNotified.contains(host)) {
accNotified[host] = accNotified[host]+1;
} else {
accNotified[host] = 1;
}
}
_guiLoggedNotifications.insert(activity._id);
}
}
// check if there are widgets that have no corresponding activity from
// the server any more. Collect them in a list
QList< Activity::Identifier > strayCats;
foreach( auto id, _widgetForNotifId.keys() ) {
NotificationWidget *widget = _widgetForNotifId[id];
bool found = false;
// do not mark widgets of other accounts to delete.
if( widget->activity()._accName != listAccountName ) {
continue;
}
foreach( auto activity, list ) {
if( activity.ident() == id ) {
// found an activity
found = true;
break;
}
}
if( ! found ) {
// the activity does not exist any more.
strayCats.append(id);
}
}
// .. and now delete all these stray cat widgets.
foreach( auto strayCatId, strayCats ) {
NotificationWidget *widgetToGo = _widgetForNotifId[strayCatId];
scheduleWidgetToRemove(widgetToGo, 0);
}
_ui->_notifyLabel->setHidden( _widgetForNotifId.isEmpty() );
_ui->_notifyScroll->setHidden( _widgetForNotifId.isEmpty() );
int newGuiLogCount = accNotified.count();
if( newGuiLogCount > 0 ) {
// restart the gui log timer now that we show a notification
_guiLogTimer.restart();
// Assemble a tray notification
QString msg = tr("You received %n new notification(s) from %2.", "", accNotified[accNotified.keys().at(0)]).
arg(accNotified.keys().at(0));
if( newGuiLogCount >= 2 ) {
QString acc1 = accNotified.keys().at(0);
QString acc2 = accNotified.keys().at(1);
if( newGuiLogCount == 2 ) {
int notiCount = accNotified[ acc1 ] + accNotified[ acc2 ];
msg = tr("You received %n new notification(s) from %1 and %2.", "", notiCount).arg(acc1, acc2);
} else {
msg = tr("You received new notifications from %1, %2 and other accounts.").arg(acc1, acc2);
}
}
const QString log = tr("%1 Notifications - Action Required").arg(Theme::instance()->appNameGUI());
emit guiLog( log, msg);
}
}
void ActivityWidget::slotSendNotificationRequest(const QString& accountName, const QString& link, const QByteArray& verb)
{
qDebug() << Q_FUNC_INFO << "Server Notification Request " << verb << link << "on account" << accountName;
NotificationWidget *theSender = qobject_cast<NotificationWidget*>(sender());
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->setWidget(theSender);
connect( job, SIGNAL( networkError(QNetworkReply*)),
this, SLOT(slotNotifyNetworkError(QNetworkReply*)));
connect( job, SIGNAL( jobFinished(QString, int)),
this, SLOT(slotNotifyServerFinished(QString, int)) );
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 {
qDebug() << Q_FUNC_INFO << "Notification Links: Invalid verb:" << verb;
}
}
void ActivityWidget::endNotificationRequest( NotificationWidget *widget, int replyCode )
{
_notificationRequestsRunning--;
if( widget ) {
widget->slotNotificationRequestFinished(replyCode);
}
}
void ActivityWidget::slotNotifyNetworkError( QNetworkReply *reply)
{
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob*>(sender());
if( !job ) {
return;
}
int resultCode =0;
if( reply ) {
resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
endNotificationRequest(job->widget(), resultCode);
qDebug() << Q_FUNC_INFO << "Server notify job failed with code " << resultCode;
}
void ActivityWidget::slotNotifyServerFinished( const QString& reply, int replyCode )
{
NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob*>(sender());
if( !job ) {
return;
}
endNotificationRequest(job->widget(), replyCode);
// FIXME: remove the widget after a couple of seconds
qDebug() << Q_FUNC_INFO << "Server Notification reply code"<< replyCode << reply;
// if the notification was successful start a timer that triggers
// removal of the done widgets in a few seconds
// Add 200 millisecs to the predefined value to make sure that the timer in
// widget's method readyToClose() has elapsed.
if( replyCode == OCS_SUCCESS_STATUS_CODE ) {
scheduleWidgetToRemove( job->widget() );
}
}
// blacklist the activity coming in here.
void ActivityWidget::slotRequestCleanupAndBlacklist(const Activity& blacklistActivity)
{
if ( ! _blacklistedNotifications.contains(blacklistActivity) ) {
_blacklistedNotifications.append(blacklistActivity);
}
NotificationWidget *widget = _widgetForNotifId[ blacklistActivity.ident() ];
scheduleWidgetToRemove(widget);
}
void ActivityWidget::scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds)
{
if( !widget ) {
return;
}
// in fife seconds from now, remove the widget.
QDateTime removeTime = QDateTime::currentDateTime().addMSecs(milliseconds);
QPair<QDateTime, NotificationWidget*> removeInfo = qMakePair(removeTime, widget);
if( !_widgetsToRemove.contains(removeInfo) ) {
_widgetsToRemove.insert( removeInfo );
if( !_removeTimer.isActive() ) {
_removeTimer.start();
}
}
}
// Called every second to see if widgets need to be removed.
void ActivityWidget::slotCheckToCleanWidgets()
{
// loop over all widgets in the to-remove queue
foreach( auto toRemove, _widgetsToRemove ) {
QDateTime t = toRemove.first;
NotificationWidget *widget = toRemove.second;
if( QDateTime::currentDateTime() > t ) {
// found one to remove!
Activity::Identifier id = widget->activity().ident();
_widgetForNotifId.remove(id);
widget->deleteLater();
_widgetsToRemove.remove(toRemove);
}
}
if( _widgetsToRemove.isEmpty() ) {
_removeTimer.stop();
}
// check to see if the whole notification pane should be hidden
if( _widgetForNotifId.isEmpty() ) {
_ui->_notifyLabel->setHidden(true);
_ui->_notifyScroll->setHidden(true);
}
}
/* ==================================================================== */ /* ==================================================================== */
ActivitySettings::ActivitySettings(QWidget *parent) ActivitySettings::ActivitySettings(QWidget *parent)
@ -414,6 +496,7 @@ ActivitySettings::ActivitySettings(QWidget *parent)
_activityTabId = _tab->insertTab(0, _activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity")); _activityTabId = _tab->insertTab(0, _activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity"));
connect(_activityWidget, SIGNAL(copyToClipboard()), this, SLOT(slotCopyToClipboard())); connect(_activityWidget, SIGNAL(copyToClipboard()), this, SLOT(slotCopyToClipboard()));
connect(_activityWidget, SIGNAL(hideAcitivityTab(bool)), this, SLOT(setActivityTabHidden(bool))); connect(_activityWidget, SIGNAL(hideAcitivityTab(bool)), this, SLOT(setActivityTabHidden(bool)));
connect(_activityWidget, SIGNAL(guiLog(QString,QString)), this, SIGNAL(guiLog(QString,QString)));
_protocolWidget = new ProtocolWidget(this); _protocolWidget = new ProtocolWidget(this);
_tab->insertTab(1, _protocolWidget, Theme::instance()->syncStateIcon(SyncResult::Success), tr("Sync Protocol")); _tab->insertTab(1, _protocolWidget, Theme::instance()->syncStateIcon(SyncResult::Success), tr("Sync Protocol"));
@ -438,6 +521,9 @@ ActivitySettings::ActivitySettings(QWidget *parent)
_progressIndicator = new QProgressIndicator(this); _progressIndicator = new QProgressIndicator(this);
_tab->setCornerWidget(_progressIndicator); _tab->setCornerWidget(_progressIndicator);
connect(&_notificationCheckTimer, SIGNAL(timeout()),
this, SLOT(slotRegularNotificationCheck()));
// connect a model signal to stop the animation. // connect a model signal to stop the animation.
connect(_activityWidget, SIGNAL(rowsInserted()), _progressIndicator, SLOT(stopAnimation())); connect(_activityWidget, SIGNAL(rowsInserted()), _progressIndicator, SLOT(stopAnimation()));
@ -445,6 +531,12 @@ ActivitySettings::ActivitySettings(QWidget *parent)
_tab->setCurrentIndex(1); _tab->setCurrentIndex(1);
} }
void ActivitySettings::setNotificationRefreshInterval( quint64 interval )
{
qDebug() << "Starting Notification refresh timer with " << interval/1000 << " sec interval";
_notificationCheckTimer.start(interval);
}
void ActivitySettings::setActivityTabHidden(bool hidden) void ActivitySettings::setActivityTabHidden(bool hidden)
{ {
if( hidden && _activityTabId > -1 ) { if( hidden && _activityTabId > -1 ) {
@ -490,10 +582,32 @@ void ActivitySettings::slotRemoveAccount( AccountState *ptr )
void ActivitySettings::slotRefresh( AccountState* ptr ) void ActivitySettings::slotRefresh( AccountState* ptr )
{ {
if( ptr && ptr->isConnected() && isVisible()) { QElapsedTimer timer = _timeSinceLastCheck[ptr];
qDebug() << "Refreshing Activity list for " << ptr->account()->displayName();
// Fetch Activities only if visible and if last check is longer than 15 secs ago
if( timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD ) {
qDebug() << Q_FUNC_INFO << "do not check as last check is only secs ago: " << timer.elapsed() / 1000;
return;
}
if( ptr && ptr->isConnected() ) {
if( isVisible() ) {
_progressIndicator->startAnimation(); _progressIndicator->startAnimation();
_activityWidget->slotRefresh(ptr); _activityWidget->slotRefreshActivities( ptr);
}
_activityWidget->slotRefreshNotifications(ptr);
if( !( _timeSinceLastCheck[ptr].isValid() ) ) {
_timeSinceLastCheck[ptr].start();
} else {
_timeSinceLastCheck[ptr].restart();
}
}
}
void ActivitySettings::slotRegularNotificationCheck()
{
AccountManager *am = AccountManager::instance();
foreach (AccountStatePtr a, am->accounts()) {
slotRefresh(a.data());
} }
} }

View file

@ -22,6 +22,7 @@
#include "progressdispatcher.h" #include "progressdispatcher.h"
#include "owncloudgui.h" #include "owncloudgui.h"
#include "account.h" #include "account.h"
#include "activitydata.h"
#include "ui_activitywidget.h" #include "ui_activitywidget.h"
@ -33,97 +34,15 @@ namespace OCC {
class Account; class Account;
class AccountStatusPtr; class AccountStatusPtr;
class ProtocolWidget; class ProtocolWidget;
class JsonApiJob;
class NotificationWidget;
class ActivityListModel;
namespace Ui { namespace Ui {
class ActivityWidget; class ActivityWidget;
} }
class Application; class Application;
/**
* @brief Activity Structure
* @ingroup gui
*
* contains all the information describing a single activity.
*/
class Activity
{
public:
qlonglong _id;
QString _subject;
QString _message;
QString _file;
QUrl _link;
QDateTime _dateTime;
QString _accName;
/**
* @brief Sort operator to sort the list youngest first.
* @param val
* @return
*/
bool operator<( const Activity& val ) const {
return _dateTime.toMSecsSinceEpoch() > val._dateTime.toMSecsSinceEpoch();
}
};
/**
* @brief The ActivityList
* @ingroup gui
*
* A QList based list of Activities
*/
class ActivityList:public QList<Activity>
{
public:
void setAccountName( const QString& name );
QString accountName() const;
private:
QString _accountName;
};
/**
* @brief The ActivityListModel
* @ingroup gui
*
* Simple list model to provide the list view with data.
*/
class ActivityListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit ActivityListModel(QWidget *parent=0);
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex& parent = QModelIndex()) const Q_DECL_OVERRIDE;
bool canFetchMore(const QModelIndex& ) const Q_DECL_OVERRIDE;
void fetchMore(const QModelIndex&) Q_DECL_OVERRIDE;
ActivityList activityList() { return _finalList; }
public slots:
void slotRefreshActivity(AccountState* ast);
void slotRemoveAccount( AccountState *ast );
private slots:
void slotActivitiesReceived(const QVariantMap& json, int statusCode);
signals:
void activityJobStatusCode(AccountState* ast, int statusCode);
private:
void startFetchJob(AccountState* s);
void combineActivityLists();
QMap<AccountState*, ActivityList> _activityLists;
ActivityList _finalList;
QSet<AccountState*> _currentlyFetching;
};
/** /**
* @brief The ActivityWidget class * @brief The ActivityWidget class
* @ingroup gui * @ingroup gui
@ -143,15 +62,27 @@ public:
public slots: public slots:
void slotOpenFile(QModelIndex indx); void slotOpenFile(QModelIndex indx);
void slotRefresh(AccountState* ptr); void slotRefreshActivities(AccountState* ptr);
void slotRefreshNotifications(AccountState *ptr);
void slotRemoveAccount( AccountState *ptr ); void slotRemoveAccount( AccountState *ptr );
void slotAccountActivityStatus(AccountState *ast, int statusCode); void slotAccountActivityStatus(AccountState *ast, int statusCode);
void slotRequestCleanupAndBlacklist(const Activity& blacklistActivity);
signals: signals:
void guiLog(const QString&, const QString&); void guiLog(const QString&, const QString&);
void copyToClipboard(); void copyToClipboard();
void rowsInserted(); void rowsInserted();
void hideAcitivityTab(bool); void hideAcitivityTab(bool);
void newNotificationList(const ActivityList& list);
private slots:
void slotBuildNotificationDisplay(const ActivityList& list);
void slotSendNotificationRequest(const QString &accountName, const QString& link, const QByteArray &verb);
void slotNotifyNetworkError( QNetworkReply* );
void slotNotifyServerFinished( const QString& reply, int replyCode );
void endNotificationRequest(NotificationWidget *widget , int replyCode);
void scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds = 4500);
void slotCheckToCleanWidgets();
private: private:
void showLabels(); void showLabels();
@ -160,8 +91,21 @@ private:
QPushButton *_copyBtn; QPushButton *_copyBtn;
QSet<QString> _accountsWithoutActivities; QSet<QString> _accountsWithoutActivities;
QMap<Activity::Identifier, NotificationWidget*> _widgetForNotifId;
QElapsedTimer _guiLogTimer;
QSet<int> _guiLoggedNotifications;
ActivityList _blacklistedNotifications;
QSet< QPair<QDateTime, NotificationWidget*> > _widgetsToRemove;
QTimer _removeTimer;
// number of currently running notification requests. If non zero,
// no query for notifications is started.
int _notificationRequestsRunning;
ActivityListModel *_model; ActivityListModel *_model;
QVBoxLayout *_notificationsLayout;
}; };
@ -184,9 +128,12 @@ public slots:
void slotRefresh( AccountState* ptr ); void slotRefresh( AccountState* ptr );
void slotRemoveAccount( AccountState *ptr ); void slotRemoveAccount( AccountState *ptr );
void setNotificationRefreshInterval( quint64 interval );
private slots: private slots:
void slotCopyToClipboard(); void slotCopyToClipboard();
void setActivityTabHidden(bool hidden); void setActivityTabHidden(bool hidden);
void slotRegularNotificationCheck();
signals: signals:
void guiLog(const QString&, const QString&); void guiLog(const QString&, const QString&);
@ -200,7 +147,8 @@ private:
ActivityWidget *_activityWidget; ActivityWidget *_activityWidget;
ProtocolWidget *_protocolWidget; ProtocolWidget *_protocolWidget;
QProgressIndicator *_progressIndicator; QProgressIndicator *_progressIndicator;
QTimer _notificationCheckTimer;
QHash<AccountState*, QElapsedTimer> _timeSinceLastCheck;
}; };
} }

View file

@ -15,23 +15,60 @@
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="_headerLabel"> <widget class="QLabel" name="_notifyLabel">
<property name="text"> <property name="text">
<string>TextLabel</string> <string>TextLabel</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QListView" name="_activityList"/> <widget class="QScrollArea" name="_notifyScroll">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="_scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>677</width>
<height>70</height>
</rect>
</property>
</widget>
</widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="_bottomLabel"> <widget class="QLabel" name="_headerLabel">
<property name="text"> <property name="text">
<string>TextLabel</string> <string>TextLabel</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QListView" name="_activityList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="_bottomLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QDialogButtonBox" name="_dialogButtonBox"/> <widget class="QDialogButtonBox" name="_dialogButtonBox"/>
</item> </item>
</layout> </layout>

View file

@ -0,0 +1,81 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 "notificationconfirmjob.h"
#include "networkjobs.h"
#include "account.h"
#include "json.h"
#include <QBuffer>
namespace OCC {
NotificationConfirmJob::NotificationConfirmJob(AccountPtr account)
: AbstractNetworkJob(account, ""),
_widget(0)
{
setIgnoreCredentialFailure(true);
}
void NotificationConfirmJob::setLinkAndVerb(const QUrl& link, const QByteArray &verb)
{
_link = link;
_verb = verb;
}
void NotificationConfirmJob::setWidget( NotificationWidget *widget )
{
_widget = widget;
}
NotificationWidget *NotificationConfirmJob::widget()
{
return _widget;
}
void NotificationConfirmJob::start()
{
if( !_link.isValid() ) {
qDebug() << "Attempt to trigger invalid URL: " << _link.toString();
return;
}
QNetworkRequest req;
req.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
QIODevice *iodevice = 0;
setReply(davRequest(_verb, _link, req, iodevice));
setupConnections(reply());
AbstractNetworkJob::start();
}
bool NotificationConfirmJob::finished()
{
int replyCode = 0;
// FIXME: check for the reply code!
const QString replyStr = reply()->readAll();
if( replyStr.contains( "<?xml version=\"1.0\"?>") ) {
QRegExp rex("<statuscode>(\\d+)</statuscode>");
if( replyStr.contains(rex) ) {
// this is a error message coming back from ocs.
replyCode = rex.cap(1).toInt();
}
}
emit jobFinished(replyStr, replyCode);
return true;
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 NOTIFICATIONCONFIRMJOB_H
#define NOTIFICATIONCONFIRMJOB_H
#include "accountfwd.h"
#include "abstractnetworkjob.h"
#include <QVector>
#include <QList>
#include <QPair>
#include <QUrl>
namespace OCC {
class NotificationWidget;
/**
* @brief The NotificationConfirmJob class
* @ingroup gui
*
* Class to call an action-link of a notification coming from the server.
* All the communication logic is handled in this class.
*
*/
class NotificationConfirmJob : public AbstractNetworkJob {
Q_OBJECT
public:
explicit NotificationConfirmJob(AccountPtr account);
/**
* @brief Set the verb and link for the job
*
* @param verb currently supported GET PUT POST DELETE
*/
void setLinkAndVerb(const QUrl& link, const QByteArray &verb);
/**
* @brief Start the OCS request
*/
void start() Q_DECL_OVERRIDE;
/**
* @brief setWidget stores the associated widget to be able to use
* it when the job has finished
* @param widget pointer to the notification widget to store
*/
void setWidget( NotificationWidget *widget );
/**
* @brief widget - get the associated notification widget as stored
* with setWidget method.
* @return widget pointer to the notification widget
*/
NotificationWidget *widget();
signals:
/**
* Result of the OCS request
*
* @param reply the reply
*/
void jobFinished(QString reply, int replyCode);
private slots:
virtual bool finished() Q_DECL_OVERRIDE;
private:
QByteArray _verb;
QUrl _link;
NotificationWidget *_widget;
};
}
#endif // NotificationConfirmJob_H

View file

@ -0,0 +1,147 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 "notificationwidget.h"
#include "QProgressIndicator.h"
#include "utility.h"
#include <QPushButton>
#include "ocsjob.h"
namespace OCC {
NotificationWidget::NotificationWidget(QWidget *parent) : QWidget(parent)
{
_ui.setupUi(this);
_progressIndi = new QProgressIndicator(this);
_ui.horizontalLayout->addWidget(_progressIndi);
}
void NotificationWidget::setActivity(const Activity& activity)
{
_myActivity = activity;
Q_ASSERT( !activity._accName.isEmpty() );
_accountName = activity._accName;
// _ui._headerLabel->setText( );
_ui._subjectLabel->setText(activity._subject);
if( activity._message.isEmpty()) {
_ui._messageLabel->hide();
} else {
_ui._messageLabel->setText(activity._message);
}
_ui._notifIcon->setPixmap(QPixmap(":/client/resources/bell.png"));
_ui._notifIcon->setMinimumWidth(64);
_ui._notifIcon->setMinimumHeight(64);
_ui._notifIcon->show();
QString tText = tr("Created at %1").arg(Utility::timeAgoInWords(activity._dateTime));
_ui._timeLabel->setText(tText);
// always remove the buttons
foreach( auto button, _ui._buttonBox->buttons() ) {
_ui._buttonBox->removeButton(button);
}
_buttons.clear();
// display buttons for the links
if( activity._links.isEmpty() ) {
// in case there is no action defined, do a close button.
QPushButton *b = _ui._buttonBox->addButton( QDialogButtonBox::Close );
b->setDefault(true);
connect(b, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
_buttons.append(b);
} else {
foreach( auto link, activity._links ) {
QPushButton *b = _ui._buttonBox->addButton(link._label, QDialogButtonBox::AcceptRole);
b->setDefault(link._isPrimary);
connect(b, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
_buttons.append(b);
}
}
}
Activity NotificationWidget::activity() const
{
return _myActivity;
}
void NotificationWidget::slotButtonClicked()
{
QObject *buttonWidget = QObject::sender();
int index = -1;
if( buttonWidget ) {
// find the button that was clicked, it has to be in the list
// of buttons that were added to the button box before.
for( int i = 0; i < _buttons.count(); i++ ) {
if( _buttons.at(i) == buttonWidget ) {
index = i;
}
_buttons.at(i)->setEnabled(false);
}
// if the button was found, the link must be called
if( index > -1 && _myActivity._links.count() == 0 ) {
// no links, that means it was the close button
// empty link. Just close and remove the widget.
QString doneText = tr("Closing in a few seconds...");
_ui._timeLabel->setText(doneText);
emit requestCleanupAndBlacklist(_myActivity);
return;
}
if( index > -1 && index < _myActivity._links.count() ) {
ActivityLink triggeredLink = _myActivity._links.at(index);
_actionLabel = triggeredLink._label;
if( ! triggeredLink._link.isEmpty() ) {
qDebug() << Q_FUNC_INFO << "Notification Link: "<< triggeredLink._verb << triggeredLink._link;
_progressIndi->startAnimation();
emit sendNotificationRequest( _accountName, triggeredLink._link, triggeredLink._verb );
}
}
}
}
void NotificationWidget::slotNotificationRequestFinished(int statusCode)
{
int i = 0;
QString doneText;
QLocale locale;
QString timeStr = locale.toString(QTime::currentTime());
// the ocs API returns stat code 100 if it succeeded.
if( statusCode != OCS_SUCCESS_STATUS_CODE ) {
qDebug() << Q_FUNC_INFO << "Notification Request to Server failed, leave button visible.";
for( i = 0; i < _buttons.count(); i++ ) {
_buttons.at(i)->setEnabled(true);
}
//: The second parameter is a time, such as 'failed at 09:58pm'
doneText = tr("%1 request failed at %2").arg(_actionLabel, timeStr);
} else {
// the call to the ocs API succeeded.
_ui._buttonBox->hide();
//: The second parameter is a time, such as 'selected at 09:58pm'
doneText = tr("'%1' selected at %2").arg(_actionLabel, timeStr);
}
_ui._timeLabel->setText( doneText );
_progressIndi->stopAnimation();
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 NOTIFICATIONWIDGET_H
#define NOTIFICATIONWIDGET_H
#include <QWidget>
#include "activitydata.h"
#include "ui_notificationwidget.h"
#define NOTIFICATION_WIDGET_CLOSE_AFTER_MILLISECS 4800
class QProgressIndicator;
namespace OCC {
class NotificationWidget : public QWidget
{
Q_OBJECT
public:
explicit NotificationWidget(QWidget *parent = 0);
bool readyToClose();
Activity activity() const;
signals:
void sendNotificationRequest( const QString&, const QString& link, const QByteArray& verb);
void requestCleanupAndBlacklist( const Activity& activity );
public slots:
void setActivity(const Activity& activity);
void slotNotificationRequestFinished(int statusCode);
private slots:
void slotButtonClicked();
private:
Ui_NotificationWidget _ui;
Activity _myActivity;
QList<QPushButton*> _buttons;
QString _accountName;
QProgressIndicator *_progressIndi;
QString _actionLabel;
};
}
#endif // NOTIFICATIONWIDGET_H

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NotificationWidget</class>
<widget class="QWidget" name="NotificationWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>725</width>
<height>139</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="_notifIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>../../../../resources/bell.png</pixmap>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="_subjectLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Lorem ipsum dolor sit amet</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="_messageLabel">
<property name="text">
<string>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam </string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="_timeLabel">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="_buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>4</number>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -23,7 +23,7 @@ namespace OCC {
OcsJob::OcsJob(AccountPtr account) OcsJob::OcsJob(AccountPtr account)
: AbstractNetworkJob(account, "") : AbstractNetworkJob(account, "")
{ {
_passStatusCodes.append(100); _passStatusCodes.append(OCS_SUCCESS_STATUS_CODE);
setIgnoreCredentialFailure(true); setIgnoreCredentialFailure(true);
} }

View file

@ -22,6 +22,8 @@
#include <QPair> #include <QPair>
#include <QUrl> #include <QUrl>
#define OCS_SUCCESS_STATUS_CODE 100
namespace OCC { namespace OCC {
/** /**

View file

@ -0,0 +1,103 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 "servernotificationhandler.h"
#include "accountstate.h"
#include "capabilities.h"
#include "json.h"
#include "networkjobs.h"
namespace OCC
{
ServerNotificationHandler::ServerNotificationHandler(QObject *parent)
: QObject(parent)
{
}
void ServerNotificationHandler::slotFetchNotifications(AccountState *ptr)
{
// check connectivity and credentials
if( !( ptr && ptr->isConnected() && ptr->account() &&
ptr->account()->credentials() &&
ptr->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( ptr->account()->capabilities().isValid() ) {
if( ! ptr->account()->capabilities().notificationsAvailable() ) {
qDebug() << Q_FUNC_INFO << "Account" << ptr->account()->displayName() << "does not have notifications enabled.";
deleteLater();
return;
}
}
// if the previous notification job has finished, start next.
_notificationJob = new JsonApiJob( ptr->account(), QLatin1String("ocs/v2.php/apps/notifications/api/v1/notifications"), this );
QObject::connect(_notificationJob.data(), SIGNAL(jsonReceived(QVariantMap, int)),
this, SLOT(slotNotificationsReceived(QVariantMap, int)));
_notificationJob->setProperty("AccountStatePtr", QVariant::fromValue<AccountState*>(ptr));
_notificationJob->start();
}
void ServerNotificationHandler::slotNotificationsReceived(const QVariantMap& json, int statusCode)
{
if( statusCode != 200 ) {
qDebug() << Q_FUNC_INFO << "Notifications failed with status code " << statusCode;
deleteLater();
return;
}
auto notifies = json.value("ocs").toMap().value("data").toList();
AccountState* ai = qvariant_cast<AccountState*>(sender()->property("AccountStatePtr"));
ActivityList list;
foreach( auto element, notifies ) {
Activity a;
auto json = element.toMap();
a._type = Activity::NotificationType;
a._accName = ai->account()->displayName();
a._id = json.value("notification_id").toLongLong();
a._subject = json.value("subject").toString();
a._message = json.value("message").toString();
QString s = json.value("link").toString();
if( !s.isEmpty() ) {
a._link = QUrl(s);
}
a._dateTime = json.value("datetime").toDateTime();
auto actions = json.value("actions").toList();
foreach( auto action, actions) {
auto actionJson = action.toMap();
ActivityLink al;
al._label = QUrl::fromPercentEncoding(actionJson.value("label").toByteArray());
al._link = actionJson.value("link").toString();
al._verb = actionJson.value("type").toByteArray();
al._isPrimary = actionJson.value("primary").toBool();
a._links.append(al);
}
list.append(a);
}
emit newNotificationList( list );
deleteLater();
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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; version 2 of the License.
*
* 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 SERVERNOTIFICATIONHANDLER_H
#define SERVERNOTIFICATIONHANDLER_H
#include <QtCore>
#include "activitywidget.h"
namespace OCC
{
class ServerNotificationHandler : public QObject
{
Q_OBJECT
public:
explicit ServerNotificationHandler(QObject *parent = 0);
signals:
void newNotificationList(ActivityList);
public slots:
void slotFetchNotifications(AccountState *ptr);
private slots:
void slotNotificationsReceived(const QVariantMap& json, int statusCode);
private:
QPointer<JsonApiJob> _notificationJob;
};
}
#endif // SERVERNOTIFICATIONHANDLER_H

View file

@ -61,6 +61,8 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent) :
QDialog(parent) QDialog(parent)
, _ui(new Ui::SettingsDialog), _gui(gui) , _ui(new Ui::SettingsDialog), _gui(gui)
{ {
ConfigFile cfg;
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
_ui->setupUi(this); _ui->setupUi(this);
_toolBar = new QToolBar; _toolBar = new QToolBar;
@ -89,6 +91,7 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent) :
_ui->stack->addWidget(_activitySettings); _ui->stack->addWidget(_activitySettings);
connect( _activitySettings, SIGNAL(guiLog(QString,QString)), _gui, connect( _activitySettings, SIGNAL(guiLog(QString,QString)), _gui,
SLOT(slotShowOptionalTrayMessage(QString,QString)) ); SLOT(slotShowOptionalTrayMessage(QString,QString)) );
_activitySettings->setNotificationRefreshInterval( cfg.notificationRefreshInterval());
QAction *generalAction = createColorAwareAction(QLatin1String(":/client/resources/settings.png"), tr("General")); QAction *generalAction = createColorAwareAction(QLatin1String(":/client/resources/settings.png"), tr("General"));
_actionGroup->addAction(generalAction); _actionGroup->addAction(generalAction);
@ -128,7 +131,6 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent) :
customizeStyle(); customizeStyle();
ConfigFile cfg;
cfg.restoreGeometry(this); cfg.restoreGeometry(this);
} }

View file

@ -148,6 +148,11 @@ QNetworkReply *AbstractNetworkJob::headRequest(const QUrl &url)
return addTimer(_account->headRequest(url)); return addTimer(_account->headRequest(url));
} }
QNetworkReply *AbstractNetworkJob::deleteRequest(const QUrl &url)
{
return addTimer(_account->deleteRequest(url));
}
void AbstractNetworkJob::slotFinished() void AbstractNetworkJob::slotFinished()
{ {
_timer.stop(); _timer.stop();

View file

@ -77,6 +77,7 @@ protected:
QNetworkReply* getRequest(const QUrl &url); QNetworkReply* getRequest(const QUrl &url);
QNetworkReply* headRequest(const QString &relPath); QNetworkReply* headRequest(const QString &relPath);
QNetworkReply* headRequest(const QUrl &url); QNetworkReply* headRequest(const QUrl &url);
QNetworkReply* deleteRequest(const QUrl &url);
int maxRedirects() const { return 10; } int maxRedirects() const { return 10; }
virtual bool finished() = 0; virtual bool finished() = 0;

View file

@ -239,6 +239,15 @@ QNetworkReply *Account::getRequest(const QUrl &url)
return _am->get(request); return _am->get(request);
} }
QNetworkReply *Account::deleteRequest( const QUrl &url)
{
QNetworkRequest request(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
request.setSslConfiguration(this->getOrCreateSslConfig());
#endif
return _am->deleteResource(request);
}
QNetworkReply *Account::davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data) QNetworkReply *Account::davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data)
{ {
return davRequest(verb, concatUrlPath(davUrl(), relPath), req, data); return davRequest(verb, concatUrlPath(davUrl(), relPath), req, data);

View file

@ -111,6 +111,7 @@ public:
QNetworkReply* headRequest(const QUrl &url); QNetworkReply* headRequest(const QUrl &url);
QNetworkReply* getRequest(const QString &relPath); QNetworkReply* getRequest(const QString &relPath);
QNetworkReply* getRequest(const QUrl &url); QNetworkReply* getRequest(const QUrl &url);
QNetworkReply* deleteRequest( const QUrl &url);
QNetworkReply* davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data = 0); QNetworkReply* davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data = 0);
QNetworkReply* davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data = 0); QNetworkReply* davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data = 0);

View file

@ -71,6 +71,16 @@ bool Capabilities::shareResharing() const
return _capabilities["files_sharing"].toMap()["resharing"].toBool(); return _capabilities["files_sharing"].toMap()["resharing"].toBool();
} }
bool Capabilities::notificationsAvailable() const
{
return _capabilities.contains("notifications");
}
bool Capabilities::isValid() const
{
return !_capabilities.isEmpty();
}
QList<QByteArray> Capabilities::supportedChecksumTypesAdvertised() const QList<QByteArray> Capabilities::supportedChecksumTypesAdvertised() const
{ {
return QList<QByteArray>(); return QList<QByteArray>();

View file

@ -40,6 +40,12 @@ public:
int sharePublicLinkExpireDateDays() const; int sharePublicLinkExpireDateDays() const;
bool shareResharing() const; bool shareResharing() const;
/// returns true if the capabilities report notifications
bool notificationsAvailable() const;
/// returns true if the capabilities are loaded already.
bool isValid() const;
/// Returns the checksum types the server explicitly advertises /// Returns the checksum types the server explicitly advertises
QList<QByteArray> supportedChecksumTypesAdvertised() const; QList<QByteArray> supportedChecksumTypesAdvertised() const;

View file

@ -42,6 +42,7 @@ namespace OCC {
//static const char caCertsKeyC[] = "CaCertificates"; only used from account.cpp //static const char caCertsKeyC[] = "CaCertificates"; only used from account.cpp
static const char remotePollIntervalC[] = "remotePollInterval"; static const char remotePollIntervalC[] = "remotePollInterval";
static const char forceSyncIntervalC[] = "forceSyncInterval"; static const char forceSyncIntervalC[] = "forceSyncInterval";
static const char notificationRefreshIntervalC[] = "notificationRefreshInterval";
static const char monoIconsC[] = "monoIcons"; static const char monoIconsC[] = "monoIcons";
static const char promptDeleteC[] = "promptDeleteAllFiles"; static const char promptDeleteC[] = "promptDeleteAllFiles";
static const char crashReporterC[] = "crashReporter"; static const char crashReporterC[] = "crashReporter";
@ -390,6 +391,22 @@ quint64 ConfigFile::forceSyncInterval(const QString& connection) const
return interval; return interval;
} }
quint64 ConfigFile::notificationRefreshInterval(const QString& connection) const
{
QString con( connection );
if( connection.isEmpty() ) con = defaultConnection();
QSettings settings(configFile(), QSettings::IniFormat);
settings.beginGroup( con );
quint64 defaultInterval = 5 * 60 * 1000ull; // 5 minutes
quint64 interval = settings.value( QLatin1String(notificationRefreshIntervalC), defaultInterval ).toULongLong();
if( interval < 60*1000ull) {
qDebug() << "notification refresh interval smaller than one minute, setting to one minute";
interval = 60*1000ull;
}
return interval;
}
int ConfigFile::updateCheckInterval( const QString& connection ) const int ConfigFile::updateCheckInterval( const QString& connection ) const
{ {
QString con( connection ); QString con( connection );

View file

@ -63,6 +63,9 @@ public:
/* Set poll interval. Value in milliseconds has to be larger than 5000 */ /* Set poll interval. Value in milliseconds has to be larger than 5000 */
void setRemotePollInterval(int interval, const QString& connection = QString() ); void setRemotePollInterval(int interval, const QString& connection = QString() );
/* Interval to check for new notifications */
quint64 notificationRefreshInterval(const QString& connection = QString()) const;
/* Force sync interval, in milliseconds */ /* Force sync interval, in milliseconds */
quint64 forceSyncInterval(const QString &connection = QString()) const; quint64 forceSyncInterval(const QString &connection = QString()) const;

View file

@ -453,7 +453,7 @@ QByteArray Utility::versionOfInstalledBinary( const QString& command )
QString Utility::timeAgoInWords(const QDateTime& dt, const QDateTime& from) QString Utility::timeAgoInWords(const QDateTime& dt, const QDateTime& from)
{ {
QDateTime now = QDateTime::currentDateTime(); QDateTime now = QDateTime::currentDateTimeUtc();
if( from.isValid() ) { if( from.isValid() ) {
now = from; now = from;