From 4450b8b83052c790c14ea757b017a74535863d04 Mon Sep 17 00:00:00 2001 From: John Furrow Date: Sun, 15 May 2016 21:02:29 -0700 Subject: [PATCH] Add notification system --- .../sass/components/_notifications.scss | 35 ++++++ .../components/_torrent-details-panel.scss | 4 +- client/source/sass/components/_torrents.scss | 2 + client/source/sass/style.scss | 1 + .../source/scripts/actions/TorrentActions.js | 1 + client/source/scripts/app.js | 2 + .../components/notifications/Notifications.js | 65 ++++++++++ .../components/panels/TorrentListView.js | 36 +----- client/source/scripts/constants/EventTypes.js | 1 + client/source/scripts/stores/BaseStore.js | 2 +- .../scripts/stores/NotificationStore.js | 113 ++++++++++++++++++ client/source/scripts/stores/TorrentStore.js | 18 ++- client/source/scripts/stores/UIStore.js | 20 ---- 13 files changed, 241 insertions(+), 59 deletions(-) create mode 100644 client/source/sass/components/_notifications.scss create mode 100644 client/source/scripts/components/notifications/Notifications.js create mode 100644 client/source/scripts/stores/NotificationStore.js diff --git a/client/source/sass/components/_notifications.scss b/client/source/sass/components/_notifications.scss new file mode 100644 index 00000000..4f40e3f6 --- /dev/null +++ b/client/source/sass/components/_notifications.scss @@ -0,0 +1,35 @@ +$notification--background: rgba($background, 0.95); +$notification--foreground: #8fa2b2; + +.notifications { + + &__list { + background: $notification--background; + border-radius: 3px; + bottom: $spacing-unit * 1/5; + color: $notification--foreground; + font-size: 0.85rem; + padding: $spacing-unit * 2/5 $spacing-unit * 3/5; + position: fixed; + right: $spacing-unit * 1/5; + transition: opacity 0.25s; + width: 250px; + z-index: 1000; + + &-leave { + opacity: 1; + + &-active { + opacity: 0; + } + } + + &-enter { + opacity: 0; + + &-active { + opacity: 1; + } + } + } +} diff --git a/client/source/sass/components/_torrent-details-panel.scss b/client/source/sass/components/_torrent-details-panel.scss index 963e744c..89dc8208 100644 --- a/client/source/sass/components/_torrent-details-panel.scss +++ b/client/source/sass/components/_torrent-details-panel.scss @@ -12,7 +12,7 @@ $torrent-details--header--icon--default--fill: rgba(#4d6f87, 0.5); $torrent-details--detail--label--foreground: #527893; -$directory-tree--filename--foreground: rgba(#527893, 0.7); +$directory-tree--foreground: #527893; $directory-tree--directory--foreground: #527893; $directory-tree--directory--foreground--open: darken($directory-tree--directory--foreground, 5%); @@ -159,9 +159,9 @@ $torrent-details--directory-tree--parent-directory--icon--fill: rgba(#527893, 0. margin-left: -8px; .directory-tree { - color: $directory-tree--filename--foreground; &__node { + color: $directory-tree--foreground; &--group { diff --git a/client/source/sass/components/_torrents.scss b/client/source/sass/components/_torrents.scss index cd0d1d15..a64dc40b 100644 --- a/client/source/sass/components/_torrents.scss +++ b/client/source/sass/components/_torrents.scss @@ -105,6 +105,7 @@ $more-info--border: $textbox-repeater--button--border; &__more-info { opacity: 1; + pointer-events: auto; transform: translateX(0); } } @@ -127,6 +128,7 @@ $more-info--border: $textbox-repeater--button--border; margin-top: -16px; position: absolute; opacity: 0; + pointer-events: none; right: -5px; top: 50%; transform: translateX(15px); diff --git a/client/source/sass/style.scss b/client/source/sass/style.scss index de51b9fc..482812d3 100644 --- a/client/source/sass/style.scss +++ b/client/source/sass/style.scss @@ -28,6 +28,7 @@ @import "components/icons"; @import "components/loading-indicator"; @import "components/modals"; +@import "components/notifications"; @import "components/priority-meter"; @import "components/progress-bar"; @import "components/scrollbars"; diff --git a/client/source/scripts/actions/TorrentActions.js b/client/source/scripts/actions/TorrentActions.js index 1154033f..df50254e 100644 --- a/client/source/scripts/actions/TorrentActions.js +++ b/client/source/scripts/actions/TorrentActions.js @@ -13,6 +13,7 @@ const TorrentActions = { AppDispatcher.dispatchServerAction({ type: ActionTypes.CLIENT_ADD_TORRENT_SUCCESS, data: { + request: options, response } }); diff --git a/client/source/scripts/app.js b/client/source/scripts/app.js index a4726d5c..dee98cfd 100644 --- a/client/source/scripts/app.js +++ b/client/source/scripts/app.js @@ -5,6 +5,7 @@ import Application from './components/layout/Application'; import ApplicationContent from './components/layout/ApplicationContent'; import ApplicationLoadingIndicator from './components/layout/ApplicationLoadingIndicator'; import Modals from './components/modals/Modals'; +import Notifications from './components/notifications/Notifications'; import Sidebar from './components/panels/Sidebar'; import SettingsStore from './stores/SettingsStore'; import TorrentActions from './actions/TorrentActions'; @@ -24,6 +25,7 @@ class FloodApp extends React.Component { + ); } diff --git a/client/source/scripts/components/notifications/Notifications.js b/client/source/scripts/components/notifications/Notifications.js new file mode 100644 index 00000000..0532027d --- /dev/null +++ b/client/source/scripts/components/notifications/Notifications.js @@ -0,0 +1,65 @@ +import classnames from 'classnames'; +import CSSTransitionGroup from 'react-addons-css-transition-group'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import EventTypes from '../../constants/EventTypes'; +import NotificationStore from '../../stores/NotificationStore'; + +const METHODS_TO_BIND = ['handleNotificationChange']; + +export default class NotificationList extends React.Component { + constructor() { + super(...arguments); + + this.state = { + notifications: [] + }; + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + componentDidMount() { + NotificationStore.listen(EventTypes.NOTIFICATIONS_CHANGE, this.handleNotificationChange); + } + + componentWillUnmount() { + NotificationStore.unlisten(EventTypes.NOTIFICATIONS_CHANGE, this.handleNotificationChange); + } + + getNotifications() { + return this.state.notifications.map((notification, index) => { + return ( +
  • + {notification.content} +
  • + ); + }); + } + + handleNotificationChange() { + this.setState({notifications: NotificationStore.getNotifications()}); + } + + render() { + let notifications = null; + + if (this.state.notifications.length > 0) { + notifications = ( +
      + {this.getNotifications()} +
    + ); + } + + return ( + + {notifications} + + ); + } +} diff --git a/client/source/scripts/components/panels/TorrentListView.js b/client/source/scripts/components/panels/TorrentListView.js index f5c9be60..4029b8ac 100644 --- a/client/source/scripts/components/panels/TorrentListView.js +++ b/client/source/scripts/components/panels/TorrentListView.js @@ -1,47 +1,13 @@ -import classnames from 'classnames'; import React from 'react'; import ActionBar from '../torrent-list/ActionBar'; import ApplicationPanel from '../layout/ApplicationPanel'; -import EventTypes from '../../constants/EventTypes'; import TorrentListContainer from '../torrent-list/TorrentListContainer'; -import TorrentStore from '../../stores/TorrentStore'; -import UIStore from '../../stores/UIStore'; - -const METHODS_TO_BIND = ['onOpenChange']; class TorrentListView extends React.Component { - constructor() { - super(); - - this.state = { - isTorrentDetailsOpen: false - }; - - METHODS_TO_BIND.forEach((method) => { - this[method] = this[method].bind(this); - }); - } - - componentDidMount() { - UIStore.listen(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange); - } - - componentWillUnmount() { - UIStore.unlisten(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange); - } - - onOpenChange() { - this.setState({ - isTorrentDetailsOpen: UIStore.isTorrentDetailsOpen() - }); - } - render() { - let classes = classnames({'is-open': this.state.isTorrentDetailsOpen}, 'view--torrent-list'); - return ( - + diff --git a/client/source/scripts/constants/EventTypes.js b/client/source/scripts/constants/EventTypes.js index 78aa1051..cfb0815a 100644 --- a/client/source/scripts/constants/EventTypes.js +++ b/client/source/scripts/constants/EventTypes.js @@ -16,6 +16,7 @@ const EventTypes = { CLIENT_TRANSFER_DATA_REQUEST_ERROR: 'CLIENT_TRANSFER_DATA_REQUEST_ERROR', CLIENT_TRANSFER_HISTORY_REQUEST_SUCCESS: 'CLIENT_TRANSFER_HISTORY_REQUEST_SUCCESS', CLIENT_TRANSFER_HISTORY_REQUEST_ERROR: 'CLIENT_TRANSFER_HISTORY_REQUEST_ERROR', + NOTIFICATIONS_CHANGE: 'NOTIFICATIONS_CHANGE', SETTINGS_FETCH_REQUEST_SUCCESS: 'SETTINGS_FETCH_REQUEST_SUCCESS', SETTINGS_FETCH_REQUEST_ERROR: 'SETTINGS_FETCH_REQUEST_ERROR', UI_CONTEXT_MENU_CHANGE: 'UI_CONTEXT_MENU_CHANGE', diff --git a/client/source/scripts/stores/BaseStore.js b/client/source/scripts/stores/BaseStore.js index 34878407..6bdde7e8 100644 --- a/client/source/scripts/stores/BaseStore.js +++ b/client/source/scripts/stores/BaseStore.js @@ -2,7 +2,7 @@ import {EventEmitter} from 'events'; export default class BaseStore extends EventEmitter { constructor() { - super(); + super(...arguments); this.dispatcherID = null; this.on('uncaughtException', this.handleError); diff --git a/client/source/scripts/stores/NotificationStore.js b/client/source/scripts/stores/NotificationStore.js new file mode 100644 index 00000000..e8f69fa6 --- /dev/null +++ b/client/source/scripts/stores/NotificationStore.js @@ -0,0 +1,113 @@ +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import EventTypes from '../constants/EventTypes'; + +const DEFAULT_DURATION = 5 * 1000; + +class NotificationStoreClass extends BaseStore { + constructor() { + super(); + + this.notifications = {}; + this.accumulation = {}; + } + + accumulate(notification) { + let {id, value} = notification.accumulation; + + if (this.accumulation[id] == null) { + this.accumulation[id] = value; + } else { + this.accumulation[id] += value; + } + } + + add(notification) { + notification.duration = this.getDuration(notification); + notification.id = this.getID(notification); + + if (notification.content == null) { + throw new Error('Notification content cannot be empty.'); + } + + if (!!notification.accumulation) { + this.accumulate(notification); + } + + this.scheduleCleanse(notification); + + this.notifications[notification.id] = { + content: this.getContent(notification) + }; + + this.emit(EventTypes.NOTIFICATIONS_CHANGE); + } + + getContent(notification) { + if (!!notification.accumulation) { + return notification.content(this.accumulation[notification.accumulation.id]); + } + + return notification.content; + } + + getDuration(notification) { + return notification.duration || DEFAULT_DURATION; + } + + getNotifications() { + let notificationIDs = Object.keys(this.notifications).sort(); + + return notificationIDs.map((id) => { + return this.notifications[id]; + }); + } + + getID(notification) { + return notification.id || Date.now(); + } + + removeExpired(notification) { + let {accumulation} = notification; + + if (!!accumulation) { + this.removeAccumulation(notification); + + if (this.accumulation[accumulation.id] === 0) { + delete this.accumulation[accumulation.id]; + delete this.notifications[notification.id]; + } + } else { + delete this.notifications[notification.id]; + } + + this.emit(EventTypes.NOTIFICATIONS_CHANGE); + } + + removeAccumulation(notification) { + let {id, value} = notification.accumulation; + + if (this.accumulation[id] == null) { + return; + } + + this.accumulation[id] -= value; + } + + scheduleCleanse(notification) { + setTimeout(this.removeExpired.bind(this, notification), + notification.duration); + } +} + +let NotificationStore = new NotificationStoreClass(); + +NotificationStore.dispatcherID = AppDispatcher.register((payload) => { + // const {action, source} = payload; + + // switch (action.type) { + // } +}); + +export default NotificationStore; diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js index 7d9c67c5..08e4de36 100644 --- a/client/source/scripts/stores/TorrentStore.js +++ b/client/source/scripts/stores/TorrentStore.js @@ -6,6 +6,7 @@ import BaseStore from './BaseStore'; import config from '../../../../config'; import EventTypes from '../constants/EventTypes'; import {filterTorrents} from '../util/filterTorrents'; +import NotificationStore from './NotificationStore'; import {searchTorrents} from '../util/searchTorrents'; import {selectTorrents} from '../util/selectTorrents'; import {sortTorrents} from '../util/sortTorrents'; @@ -99,8 +100,23 @@ class TorrentStoreClass extends BaseStore { this.emit(EventTypes.CLIENT_ADD_TORRENT_ERROR); } - handleAddTorrentSuccess() { + handleAddTorrentSuccess(responseData) { this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS); + + NotificationStore.add({ + content: function (count = 0) { + if (count === 1) { + return 'Successfully added torrent.'; + } + + return `Successfully added ${count} torrents.`; + }, + accumulation: { + id: 'add-torrents', + value: responseData.request.urls.length || 1 + }, + id: 'add-torrents' + }); } getTorrent(hash) { diff --git a/client/source/scripts/stores/UIStore.js b/client/source/scripts/stores/UIStore.js index 0ee38ba0..9e00f748 100644 --- a/client/source/scripts/stores/UIStore.js +++ b/client/source/scripts/stores/UIStore.js @@ -15,16 +15,8 @@ class UIStoreClass extends BaseStore { this.dependencies = []; this.latestTorrentLocation = null; this.torrentDetailsHash = null; - // this.torrentDetailsOpen = false; } - // closeTorrentDetailsPanel() { - // if (this.torrentDetailsOpen) { - // this.torrentDetailsOpen = false; - // this.emit(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE); - // } - // } - getActiveContextMenu() { return this.activeContextMenu; } @@ -46,19 +38,10 @@ class UIStoreClass extends BaseStore { this.emit(EventTypes.UI_TORRENT_DETAILS_HASH_CHANGE); } - handleTorrentDetailsClick(hash, event) { - this.torrentDetailsOpen = !this.torrentDetailsOpen; - this.emit(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE); - } - hasSatisfiedDependencies() { return this.dependencies.length === 0; } - // isTorrentDetailsOpen() { - // return this.torrentDetailsOpen; - // } - registerDependency(ids) { if (!Array.isArray(ids)) { ids = [ids]; @@ -104,9 +87,6 @@ UIStore.dispatcherID = AppDispatcher.register((payload) => { const {action, source} = payload; switch (action.type) { - // case ActionTypes.UI_CLICK_TORRENT_DETAILS: - // UIStore.handleTorrentDetailsClick(action.data.hash, action.data.event); - // break; case ActionTypes.UI_CLICK_TORRENT: UIStore.handleTorrentClick(action.data.hash); break;