diff --git a/react/src/actions/actionCreators.js b/react/src/actions/actionCreators.js index fe8d8b6..19841c2 100644 --- a/react/src/actions/actionCreators.js +++ b/react/src/actions/actionCreators.js @@ -20,7 +20,8 @@ try { Config = _config; } -export const TOASTER_MESSAGE = 'TOASTER_MESSAGE'; +export const ADD_TOASTER_MESSAGE = 'ADD_TOASTER_MESSAGE'; +export const REMOVE_TOASTER_MESSAGE = 'REMOVE_TOASTER_MESSAGE'; export const DISPLAY_ADDCOIN_MODAL = 'DISPLAY_ADDCOIN_MODAL'; export const GET_ACTIVE_COINS = 'GET_ACTIVE_COINS'; export const LOGIN = 'LOGIN'; @@ -195,7 +196,7 @@ function toggleSendReceiveCoinFormsState() { export function triggerToaster(display, message, title, _type) { return { - type: TOASTER_MESSAGE, + type: ADD_TOASTER_MESSAGE, display, message, title, @@ -203,6 +204,14 @@ export function triggerToaster(display, message, title, _type) { } } +// triggers removing of the toast with the provided toastId +export function dismissToaster(toastId) { + return { + type: REMOVE_TOASTER_MESSAGE, + toastId: toastId + } +} + function toggleAddcoinModalState(display, isLogin) { return { type: DISPLAY_ADDCOIN_MODAL, @@ -378,9 +387,9 @@ export function copyCoinAddress(address) { } } -export function dismissToasterMessage() { +export function dismissToasterMessage(toastId) { return dispatch => { - dispatch(triggerToaster(false)) + dispatch(dismissToaster(toastId)) } } diff --git a/react/src/components/toaster/toaster-item.js b/react/src/components/toaster/toaster-item.js new file mode 100644 index 0000000..94904d7 --- /dev/null +++ b/react/src/components/toaster/toaster-item.js @@ -0,0 +1,79 @@ +import React from "react"; +import {dismissToasterMessage} from "../../actions/actionCreators"; +import Store from "../../store"; + +// each toast will be displayed for 5 seconds +const DISPLAY_LENGTH_MILLIS = 5000; + +/** + * Displays one toast message + * each messages has a type, title and a content message + */ +class ToasterItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + display: false, + message: null, + type: null, + title: null + }; + + this.dismissToast = this.dismissToast.bind(this); + + this.timeoutHandler = null; + } + + componentWillReceiveProps(props) { + if (props && + props.message && + props.display) { + this.setState({ + message: props.message, + display: props.display, + type: props._type, + title: props.title, + toastId: props.toastId + }); + } else { + this.setState({ + display: false, + message: null, + type: null, + title: null, + toastId: null + }); + } + } + + dismissToast(toastId) { + Store.dispatch(dismissToasterMessage(toastId)); + } + + renderToast() { + // ensure that setTimeout is called only once for each toast message + if (!this.timeoutHandler) { + this.timeoutHandler = setTimeout(() => { + this.dismissToast(this.state.toastId); + }, DISPLAY_LENGTH_MILLIS); + } + + return ( +
+ +
{ this.state.title }
+
{ this.state.message }
+
+ ); + } + + render() { + return (this.state.message && this.state.display) ? + this.renderToast() : null; + } +} + +export default ToasterItem; diff --git a/react/src/components/toaster/toaster.js b/react/src/components/toaster/toaster.js index 2e94586..d8fdf6d 100644 --- a/react/src/components/toaster/toaster.js +++ b/react/src/components/toaster/toaster.js @@ -1,70 +1,60 @@ -import React from 'react'; -import { dismissToasterMessage } from '../../actions/actionCreators'; -import Store from '../../store'; +import React from "react"; +import {dismissToasterMessage} from "../../actions/actionCreators"; +import Store from "../../store"; +import ToasterItem from "./toaster-item"; +/** + * Container component used for creating multiple toasts + */ class Toaster extends React.Component { constructor(props) { super(props); this.state = { - display: false, - message: null, - type: null, - title: null, + toasts: [] }; - this.dismissToast = this.dismissToast.bind(this); + this.toastId = 0; } componentWillReceiveProps(props) { if (props && - props.message && - props.display) { + props.toasts) { this.setState({ - message: props.message, - display: props.display, - type: props.type, - title: props.title, + toasts: props.toasts, + toastId: props.toasts.length }); } else { this.setState({ - display: false, - message: null, - type: null, - title: null, + toasts: [], + toastId: 0 }); } } - dismissToast() { - Store.dispatch(dismissToasterMessage()); + dismissToast(toastId) { + Store.dispatch(dismissToasterMessage(toastId)); } - // TODO: multiple toasts - renderToast() { - setTimeout(() => { - Store.dispatch(dismissToasterMessage()); - }, 5000); - + // render all current toasts + render() { return ( -
- + ); } - - render() { - return (this.state.message && this.state.display) ? - this.renderToast() : null; - } } -export default Toaster; +export default Toaster; \ No newline at end of file diff --git a/react/src/reducers/toaster.js b/react/src/reducers/toaster.js index 7ea1ed8..e0ddc4e 100644 --- a/react/src/reducers/toaster.js +++ b/react/src/reducers/toaster.js @@ -1,19 +1,21 @@ -import { TOASTER_MESSAGE } from '../actions/actionCreators'; +import {ADD_TOASTER_MESSAGE, REMOVE_TOASTER_MESSAGE} from "../actions/actionCreators"; export function toaster(state = { - display: false, - message: null, - title: null, - type: null, + toasts: [], }, action) { + if (state === null) state = {toasts: []}; switch (action.type) { - case TOASTER_MESSAGE: - return Object.assign({}, state, { - display: action.display, - message: action.message, - title: action.title, - type: action._type, - }); + case ADD_TOASTER_MESSAGE: + return { + ...state, + toasts: [...state.toasts, action] + }; + case REMOVE_TOASTER_MESSAGE: + // filter out the toastId that should be removed + return { + ...state, + toasts: state.toasts.filter(t => t.toastId !== action.toastId) + }; default: return state; }