Add notification system

This commit is contained in:
John Furrow
2016-05-15 21:02:29 -07:00
parent 84e910c5cf
commit 4450b8b830
13 changed files with 241 additions and 59 deletions
@@ -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;
}
}
}
}
@@ -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 {
@@ -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);
+1
View File
@@ -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";
@@ -13,6 +13,7 @@ const TorrentActions = {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_ADD_TORRENT_SUCCESS,
data: {
request: options,
response
}
});
+2
View File
@@ -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 {
<TorrentListView />
</ApplicationContent>
<Modals />
<Notifications />
</Application>
);
}
@@ -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 (
<li className="notifications__list__item" key={index}>
{notification.content}
</li>
);
});
}
handleNotificationChange() {
this.setState({notifications: NotificationStore.getNotifications()});
}
render() {
let notifications = null;
if (this.state.notifications.length > 0) {
notifications = (
<ul className="notifications__list" key="notifications-list">
{this.getNotifications()}
</ul>
);
}
return (
<CSSTransitionGroup transitionName="notifications__list"
transitionEnterTimeout={250} transitionLeaveTimeout={250}
className="notifications">
{notifications}
</CSSTransitionGroup>
);
}
}
@@ -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 (
<ApplicationPanel modifier="torrent-list" className={classes}>
<ApplicationPanel modifier="torrent-list" className="view--torrent-list">
<ActionBar />
<TorrentListContainer />
</ApplicationPanel>
@@ -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',
+1 -1
View File
@@ -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);
@@ -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;
+17 -1
View File
@@ -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) {
-20
View File
@@ -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;