Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Server Notifications on the Client #4567

Merged
merged 44 commits into from
Apr 4, 2016
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0eb1041
AbstractNetworkJob: Add a delete job.
dragotin Mar 4, 2016
a831b74
Added temporar icon for notifications.
dragotin Mar 4, 2016
eb00b34
Minor wording fixes
dragotin Mar 4, 2016
688c550
New GUI class NotificationWidget.
dragotin Mar 4, 2016
32e16b3
Display server notifications on the client (#3733)
dragotin Mar 4, 2016
4a4dac2
Notifications: Add a Progress indicator and handle job results.
dragotin Mar 9, 2016
b97c832
Capabilities: Add isValid check and check for notifications
dragotin Mar 9, 2016
7d13a1d
Notifications: Check capabilities if the notifications are enabled
dragotin Mar 9, 2016
8a0ce46
Notifications: Properly delete the notification check job.
dragotin Mar 9, 2016
903e79a
Notifications: Do a GUI tray notification if new notifciations arrive.
dragotin Mar 10, 2016
2d1ab27
Notifications: Refactor - create a notification handler class
dragotin Mar 10, 2016
2c2a18a
Activitiy: Refactor - move classes to their own source files.
dragotin Mar 11, 2016
adf9570
Notification: Enhance the tray message
dragotin Mar 11, 2016
73cd5a9
Notifications: Cleaner notification string build
dragotin Mar 14, 2016
97f1694
ActivityData: Simplified implementation.
dragotin Mar 14, 2016
9d219a1
ActivityListModel: Code cleanups
dragotin Mar 14, 2016
9a2f145
ocs jobs: Add a define for OCS job success.
dragotin Mar 14, 2016
a4dcc27
Notification: Fix plural handling for tray message
dragotin Mar 14, 2016
45c32ec
NotificationWidget: Remove not needed method.
dragotin Mar 16, 2016
f7f4120
Activity: Some documentation and better varialbe names
dragotin Mar 16, 2016
f71fdab
Fix timeAgoInWords
dragotin Mar 18, 2016
05de710
Notifications: Display timestamp of the notification in the widget
dragotin Mar 18, 2016
0a590b7
Notifications: Give feedback if notifcation request succeeded.
dragotin Mar 18, 2016
328d254
Notifications: Remove "done" notification widgets after fife seconds.
dragotin Mar 18, 2016
7f22a07
Notifications: Check if the account is connected before querying.
dragotin Mar 11, 2016
b966345
Notifications: Refresh the notifications based on a config value.
dragotin Mar 18, 2016
f587f35
Fix plural translation handling, remove the superflous arg()
dragotin Mar 18, 2016
d407aac
Notifications: remove notification widgets if the notification is gone.
dragotin Mar 21, 2016
d03fcc9
Notifications: Maintain a timeSinceLastCheck for every Account.
dragotin Mar 22, 2016
ad60e8a
Notifications: Fix handling of notifications to remove from the list.
dragotin Mar 22, 2016
f70c628
Notifications: Remove unused variable.
dragotin Mar 22, 2016
ea2f19b
Docs: Add new config option for the notification sync interval.
dragotin Mar 22, 2016
161d219
ActivityData: Add source file for implementation details
dragotin Mar 23, 2016
1bb3a4a
NotificationWidget: Remove accountName() and add activity() method.
dragotin Mar 23, 2016
0c944a0
NotificationWidgetUI: Fix sizing and sizePolicy
dragotin Mar 23, 2016
1fe5d6b
Notifications: Handle Notifications without an action.
dragotin Mar 23, 2016
69e8e15
Remove explicit time spec specification as it is not needed.
dragotin Mar 29, 2016
4d59f5e
ActivityData: Declare operators outside the class
dragotin Mar 29, 2016
cacb751
Cleaups based on review feedback.
dragotin Mar 29, 2016
9f438cb
Doc: Add milliseconds unit to notificationRefreshInterval doc
dragotin Mar 29, 2016
2e30a0e
Remove superflous iterator increment
dragotin Mar 29, 2016
cd3f612
ActivityWidget: Rename blacklistActivities to blacklistNotifications.
dragotin Mar 29, 2016
8166c52
NotificationHandling: Use QByteArray for the verb.
dragotin Mar 29, 2016
885f8b3
ActivityWidget: Handle plural properly in translations.
dragotin Mar 29, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
<file>resources/account.png</file>
<file>resources/more.png</file>
<file>resources/delete.png</file>
<file>resources/bell.png</file>
</qresource>
</RCC>
Binary file added resources/bell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ set(client_UI
owncloudsetuppage.ui
addcertificatedialog.ui
proxyauthdialog.ui
notificationwidget.ui
wizard/owncloudadvancedsetuppage.ui
wizard/owncloudconnectionmethoddialog.ui
wizard/owncloudhttpcredspage.ui
Expand Down Expand Up @@ -62,6 +63,7 @@ set(client_SRCS
owncloudgui.cpp
owncloudsetupwizard.cpp
protocolwidget.cpp
activitylistmodel.cpp
activitywidget.cpp
activityitemdelegate.cpp
selectivesyncdialog.cpp
Expand All @@ -84,6 +86,9 @@ set(client_SRCS
proxyauthhandler.cpp
proxyauthdialog.cpp
synclogdialog.cpp
notificationwidget.cpp
notificationconfirmjob.cpp
servernotificationhandler.cpp
creds/credentialsfactory.cpp
creds/httpcredentialsgui.cpp
creds/shibbolethcredentials.cpp
Expand Down
106 changes: 106 additions & 0 deletions src/gui/activitydata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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:
QHash <QString, QVariant> toVariantHash() {
QHash<QString, QVariant> hash;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use QVariantHash (introduced in Qt 4.5)? (the function could also be const)


hash["label"] = _label;
hash["link"] = _link;
hash["verb"] = _verb;
hash["primary"] = _isPrimary;

return hash;
}

QString _label;
QString _link;
QString _verb;
bool _isPrimary;
};

/* ==================================================================== */
/**
* @brief Activity Structure
* @ingroup gui
*
* contains all the information describing a single activity.
*/

class Activity
{
public:
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
*/
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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If inheriting here isn't mandatory for some reason, I'd prefer to have the list as a member. In fact, the accountName on this list seems to be unused anyway? Just typedef QList to ActivityList instead?

{
public:
void setAccountName( const QString& name ) {
_accountName = name;
}

QString accountName() const {
return _accountName;
}

private:
QString _accountName;
};

}

#endif // ACTIVITYDATA_H
237 changes: 237 additions & 0 deletions src/gui/activitylistmodel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* 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;

if (role == Qt::EditRole)
return QVariant();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove? (or put next to the default case in the switch statement)


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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for() loops are better since they are more readable and make it obvious it's an iteration.

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;
}
}
++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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

job->start();
}

void ActivityListModel::slotActivitiesReceived(const QVariantMap& json, int statusCode)
{
auto activities = json.value("ocs").toMap().value("data").toList();
// qDebug() << "*** activities" << activities;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?


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._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();
a._dateTime.setTimeSpec(Qt::UTC);
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there used to be more rows before? Does one need to explicitly clear the other rows?

_finalList = resultList;
endInsertRows();
}

void ActivityListModel::fetchMore(const QModelIndex &)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normaly, fetchMore is too be used when there are more items to fetch that are hidden.
So that can be used in order to fetch older items form example.
But here it's used to fetch other accounts despite it's not the next item.
fetchMore will be called when the scrollbar reach the end.
I don't think it's a good use of the fetchMore capabilities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. let me handle that in a new PR together with a proxy model maybe.

{
QList<AccountStatePtr> accounts = AccountManager::instance()->accounts();

foreach (AccountStatePtr asp, accounts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const AccountStatePtr &

bool newItem = false;

if( !_activityLists.contains(asp.data()) && asp->isConnected() ) {
_activityLists[asp.data()] = ActivityList();
newItem = true;
}
if( newItem ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newItem seems unnecessary.

startFetchJob(asp.data());
}
}
}

void ActivityListModel::slotRefreshActivity(AccountState *ast)
{
if(ast && _activityLists.contains(ast)) {
qDebug() << "**** Refreshing Activity list for" << ast->account()->displayName();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

_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);
}
}

}
Loading