From 84bc57fd32b82091848bd5a31494201b0eed8e3b Mon Sep 17 00:00:00 2001 From: John Furrow Date: Tue, 20 Sep 2016 20:42:15 -0700 Subject: [PATCH] Add server-side notifications --- client/scripts/actions/FloodActions.js | 23 +++++++ client/scripts/components/Panels/Sidebar.js | 2 + .../components/Sidebar/NotificationsButton.js | 59 +++++++++++++++++ client/scripts/constants/ActionTypes.js | 2 + server/models/NotificationCollection.js | 65 +++++++++++++++++++ server/models/client.js | 3 + server/routes/api.js | 5 ++ 7 files changed, 159 insertions(+) create mode 100644 client/scripts/components/Sidebar/NotificationsButton.js create mode 100644 server/models/NotificationCollection.js diff --git a/client/scripts/actions/FloodActions.js b/client/scripts/actions/FloodActions.js index f7843496..719d3818 100644 --- a/client/scripts/actions/FloodActions.js +++ b/client/scripts/actions/FloodActions.js @@ -5,6 +5,29 @@ import ActionTypes from '../constants/ActionTypes'; import AuthStore from '../stores/AuthStore'; let FloodActions = { + fetchNotifications: () => { + return axios.get('/api/notifications') + .then((json = {}) => { + return json.data; + }) + .then((notifications) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.FLOOD_FETCH_NOTIFICATIONS_SUCCESS, + data: { + notifications + } + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.FLOOD_FETCH_NOTIFICATIONS_ERROR, + data: { + error + } + }); + }); + }, + fetchTransferData: () => { return axios.get('/api/stats') .then((json = {}) => { diff --git a/client/scripts/components/Panels/Sidebar.js b/client/scripts/components/Panels/Sidebar.js index 8b84d3b4..ea4ff4bd 100644 --- a/client/scripts/components/Panels/Sidebar.js +++ b/client/scripts/components/Panels/Sidebar.js @@ -5,6 +5,7 @@ import ClientStats from '../Sidebar/TransferData'; import CustomScrollbars from '../General/CustomScrollbars'; import EventTypes from '../../constants/EventTypes'; import FeedsButton from '../Sidebar/FeedsButton'; +import NotificationsButton from '../Sidebar/NotificationsButton'; import SearchTorrents from '../Sidebar/SearchTorrents'; import SettingsButton from '../Sidebar/SettingsButton'; import SidebarActions from '../Sidebar/SidebarActions'; @@ -53,6 +54,7 @@ class Sidebar extends React.Component { + diff --git a/client/scripts/components/Sidebar/NotificationsButton.js b/client/scripts/components/Sidebar/NotificationsButton.js new file mode 100644 index 00000000..e9e809c7 --- /dev/null +++ b/client/scripts/components/Sidebar/NotificationsButton.js @@ -0,0 +1,59 @@ +import {defineMessages, injectIntl} from 'react-intl'; +import React from 'react'; + +import FeedIcon from '../Icons/FeedIcon'; +import FloodActions from '../../actions/FloodActions'; +import Tooltip from '../General/Tooltip'; +import UIActions from '../../actions/UIActions'; + +const MESSAGES = defineMessages({ + notifications: { + id: 'sidebar.button.notifications', + defaultMessage: 'Notifications' + } +}); + +const METHODS_TO_BIND = ['handleNotificationsButtonClick']; + +class NotificationsButton extends React.Component { + constructor() { + super(); + + this.state = {isOpen: false}; + this.tooltipRef = null; + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + componentDidMount() { + FloodActions.fetchNotifications(); + } + + handleNotificationsButtonClick() { + if (this.tooltipRef != null) { + this.tooltipRef.dismissTooltip(); + } + + this.setState({isOpen: true}); + } + + render() { + let label = this.props.intl.formatMessage(MESSAGES.notifications); + + return ( + this.tooltipRef = ref} + position="bottom" + wrapperClassName="sidebar__action sidebar__icon-button + tooltip__wrapper"> + + + ); + } +} + +export default injectIntl(NotificationsButton); diff --git a/client/scripts/constants/ActionTypes.js b/client/scripts/constants/ActionTypes.js index 883151da..f4f8a426 100644 --- a/client/scripts/constants/ActionTypes.js +++ b/client/scripts/constants/ActionTypes.js @@ -45,6 +45,8 @@ const ACTION_TYPES = { CLIENT_START_TORRENT_SUCCESS: 'CLIENT_START_TORRENT_SUCCESS', CLIENT_STOP_TORRENT_ERROR: 'CLIENT_STOP_TORRENT_ERROR', CLIENT_STOP_TORRENT_SUCCESS: 'CLIENT_STOP_TORRENT_SUCCESS', + FLOOD_FETCH_NOTIFICATIONS_ERROR: 'FLOOD_FETCH_NOTIFICATIONS_ERROR', + FLOOD_FETCH_NOTIFICATIONS_SUCCESS: 'FLOOD_FETCH_NOTIFICATIONS_SUCCESS', SETTINGS_FEED_MONITOR_FEED_ADD_ERROR: 'SETTINGS_FEED_MONITOR_FEED_ADD_ERROR', SETTINGS_FEED_MONITOR_FEED_ADD_SUCCESS: 'SETTINGS_FEED_MONITOR_FEED_ADD_SUCCESS', SETTINGS_FEED_MONITOR_FEEDS_FETCH_ERROR: 'SETTINGS_FEED_MONITOR_FEEDS_FETCH_ERROR', diff --git a/server/models/NotificationCollection.js b/server/models/NotificationCollection.js new file mode 100644 index 00000000..ef027b56 --- /dev/null +++ b/server/models/NotificationCollection.js @@ -0,0 +1,65 @@ +'use strict'; + +let Datastore = require('nedb'); + +let config = require('../../config'); + +const DEFAULT_AGE_LIMIT = 1000 * 60; + +class NotificationCollection { + constructor() { + this.count = 0; + this.ready = false; + + this.db = this.loadDatabase(); + + this.countNotifications(); + } + + addNotification(notification) { + this.count++; + + this.db.insert({ + ts: Date.now(), + id: notification.id, + message: notification.message + }); + } + + countNotifications() { + this.db.count({}, (err, count) => { + if (err) { + this.count = 0; + } else { + this.count = count; + } + }); + } + + getNotifications(query, callback) { + let dbQuery = query.allNotifications ? {} : + {ts: {$gte: Date.now() - (query.ageLimit || DEFAULT_AGE_LIMIT)}}; + + // Return the notifications sorted by timestamp. + this.db.find(dbQuery).sort({ts: 1}).exec((err, docs) => { + if (err) { + callback(null, err); + return; + } + + callback({notifications: docs, count: this.count}); + }); + } + + loadDatabase() { + let db = new Datastore({ + autoload: true, + filename: `${config.dbPath}notifications.db` + }); + + this.ready = true; + return db; + } +} + +module.exports = new NotificationCollection(); diff --git a/server/models/client.js b/server/models/client.js index 11842983..29e17a6c 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -9,6 +9,7 @@ let ClientRequest = require('./ClientRequest'); let formatUtil = require('../../shared/util/formatUtil'); let scgi = require('../util/scgi'); let Torrent = require('./Torrent'); +let NotificationCollection = require('./NotificationCollection'); let TorrentCollection = require('./TorrentCollection'); let torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap'); let torrentGeneralPropsMap = require('../../shared/constants/torrentGeneralPropsMap'); @@ -199,6 +200,8 @@ var client = { tagCount = torrentCollection.getTagCount(); trackerCount = torrentCollection.getTrackerCount(); + NotificationCollection.addNotification({id: 'torrent-list-update', message: 'Torrent List Updated'}); + return torrentCollection.getTorrents(); }); request.onComplete(callback); diff --git a/server/routes/api.js b/server/routes/api.js index 1d9d3a56..5dc690bf 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -6,6 +6,7 @@ let ajaxUtil = require('../util/ajaxUtil'); let client = require('../models/client'); let clientRoutes = require('./client'); let FeedCollection = require('../models/FeedCollection'); +let NotificationCollection = require('../models/NotificationCollection'); let history = require('../models/history'); let passport = require('passport'); let router = express.Router(); @@ -45,6 +46,10 @@ router.get('/history', (req, res, next) => { history.get(req.query, ajaxUtil.getResponseFn(res)); }); +router.get('/notifications', (req, res, next) => { + NotificationCollection.getNotifications(req.query, ajaxUtil.getResponseFn(res)); +}); + router.get('/settings', (req, res, next) => { settings.get(req.query, ajaxUtil.getResponseFn(res)); });