diff --git a/src/tests/ui_test.py b/src/tests/ui_test.py
index f038b261e..462483aa0 100644
--- a/src/tests/ui_test.py
+++ b/src/tests/ui_test.py
@@ -97,6 +97,7 @@ def test_getclu(self):
resp = self.app.get('/getclu/1',
headers={'Content-Type': 'application/json'})
self.assertEquals(200, resp.status_code)
+ assert 'notifications' in json.loads(resp.data.decode('utf8'))
self.app.get('/logout')
self.app.post('/login', data={'login': 'user2', 'password': 'user2'})
resp = self.app.get('/getclu/1',
diff --git a/src/web/js/actions/MenuActions.js b/src/web/js/actions/MenuActions.js
index 00006a4fc..9cb804cb1 100644
--- a/src/web/js/actions/MenuActions.js
+++ b/src/web/js/actions/MenuActions.js
@@ -10,17 +10,8 @@ var MenuActions = {
reload: function(set_filter, setFilterFunc, id) {
jquery.getJSON('/menu', function(payload) {
var old_all_unread_count = MenuStore.all_unread_count;
- JarrDispatcher.dispatch({
- type: ActionTypes.RELOAD_MENU,
- feeds: payload.feeds,
- categories: payload.categories,
- categories_order: payload.categories_order,
- is_admin: payload.is_admin,
- max_error: payload.max_error,
- error_threshold: payload.error_threshold,
- crawling_method: payload.crawling_method,
- all_unread_count: payload.all_unread_count,
- });
+ payload.type = ActionTypes.RELOAD_MENU;
+ JarrDispatcher.dispatch(payload);
/* setfilter param is here so were sure it's called in the sole
* purpose of setting filter and that the setFilterFunc is not
* some event passed by react
diff --git a/src/web/js/actions/RightPanelActions.js b/src/web/js/actions/RightPanelActions.js
index 9f3707825..92308b707 100644
--- a/src/web/js/actions/RightPanelActions.js
+++ b/src/web/js/actions/RightPanelActions.js
@@ -24,6 +24,7 @@ var RightPanelActions = {
JarrDispatcher.dispatch({
type: ActionTypes.LOAD_CLUSTER,
cluster: payload,
+ notifications: payload.notifications,
was_read_before: was_read_before,
article_id: article_id,
});
diff --git a/src/web/js/components/MainApp.react.js b/src/web/js/components/MainApp.react.js
index 7944a00b3..486049c5d 100644
--- a/src/web/js/components/MainApp.react.js
+++ b/src/web/js/components/MainApp.react.js
@@ -5,6 +5,7 @@ var JarrNavBar = require('./Navbar.react');
var Menu = require('./Menu.react');
var MiddlePanel = require('./MiddlePanel.react');
var RightPanel = require('./RightPanel.react');
+var Notifications = require('./Notifications.react');
var MainApp = React.createClass({
@@ -16,6 +17,7 @@ var MainApp = React.createClass({
+
);
},
diff --git a/src/web/js/components/Notifications.react.js b/src/web/js/components/Notifications.react.js
new file mode 100644
index 000000000..8426a65b3
--- /dev/null
+++ b/src/web/js/components/Notifications.react.js
@@ -0,0 +1,46 @@
+var React = require('react');
+var NotificationsStore = require('../stores/NotificationsStore');
+var NotificationSystem = require('react-notification-system');
+
+
+var Notifications = React.createClass({
+ _notificationSystem: null,
+ addNotification: function(notif) {
+ this._notificationSystem.addNotification({
+ message: notif.message,
+ level: notif.level,
+ autoDismiss: 30,
+ onRemove: this.removeNotification,
+ });
+ },
+ removeNotification: function(notif) {
+ for(var idx in NotificationsStore.notifs) {
+ if(NotificationsStore.notifs[idx].read == false
+ && NotificationsStore.notifs[idx].level == notif.level
+ && NotificationsStore.notifs[idx].message == notif.message) {
+ NotificationsStore.notifs[idx].read = true;
+ break;
+ }
+
+ }
+ },
+ render: function() {
+ return ;
+ },
+ componentDidMount: function() {
+ this._notificationSystem = this.refs.notificationSystem;
+ NotificationsStore.addChangeListener(this._onChange);
+ },
+ componentWillUnmount: function() {
+ NotificationsStore.removeChangeListener(this._onChange);
+ },
+ _onChange: function() {
+ for(var idx in NotificationsStore.notifs) {
+ if(!NotificationsStore.notifs[idx].read) {
+ this.addNotification(NotificationsStore.notifs[idx]);
+ }
+ }
+ },
+});
+
+module.exports = Notifications;
diff --git a/src/web/js/stores/NotificationsStore.js b/src/web/js/stores/NotificationsStore.js
new file mode 100644
index 000000000..8f9613745
--- /dev/null
+++ b/src/web/js/stores/NotificationsStore.js
@@ -0,0 +1,50 @@
+var JarrDispatcher = require('../dispatcher/JarrDispatcher');
+var ActionTypes = require('../constants/JarrConstants');
+var EventEmitter = require('events').EventEmitter;
+
+var CHANGE_EVENT = 'change_menu';
+var assign = require('object-assign');
+
+
+var NotificationsStore = assign({}, EventEmitter.prototype, {
+ notifs: [],
+
+ addNotifications: function(notifications) {
+ var count = this.notifs.length;
+ for(var idx in notifications) {
+ this.notifs.push({
+ key: parseInt(idx) + count,
+ read: false,
+ level: notifications[idx].level,
+ message: notifications[idx].message,
+ });
+ }
+ },
+ getNotifications: function() {
+ this.notifs = this.notifs.filter(function(notif) {return !notif.read;});
+ return this.notifs;
+ },
+ emitChange: function(all_folded) {
+ if (all_folded) {
+ this.all_folded = all_folded;
+ } else {
+ this.all_folded = null;
+ }
+ this.emit(CHANGE_EVENT);
+ },
+ addChangeListener: function(callback) {
+ this.on(CHANGE_EVENT, callback);
+ },
+ removeChangeListener: function(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ },
+});
+
+NotificationsStore.dispatchToken = JarrDispatcher.register(function(action) {
+ if(action.notifications) {
+ NotificationsStore.addNotifications(action.notifications);
+ NotificationsStore.emitChange();
+ }
+});
+
+module.exports = NotificationsStore;
diff --git a/src/web/lib/view_utils.py b/src/web/lib/view_utils.py
index 676a513c5..642470fce 100644
--- a/src/web/lib/view_utils.py
+++ b/src/web/lib/view_utils.py
@@ -1,12 +1,14 @@
import pytz
from functools import wraps
from datetime import datetime
-from flask import request, Response, make_response
+from flask import request, Response, make_response, get_flashed_messages
from flask_babel import get_locale
from babel.dates import format_datetime, format_timedelta
from web.views.common import jsonify
from lib.utils import to_hash
+ACCEPTED_LEVELS = {'success', 'info', 'warning', 'error'}
+
def etag_match(func):
@wraps(func)
@@ -41,7 +43,14 @@ def _iter_on_rows(rows, now, locale):
yield row
+def get_notifications():
+ for msg in get_flashed_messages(with_categories=True):
+ yield {'level': msg[0] if msg[0] in ACCEPTED_LEVELS else 'info',
+ 'message': msg[1]}
+
+
@jsonify
def clusters_to_json(clusters):
return {'clusters': _iter_on_rows(clusters,
- datetime.utcnow(), get_locale())}
+ datetime.utcnow(), get_locale()),
+ 'notifications': get_notifications()}
diff --git a/src/web/views/admin.py b/src/web/views/admin.py
index 1103635b2..baa2b3d4b 100644
--- a/src/web/views/admin.py
+++ b/src/web/views/admin.py
@@ -50,7 +50,7 @@ def user(user_id=None):
unread_counts=clu_contr.count_by_feed(read=False))
else:
- flash(gettext('This user does not exist.'), 'warn')
+ flash(gettext('This user does not exist.'), 'warning')
return redirect(redirect_url())
@@ -67,7 +67,7 @@ def toggle_user(user_id=None):
{'is_active': not user.is_active})
if not user_changed:
- flash(gettext('This user does not exist.'), 'danger')
+ flash(gettext('This user does not exist.'), 'error')
return redirect(url_for('admin.dashboard'))
else:
diff --git a/src/web/views/feed.py b/src/web/views/feed.py
index 050250858..0c060abd2 100644
--- a/src/web/views/feed.py
+++ b/src/web/views/feed.py
@@ -1,5 +1,4 @@
import logging
-import requests.exceptions
from werkzeug.exceptions import BadRequest
from flask import Blueprint, render_template, flash, \
diff --git a/src/web/views/home.py b/src/web/views/home.py
index 49187fb27..2f2583386 100644
--- a/src/web/views/home.py
+++ b/src/web/views/home.py
@@ -9,7 +9,7 @@
from bootstrap import conf
from web.lib.article_cleaner import clean_urls
-from web.lib.view_utils import etag_match, clusters_to_json
+from web.lib.view_utils import etag_match, clusters_to_json, get_notifications
from web.views.common import jsonify
from web.controllers import (UserController, CategoryController,
@@ -71,6 +71,7 @@ def get_menu():
'max_error': conf.FEED_ERROR_MAX,
'error_threshold': conf.FEED_ERROR_THRESHOLD,
'is_admin': current_user.is_admin,
+ 'notifications': get_notifications(),
'all_unread_count': 0}
@@ -144,7 +145,7 @@ def get_cluster(cluster_id, parse=False, article_id=None):
new_content = clean_urls(new_content, article['link'],
fix_readability=True)
except Exception as error:
- flash("Readability failed with %r" % error, "danger")
+ flash("Readability failed with %r" % error, "warning")
article['readability_parsed'] = False
else:
article['readability_parsed'] = True
@@ -153,6 +154,7 @@ def get_cluster(cluster_id, parse=False, article_id=None):
{'readability_parsed': True, 'content': new_content})
for article in cluster.articles:
article['readability_available'] = readability_available
+ cluster['notifications'] = get_notifications()
return cluster
diff --git a/src/web/views/user.py b/src/web/views/user.py
index b883ab9a4..dae1222ef 100644
--- a/src/web/views/user.py
+++ b/src/web/views/user.py
@@ -45,7 +45,7 @@ def opml_import():
try:
subscriptions = opml.from_string(data.read())
except:
- flash(gettext("Couldn't parse file"), 'danger')
+ flash(gettext("Couldn't parse file"), 'error')
return redirect(request.referrer)
ccontr = CategoryController(current_user.id)
diff --git a/src/web/views/views.py b/src/web/views/views.py
index 7588ea998..3ce3e1804 100644
--- a/src/web/views/views.py
+++ b/src/web/views/views.py
@@ -21,7 +21,7 @@ def authentication_required(error):
def authentication_failed(error):
if conf.API_ROOT in request.url:
return error
- flash(gettext('Forbidden.'), 'danger')
+ flash(gettext('Forbidden.'), 'error')
return redirect(url_for('login'))