mirror of https://github.com/jitsi/jitsi-meet
feat(notifications): implement a react/redux notification system (#1786)
* feat(notifications): implement a react/redux notification system * squash into impl explicit timeout, style * ref(notifications): convert toastr notifications to use react * ref(toastr): remove library * squash into conversion: pass timeout * squash into clean remove from debian patchpull/1841/head jitsi-meet_2309
parent
e818fa1e9e
commit
da1c760abf
@ -0,0 +1,24 @@ |
||||
/* |
||||
* The type of (redux) action which signals that a specific notification should |
||||
* not be displayed anymore. |
||||
* |
||||
* { |
||||
* type: HIDE_NOTIFICATION, |
||||
* uid: string |
||||
* } |
||||
*/ |
||||
export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION'); |
||||
|
||||
/* |
||||
* The type of (redux) action which signals that a notification component should |
||||
* be displayed. |
||||
* |
||||
* { |
||||
* type: SHOW_NOTIFICATION, |
||||
* component: ReactComponent, |
||||
* props: Object, |
||||
* timeout: number, |
||||
* uid: number |
||||
* } |
||||
*/ |
||||
export const SHOW_NOTIFICATION = Symbol('SHOW_NOTIFICATION'); |
@ -0,0 +1,47 @@ |
||||
import { |
||||
HIDE_NOTIFICATION, |
||||
SHOW_NOTIFICATION |
||||
} from './actionTypes'; |
||||
|
||||
/** |
||||
* Removes the notification with the passed in id. |
||||
* |
||||
* @param {string} uid - The unique identifier for the notification to be |
||||
* removed. |
||||
* @returns {{ |
||||
* type: HIDE_NOTIFICATION, |
||||
* uid: string |
||||
* }} |
||||
*/ |
||||
export function hideNotification(uid) { |
||||
return { |
||||
type: HIDE_NOTIFICATION, |
||||
uid |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Queues a notification for display. |
||||
* |
||||
* @param {ReactComponent} component - The notification component to be |
||||
* displayed. |
||||
* @param {Object} props - The props needed to show the notification component. |
||||
* @param {number} timeout - How long the notification should display before |
||||
* automatically being hidden. |
||||
* @returns {{ |
||||
* type: SHOW_NOTIFICATION, |
||||
* component: ReactComponent, |
||||
* props: Object, |
||||
* timeout: number, |
||||
* uid: number |
||||
* }} |
||||
*/ |
||||
export function showNotification(component, props = {}, timeout) { |
||||
return { |
||||
type: SHOW_NOTIFICATION, |
||||
component, |
||||
props, |
||||
timeout, |
||||
uid: window.Date.now() |
||||
}; |
||||
} |
@ -0,0 +1,100 @@ |
||||
import Flag from '@atlaskit/flag'; |
||||
import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info'; |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} to display a notification. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class Notification extends Component { |
||||
/** |
||||
* {@code Notification} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The translation key to display as the title of the notification if |
||||
* no title is provided. |
||||
*/ |
||||
defaultTitleKey: React.PropTypes.string, |
||||
|
||||
/** |
||||
* The translation arguments that may be necessary for the description. |
||||
*/ |
||||
descriptionArguments: React.PropTypes.object, |
||||
|
||||
/** |
||||
* The translation key to use as the body of the notification. |
||||
*/ |
||||
descriptionKey: React.PropTypes.string, |
||||
|
||||
/** |
||||
* Whether or not the dismiss button should be displayed. This is passed |
||||
* in by {@code FlagGroup}. |
||||
*/ |
||||
isDismissAllowed: React.PropTypes.bool, |
||||
|
||||
/** |
||||
* Callback invoked when the user clicks to dismiss the notification. |
||||
* this is passed in by {@code FlagGroup}. |
||||
*/ |
||||
onDismissed: React.PropTypes.func, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func, |
||||
|
||||
/** |
||||
* The text to display at the top of the notification. If not passed in, |
||||
* the passed in defaultTitleKey will be used. |
||||
*/ |
||||
title: React.PropTypes.string, |
||||
|
||||
/** |
||||
* The unique identifier for the notification. Passed back by the |
||||
* {@code Flag} component in the onDismissed callback. |
||||
*/ |
||||
uid: React.PropTypes.number |
||||
}; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
defaultTitleKey, |
||||
descriptionArguments, |
||||
descriptionKey, |
||||
isDismissAllowed, |
||||
onDismissed, |
||||
t, |
||||
title, |
||||
uid |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<Flag |
||||
appearance = 'normal' |
||||
description = { t(descriptionKey, descriptionArguments) } |
||||
icon = { ( |
||||
<EditorInfoIcon |
||||
label = 'info' |
||||
size = 'medium' /> |
||||
) } |
||||
id = { uid } |
||||
isDismissAllowed = { isDismissAllowed } |
||||
onDismissed = { onDismissed } |
||||
title = { title || t(defaultTitleKey) } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(Notification); |
@ -0,0 +1,157 @@ |
||||
import { FlagGroup } from '@atlaskit/flag'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { hideNotification } from '../actions'; |
||||
|
||||
/** |
||||
* The duration for which a notification should be displayed before being |
||||
* dismissed automatically. |
||||
* |
||||
* @type {number} |
||||
*/ |
||||
const DEFAULT_NOTIFICATION_TIMEOUT = 2500; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays notifications and handles |
||||
* automatic dismissmal after a notification is shown for a defined timeout |
||||
* period. |
||||
* |
||||
* @extends {Component} |
||||
*/ |
||||
class NotificationsContainer extends Component { |
||||
/** |
||||
* {@code NotificationsContainer} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The notifications to be displayed, with the first index being the |
||||
* notification at the top and the rest shown below it in order. |
||||
*/ |
||||
_notifications: React.PropTypes.array, |
||||
|
||||
/** |
||||
* Invoked to update the redux store in order to remove notifications. |
||||
*/ |
||||
dispatch: React.PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code NotificationsContainer} instance. |
||||
* |
||||
* @param {Object} props - The read-only React Component props with which |
||||
* the new instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
/** |
||||
* The timeout set for automatically dismissing a displayed |
||||
* notification. This value is set on the instance and not state to |
||||
* avoid additional re-renders. |
||||
* |
||||
* @type {number|null} |
||||
*/ |
||||
this._notificationDismissTimeout = null; |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDismissed = this._onDismissed.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Sets a timeout if the currently displayed notification has changed. |
||||
* |
||||
* @inheritdoc |
||||
* returns {void} |
||||
*/ |
||||
componentDidUpdate() { |
||||
const { _notifications } = this.props; |
||||
|
||||
if (_notifications.length && !this._notificationDismissTimeout) { |
||||
const notification = _notifications[0]; |
||||
const { timeout, uid } = notification; |
||||
|
||||
this._notificationDismissTimeout = setTimeout(() => { |
||||
this._onDismissed(uid); |
||||
}, timeout || DEFAULT_NOTIFICATION_TIMEOUT); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Clear any dismissal timeout that is still active. |
||||
* |
||||
* @inheritdoc |
||||
* returns {void} |
||||
*/ |
||||
componentWillUnmount() { |
||||
clearTimeout(this._notificationDismissTimeout); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { _notifications } = this.props; |
||||
|
||||
const flags = _notifications.map(notification => { |
||||
const Notification = notification.component; |
||||
const { props, uid } = notification; |
||||
|
||||
// The id attribute is necessary as {@code FlagGroup} looks for
|
||||
// either id or key to set a key on notifications, but accessing
|
||||
// props.key will cause React to print an error.
|
||||
return ( |
||||
<Notification |
||||
{ ...props } |
||||
id = { uid } |
||||
key = { uid } |
||||
uid = { uid } /> |
||||
|
||||
); |
||||
}); |
||||
|
||||
return ( |
||||
<FlagGroup onDismissed = { this._onDismissed }> |
||||
{ flags } |
||||
</FlagGroup> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Emits an action to remove the notification from the redux store so it |
||||
* stops displaying. |
||||
* |
||||
* @param {number} flagUid - The id of the notification to be removed. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onDismissed(flagUid) { |
||||
clearTimeout(this._notificationDismissTimeout); |
||||
this._notificationDismissTimeout = null; |
||||
|
||||
this.props.dispatch(hideNotification(flagUid)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated NotificationsContainer's |
||||
* props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _notifications: React.PropTypes.array |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
return { |
||||
_notifications: state['features/notifications'] |
||||
}; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(NotificationsContainer); |
@ -0,0 +1,2 @@ |
||||
export { default as Notification } from './Notification'; |
||||
export { default as NotificationsContainer } from './NotificationsContainer'; |
@ -0,0 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
export * from './components'; |
||||
|
||||
import './reducer'; |
@ -0,0 +1,43 @@ |
||||
import { ReducerRegistry } from '../base/redux'; |
||||
|
||||
import { |
||||
HIDE_NOTIFICATION, |
||||
SHOW_NOTIFICATION |
||||
} from './actionTypes'; |
||||
|
||||
/** |
||||
* The initial state of the feature notifications. |
||||
* |
||||
* @type {array} |
||||
*/ |
||||
const DEFAULT_STATE = []; |
||||
|
||||
/** |
||||
* Reduces redux actions which affect the display of notifications. |
||||
* |
||||
* @param {Object} state - The current redux state. |
||||
* @param {Object} action - The redux action to reduce. |
||||
* @returns {Object} The next redux state which is the result of reducing the |
||||
* specified {@code action}. |
||||
*/ |
||||
ReducerRegistry.register('features/notifications', |
||||
(state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case HIDE_NOTIFICATION: |
||||
return state.filter( |
||||
notification => notification.uid !== action.uid); |
||||
|
||||
case SHOW_NOTIFICATION: |
||||
return [ |
||||
...state, |
||||
{ |
||||
component: action.component, |
||||
props: action.props, |
||||
timeout: action.timeout, |
||||
uid: action.uid |
||||
} |
||||
]; |
||||
} |
||||
|
||||
return state; |
||||
}); |
Loading…
Reference in new issue