Improve notifications

This commit is contained in:
John Furrow
2016-05-21 14:36:12 -07:00
parent 7748660783
commit e8515ac16b
13 changed files with 252 additions and 50 deletions
@@ -13,7 +13,8 @@ const TorrentActions = {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_ADD_TORRENT_SUCCESS,
data: {
request: options,
count: options.urls.length,
destination: options.destination,
response
}
});
@@ -37,6 +38,8 @@ const TorrentActions = {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_ADD_TORRENT_SUCCESS,
data: {
count: filesData.getAll('torrents').length,
destination,
response
}
});
@@ -59,13 +62,19 @@ const TorrentActions = {
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_REMOVE_TORRENT_SUCCESS,
data
data: {
data,
count: hash.length
}
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_REMOVE_TORRENT_ERROR,
error
error: {
error,
count: hash.length
}
});
});
},
@@ -169,7 +178,10 @@ const TorrentActions = {
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS,
data
data: {
data,
count: hashes.length
}
});
})
.catch((error) => {
@@ -0,0 +1,16 @@
import React from 'react';
import BaseIcon from './BaseIcon';
export default class CircleCheckmarkIcon extends BaseIcon {
render() {
return (
<svg className={`icon icon--circle-checkmark ${this.props.className}`}
xmlns={this.getXmlns()} viewBox={this.getViewBox()}>
<path fillOpacity="0.05" d="M30,0A30,30,0,1,1,0,30,30,30,0,0,1,30,0Z"/>
<path fillOpacity="0.2" d="M30,0A30,30,0,1,0,60,30,30,30,0,0,0,30,0Zm0,56.47A26.47,26.47,0,1,1,56.47,30,26.47,26.47,0,0,1,30,56.47Z"/>
<polygon points="43.93 19.51 27.64 35.46 19.07 27.07 16.5 29.58 27.64 40.5 46.5 22.03 43.93 19.51"/>
</svg>
);
}
}
@@ -0,0 +1,16 @@
import React from 'react';
import BaseIcon from './BaseIcon';
export default class CircleExclamationIcon extends BaseIcon {
render() {
return (
<svg className={`icon icon--circle-checkmark ${this.props.className}`}
xmlns={this.getXmlns()} viewBox={this.getViewBox()}>
<path fillOpacity="0.05" d="M30,0A30,30,0,1,1,0,30,30,30,0,0,1,30,0Z"/>
<path fillOpacity="0.2" d="M30,0A30,30,0,1,0,60,30,30,30,0,0,0,30,0Zm0,56.47A26.47,26.47,0,1,1,56.47,30,26.47,26.47,0,0,1,30,56.47Z"/>
<path d="M30,39.18a3.12,3.12,0,0,1,2.26.83,3,3,0,0,1,0,4.21,3.48,3.48,0,0,1-4.5,0,2.79,2.79,0,0,1-.86-2.1A2.82,2.82,0,0,1,27.75,40,3.07,3.07,0,0,1,30,39.18Zm2.31-3H27.68L27,16.72H33Z"/>
</svg>
);
}
}
@@ -8,7 +8,6 @@ import Close from '../icons/Close';
import File from '../icons/File';
import Files from '../icons/Files';
import ModalActions from './ModalActions';
import SettingsStore from '../../stores/SettingsStore';
import TorrentActions from '../../actions/TorrentActions';
const METHODS_TO_BIND = [
@@ -75,7 +74,7 @@ export default class AddTorrentsByFile extends React.Component {
<File />
</span>
<span className="dropzone__file__item dropzone__file__item--file-name">
{file.name}{file.name}
{file.name}
</span>
<span className="dropzone__file__item dropzone__file__item--icon dropzone__file__item--remove-icon" onClick={this.handleFileRemove.bind(this, index)}>
<Close />
@@ -106,8 +105,6 @@ export default class AddTorrentsByFile extends React.Component {
return;
}
SettingsStore.saveSettings({id: 'torrentDestination', data: this.state.destination});
this.setState({isAddingTorrents: true});
let fileData = new FormData();
@@ -2,7 +2,6 @@ import React from 'react';
import AddTorrentsActions from './AddTorrentsActions';
import AddTorrentsDestination from './AddTorrentsDestination';
import SettingsStore from '../../stores/SettingsStore';
import TextboxRepeater from '../forms/TextboxRepeater';
import TorrentActions from '../../actions/TorrentActions';
@@ -21,7 +20,7 @@ export default class AddTorrentsByURL extends React.Component {
this.state = {
addTorrentsError: null,
destination: null,
destination: '',
isAddingTorrents: false,
urlTextboxes: [{value: ''}],
startTorrents: true
@@ -40,7 +39,6 @@ export default class AddTorrentsByURL extends React.Component {
destination: this.state.destination,
start: this.state.startTorrents
});
SettingsStore.saveSettings({id: 'torrentDestination', data: this.state.destination});
}
handleDestinationChange(destination) {
@@ -11,7 +11,7 @@ export default class AddTorrentsDestination extends React.Component {
super();
this.state = {
destination: null
destination: ''
};
METHODS_TO_BIND.forEach((method) => {
@@ -20,7 +20,7 @@ export default class AddTorrentsDestination extends React.Component {
}
componentWillMount() {
let destination = SettingsStore.getSettings('torrentDestination');
let destination = SettingsStore.getSettings('torrentDestination') || '';
if (this.props.suggested) {
destination = this.props.suggested;
}
@@ -0,0 +1,55 @@
import classnames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
import CircleCheckmarkIcon from '../icons/CircleCheckmarkIcon';
import CircleExclamationIcon from '../icons/CircleExclamationIcon';
import stringUtil from '../../../../../shared/util/stringUtil';
export default class Notification extends React.Component {
render() {
let icon = <CircleCheckmarkIcon />;
let countText = null;
let itemText = this.props.subject;
let notificationClasses = classnames('notification', {
'is-success': this.props.type === 'success',
'is-error': this.props.type === 'error'
});
if (this.props.type === 'error') {
icon = <CircleExclamationIcon />;
}
if (this.props.count !== 1) {
countText = (
<span className="notification__count">
{this.props.count}
</span>
);
itemText = stringUtil.pluralize(itemText, this.props.count);
}
return (
<li className={notificationClasses}>
{icon}
<span className="notification__content">
{this.props.adverb} {this.props.action} {countText} {itemText}.
</span>
</li>
);
}
}
Notification.defaultProps = {
count: 0,
type: 'success'
};
Notification.propTypes = {
count: React.PropTypes.number,
action: React.PropTypes.string.isRequired,
adverb: React.PropTypes.string.isRequired,
subject: React.PropTypes.string.isRequired,
subject: React.PropTypes.string
};
@@ -4,6 +4,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import EventTypes from '../../constants/EventTypes';
import Notification from './Notification';
import NotificationStore from '../../stores/NotificationStore';
const METHODS_TO_BIND = ['handleNotificationChange'];
@@ -31,11 +32,7 @@ export default class NotificationList extends React.Component {
getNotifications() {
return this.state.notifications.map((notification, index) => {
return (
<li className="notifications__list__item" key={index}>
{notification.content}
</li>
);
return <Notification {...notification} key={index} />;
});
}
@@ -27,31 +27,17 @@ class NotificationStoreClass extends BaseStore {
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.notifications[notification.id] = 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;
}
@@ -60,7 +46,13 @@ class NotificationStoreClass extends BaseStore {
let notificationIDs = Object.keys(this.notifications).sort();
return notificationIDs.map((id) => {
return this.notifications[id];
let notification = this.notifications[id];
if (!!notification.accumulation) {
notification.count = this.accumulation[notification.accumulation.id];
}
return notification;
});
}
+81 -16
View File
@@ -9,6 +9,7 @@ import {filterTorrents} from '../util/filterTorrents';
import NotificationStore from './NotificationStore';
import {searchTorrents} from '../util/searchTorrents';
import {selectTorrents} from '../util/selectTorrents';
import SettingsStore from './SettingsStore';
import {sortTorrents} from '../util/sortTorrents';
import TorrentActions from '../actions/TorrentActions';
import TorrentFilterStore from './TorrentFilterStore';
@@ -100,22 +101,20 @@ class TorrentStoreClass extends BaseStore {
this.emit(EventTypes.CLIENT_ADD_TORRENT_ERROR);
}
handleAddTorrentSuccess(responseData) {
handleAddTorrentSuccess(response) {
this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS);
NotificationStore.add({
content: function (count = 0) {
if (count === 1) {
return 'Successfully added torrent.';
}
SettingsStore.saveSettings({id: 'torrentDestination', data: response.destination});
return `Successfully added ${count} torrents.`;
},
NotificationStore.add({
adverb: 'Successfully',
action: 'added',
subject: 'torrent',
accumulation: {
id: 'add-torrents',
value: responseData.request.urls.length || 1
id: 'add-torrents-success',
value: response.count || 1
},
id: 'add-torrents'
id: 'add-torrents-success'
});
}
@@ -135,12 +134,34 @@ class TorrentStoreClass extends BaseStore {
return this.sortedTorrents;
}
handleMoveTorrentsSuccess(data) {
handleMoveTorrentsSuccess(response) {
this.emit(EventTypes.CLIENT_MOVE_TORRENTS_SUCCESS);
NotificationStore.add({
adverb: 'Successfully',
action: 'moved',
accumulation: {
id: 'move-torrents-success',
value: response.count
},
id: 'move-torrents-success',
subject: 'torrent'
});
}
handleMoveTorrentsError(error) {
this.emit(EventTypes.CLIENT_MOVE_TORRENTS_REQUEST_ERROR);
NotificationStore.add({
adverb: 'Failed to',
action: 'move',
subject: 'torrent',
accumulation: {
id: 'move-torrents-error',
value: error.count
},
id: 'move-torrents-error'
});
}
setSelectedTorrents(event, hash) {
@@ -153,11 +174,23 @@ class TorrentStoreClass extends BaseStore {
this.emit(EventTypes.UI_TORRENT_SELECTION_CHANGE);
}
handleFetchTorrentsError(action) {
console.log(action);
handleFetchTorrentsError(error) {
console.log(error);
}
handleFetchTorrentsSuccess(torrents) {
NotificationStore.add({
adverb: 'Successfully',
action: 'fetched',
duration: 20000,
subject: 'torrent',
accumulation: {
id: 'remove-torrents-error',
value: 1
},
id: 'remove-torrents-error'
});
this.sortTorrents(torrents);
this.filterTorrents();
@@ -165,6 +198,32 @@ class TorrentStoreClass extends BaseStore {
this.resolveRequest('fetch-torrents');
}
handleRemoveTorrentsSuccess(response) {
NotificationStore.add({
adverb: 'Successfully',
action: 'removed',
subject: 'torrent',
accumulation: {
id: 'remove-torrents-error',
value: response.count
},
id: 'remove-torrents-error'
});
}
handleRemoveTorrentsError(error) {
NotificationStore.add({
adverb: 'Failed to',
action: 'remove',
subject: 'torrent',
accumulation: {
id: 'remove-torrents-error',
value: error.count
},
id: 'remove-torrents-error'
});
}
setTorrentDetails(hash, torrentDetails) {
this.torrents[hash].details = torrentDetails;
this.resolveRequest('fetch-torrent-details');
@@ -231,14 +290,20 @@ TorrentStore.dispatcherID = AppDispatcher.register((payload) => {
case ActionTypes.CLIENT_FETCH_TORRENTS_SUCCESS:
TorrentStore.handleFetchTorrentsSuccess(action.data.torrents);
break;
case ActionTypes.CLIENT_FETCH_TORRENTS_ERROR:
TorrentStore.handleFetchTorrentsError(action.error);
break;
case ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS:
TorrentStore.handleMoveTorrentsSuccess(action.data);
break;
case ActionTypes.CLIENT_MOVE_TORRENTS_ERROR:
TorrentStore.handleMoveTorrentsError(action.error);
break;
case ActionTypes.CLIENT_FETCH_TORRENTS_ERROR:
TorrentStore.handleFetchTorrentsError();
case ActionTypes.CLIENT_REMOVE_TORRENT_SUCCESS:
TorrentStore.handleRemoveTorrentsSuccess(action.data);
break;
case ActionTypes.CLIENT_REMOVE_TORRENT_ERROR:
TorrentStore.handleRemoveTorrentsError(action.error);
break;
case ActionTypes.UI_CLICK_TORRENT:
TorrentStore.setSelectedTorrents(action.data.event, action.data.hash);