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 patch
This commit is contained in:
parent
e818fa1e9e
commit
da1c760abf
2
app.js
2
app.js
|
@ -13,8 +13,6 @@ import 'aui-experimental';
|
||||||
import 'aui-css';
|
import 'aui-css';
|
||||||
import 'aui-experimental-css';
|
import 'aui-experimental-css';
|
||||||
|
|
||||||
window.toastr = require('toastr');
|
|
||||||
|
|
||||||
import conference from './conference';
|
import conference from './conference';
|
||||||
import API from './modules/API';
|
import API from './modules/API';
|
||||||
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
|
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
|
||||||
|
|
107
css/_toastr.scss
107
css/_toastr.scss
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
* Toastr
|
|
||||||
* Copyright 2012-2014 John Papa and Hans Fjällemark.
|
|
||||||
* All Rights Reserved.
|
|
||||||
* Use, reproduction, distribution, and modification of this code is subject to the terms and
|
|
||||||
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
|
|
||||||
*
|
|
||||||
* Author: John Papa and Hans Fjällemark
|
|
||||||
* Project: https://github.com/CodeSeven/toastr
|
|
||||||
*
|
|
||||||
* Last updated: October 13, 2016
|
|
||||||
*/
|
|
||||||
|
|
||||||
.toast-title,
|
|
||||||
.toast-message .title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 3px;
|
|
||||||
@include text-truncate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-message {
|
|
||||||
-ms-word-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-message a,
|
|
||||||
.toast-message label,
|
|
||||||
.toast-message .connected,
|
|
||||||
.toast-message .disconnected {
|
|
||||||
color: $notificationLinkColor;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-message a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-message br {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// close button
|
|
||||||
.toast-close-button {
|
|
||||||
color: $notificationColor;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.2;
|
|
||||||
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
margin: -6px -10px 0 0;
|
|
||||||
float: right;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
@include opacity(0.4);
|
|
||||||
/* Additional properties for button version
|
|
||||||
iOS requires the button element instead of an anchor tag.
|
|
||||||
If you want the anchor version, it requires `href="#"`. */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-close-button:hover,
|
|
||||||
.toast-close-button:focus {
|
|
||||||
@include opacity(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
color: $notificationColor;
|
|
||||||
background-color: $notificationBackground;
|
|
||||||
|
|
||||||
font-size: $notificationFontSize;
|
|
||||||
|
|
||||||
padding: $notificationPadding;
|
|
||||||
border: 1px solid lighten($notificationBackground, 10%);
|
|
||||||
|
|
||||||
@include border-radius($notificationBorderRadius);
|
|
||||||
@include box-shadow(1px, 1px, 2px, rgba(0,0,0,0.3));
|
|
||||||
@include opacity($notificationOpacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast:hover {
|
|
||||||
@include opacity(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast-container {
|
|
||||||
position: fixed;
|
|
||||||
z-index: $notificationZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast-container.notification-bottom-right {
|
|
||||||
$videoOffset: 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder;
|
|
||||||
bottom: 135px;
|
|
||||||
right: $filmstripToggleButtonWidth + $videoOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast-container * {
|
|
||||||
@include box-sizing(border-box);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast-container .toast {
|
|
||||||
width: $notificationWidth;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
|
|
@ -85,20 +85,6 @@ $modalMockAKInputBackground: #fafbfc;
|
||||||
$modalMockAKInputBorder: 1px solid #f4f5f7;
|
$modalMockAKInputBorder: 1px solid #f4f5f7;
|
||||||
$modalTextColor: #333;
|
$modalTextColor: #333;
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifications
|
|
||||||
*/
|
|
||||||
$notificationFontSize: 13px;
|
|
||||||
$notificationColor: #FFFFFF;
|
|
||||||
$notificationBackground: $tooltipBg;
|
|
||||||
$notificationTitleColor: $notificationColor;
|
|
||||||
$notificationMessageColor: $notificationColor;
|
|
||||||
$notificationLinkColor: $notificationColor;
|
|
||||||
$notificationOpacity: 0.9;
|
|
||||||
$notificationPadding: 15px 20px;
|
|
||||||
$notificationBorderRadius: 4px;
|
|
||||||
$notificationWidth: 215px;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Misc.
|
* Misc.
|
||||||
*/
|
*/
|
||||||
|
@ -126,7 +112,6 @@ $tooltipsZ: 401;
|
||||||
$dropdownMaskZ: 900;
|
$dropdownMaskZ: 900;
|
||||||
$dropdownZ: 901;
|
$dropdownZ: 901;
|
||||||
$centeredVideoLabelZ: 1010;
|
$centeredVideoLabelZ: 1010;
|
||||||
$notificationZ: 1011;
|
|
||||||
$jitsipopoverZ: 1012;
|
$jitsipopoverZ: 1012;
|
||||||
$popoverZ: 1015;
|
$popoverZ: 1015;
|
||||||
$overlayZ: 1016;
|
$overlayZ: 1016;
|
||||||
|
|
|
@ -159,15 +159,4 @@
|
||||||
transition-delay: 0.1s;
|
transition-delay: 0.1s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move toastr closer to the bottom of the screen and move left to avoid
|
|
||||||
* overlapping of videos when they are configured at default height.
|
|
||||||
*/
|
|
||||||
#toast-container {
|
|
||||||
&.notification-bottom-right {
|
|
||||||
bottom: 25px;
|
|
||||||
right: 130 + 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
/* Modules BEGIN */
|
/* Modules BEGIN */
|
||||||
|
|
||||||
@import 'dial-out';
|
@import 'dial-out';
|
||||||
@import 'toastr';
|
|
||||||
@import 'base';
|
@import 'base';
|
||||||
@import 'utils';
|
@import 'utils';
|
||||||
@import 'overlay/overlay';
|
@import 'overlay/overlay';
|
||||||
|
|
|
@ -3,7 +3,7 @@ Index: jitsi-meet/index.html
|
||||||
===================================================================
|
===================================================================
|
||||||
--- jitsi-meet.orig/index.html
|
--- jitsi-meet.orig/index.html
|
||||||
+++ jitsi-meet/index.html
|
+++ jitsi-meet/index.html
|
||||||
@@ -10,14 +10,14 @@
|
@@ -10,13 +10,13 @@
|
||||||
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||||
<meta itemprop="image" content="/images/jitsilogo.png"/>
|
<meta itemprop="image" content="/images/jitsilogo.png"/>
|
||||||
<script src="https://api.callstats.io/static/callstats.min.js"></script>
|
<script src="https://api.callstats.io/static/callstats.min.js"></script>
|
||||||
|
@ -19,4 +19,4 @@ Index: jitsi-meet/index.html
|
||||||
+ <script src="libs/jquery-ui.min.js"></script>
|
+ <script src="libs/jquery-ui.min.js"></script>
|
||||||
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||||
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||||
<script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
|
- <script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global APP, JitsiMeetJS, $, config, interfaceConfig, toastr */
|
/* global APP, JitsiMeetJS, $, config, interfaceConfig */
|
||||||
|
|
||||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||||
|
|
||||||
|
@ -343,26 +343,6 @@ UI.start = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = interfaceConfig.APP_NAME;
|
document.title = interfaceConfig.APP_NAME;
|
||||||
|
|
||||||
if (!interfaceConfig.filmStripOnly) {
|
|
||||||
toastr.options = {
|
|
||||||
"closeButton": true,
|
|
||||||
"debug": false,
|
|
||||||
"positionClass": "notification-bottom-right",
|
|
||||||
"onclick": null,
|
|
||||||
"showDuration": "300",
|
|
||||||
"hideDuration": "1000",
|
|
||||||
"timeOut": "2000",
|
|
||||||
"extendedTimeOut": "1000",
|
|
||||||
"showEasing": "swing",
|
|
||||||
"hideEasing": "linear",
|
|
||||||
"showMethod": "fadeIn",
|
|
||||||
"hideMethod": "fadeOut",
|
|
||||||
"newestOnTop": false,
|
|
||||||
// this is the default toastr close button html, just adds tabIndex
|
|
||||||
"closeHtml": '<button type="button" tabIndex="-1">×</button>'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -868,7 +848,7 @@ UI.notifyInitiallyMuted = function () {
|
||||||
"connected",
|
"connected",
|
||||||
"notify.muted",
|
"notify.muted",
|
||||||
null,
|
null,
|
||||||
{ timeOut: 120000 });
|
120000);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
/* global $, APP, toastr */
|
/* global $, APP */
|
||||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||||
|
|
||||||
import UIUtil from './UIUtil';
|
|
||||||
import jitsiLocalStorage from '../../util/JitsiLocalStorage';
|
import jitsiLocalStorage from '../../util/JitsiLocalStorage';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Notification,
|
||||||
|
showNotification
|
||||||
|
} from '../../../react/features/notifications';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag for enable/disable of the notifications.
|
* Flag for enable/disable of the notifications.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -448,31 +452,25 @@ var messageHandler = {
|
||||||
* @param messageKey the key from the language file for the text of the
|
* @param messageKey the key from the language file for the text of the
|
||||||
* message.
|
* message.
|
||||||
* @param messageArguments object with the arguments for the message.
|
* @param messageArguments object with the arguments for the message.
|
||||||
* @param options passed to toastr (e.g. timeOut)
|
* @param optional configurations for the notification (e.g. timeout)
|
||||||
*/
|
*/
|
||||||
participantNotification: function(displayName, displayNameKey, cls,
|
participantNotification: function(displayName, displayNameKey, cls,
|
||||||
messageKey, messageArguments, options) {
|
messageKey, messageArguments, timeout) {
|
||||||
|
// If we're in ringing state we skip all notifications.
|
||||||
// If we're in ringing state we skip all toaster notifications.
|
if (!notificationsEnabled || APP.UI.isOverlayVisible()) {
|
||||||
if(!notificationsEnabled || APP.UI.isOverlayVisible())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var displayNameSpan = '<span class="title" ';
|
|
||||||
if (displayName) {
|
|
||||||
displayNameSpan += ">" + UIUtil.escapeHtml(displayName);
|
|
||||||
} else {
|
|
||||||
displayNameSpan += "data-i18n='" + displayNameKey + "'>";
|
|
||||||
}
|
}
|
||||||
displayNameSpan += "</span>";
|
|
||||||
let element = toastr.info(
|
APP.store.dispatch(
|
||||||
displayNameSpan + '<br>' +
|
showNotification(
|
||||||
'<span class=' + cls + ' data-i18n="' + messageKey + '"' +
|
Notification,
|
||||||
(messageArguments?
|
{
|
||||||
" data-i18n-options='"
|
defaultTitleKey: displayNameKey,
|
||||||
+ JSON.stringify(messageArguments) + "'"
|
descriptionArguments: messageArguments,
|
||||||
: "") + "></span>", null, options);
|
descriptionKey: messageKey,
|
||||||
APP.translation.translateElement(element);
|
title: displayName
|
||||||
return element;
|
},
|
||||||
|
timeout));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -488,28 +486,12 @@ var messageHandler = {
|
||||||
*/
|
*/
|
||||||
notify: function(titleKey, messageKey, messageArguments) {
|
notify: function(titleKey, messageKey, messageArguments) {
|
||||||
|
|
||||||
// If we're in ringing state we skip all toaster notifications.
|
// If we're in ringing state we skip all notifications.
|
||||||
if(!notificationsEnabled || APP.UI.isOverlayVisible())
|
if(!notificationsEnabled || APP.UI.isOverlayVisible())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const options = messageArguments
|
this.participantNotification(
|
||||||
? `data-i18n-options='${JSON.stringify(messageArguments)}'` : "";
|
null, titleKey, null, messageKey, messageArguments);
|
||||||
let element = toastr.info(`
|
|
||||||
<span class="title" data-i18n="${titleKey}"></span>
|
|
||||||
<br>
|
|
||||||
<span data-i18n="${messageKey}" ${options}></span>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
APP.translation.translateElement(element);
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the toaster.
|
|
||||||
* @param toasterElement
|
|
||||||
*/
|
|
||||||
remove: function(toasterElement) {
|
|
||||||
toasterElement.remove();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@atlaskit/button-group": "1.1.3",
|
"@atlaskit/button-group": "1.1.3",
|
||||||
"@atlaskit/dropdown-menu": "1.4.0",
|
"@atlaskit/dropdown-menu": "1.4.0",
|
||||||
"@atlaskit/field-text": "2.7.0",
|
"@atlaskit/field-text": "2.7.0",
|
||||||
|
"@atlaskit/flag": "3.0.0",
|
||||||
"@atlaskit/icon": "7.0.0",
|
"@atlaskit/icon": "7.0.0",
|
||||||
"@atlaskit/inline-dialog": "3.2.0",
|
"@atlaskit/inline-dialog": "3.2.0",
|
||||||
"@atlaskit/inline-message": "2.1.1",
|
"@atlaskit/inline-message": "2.1.1",
|
||||||
|
@ -67,7 +68,6 @@
|
||||||
"strophe": "1.2.4",
|
"strophe": "1.2.4",
|
||||||
"strophejs-plugins": "0.0.7",
|
"strophejs-plugins": "0.0.7",
|
||||||
"styled-components": "1.3.0",
|
"styled-components": "1.3.0",
|
||||||
"toastr": "2.1.2",
|
|
||||||
"url-polyfill": "github/url-polyfill",
|
"url-polyfill": "github/url-polyfill",
|
||||||
"xmldom": "0.1.27"
|
"xmldom": "0.1.27"
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection';
|
||||||
import { DialogContainer } from '../../base/dialog';
|
import { DialogContainer } from '../../base/dialog';
|
||||||
import { Filmstrip } from '../../filmstrip';
|
import { Filmstrip } from '../../filmstrip';
|
||||||
import { LargeVideo } from '../../large-video';
|
import { LargeVideo } from '../../large-video';
|
||||||
|
import { NotificationsContainer } from '../../notifications';
|
||||||
import { OverlayContainer } from '../../overlay';
|
import { OverlayContainer } from '../../overlay';
|
||||||
import { Toolbox } from '../../toolbox';
|
import { Toolbox } from '../../toolbox';
|
||||||
import { HideNotificationBarStyle } from '../../unsupported-browser';
|
import { HideNotificationBarStyle } from '../../unsupported-browser';
|
||||||
|
@ -78,6 +79,7 @@ class Conference extends Component {
|
||||||
{ filmStripOnly ? null : <Toolbox /> }
|
{ filmStripOnly ? null : <Toolbox /> }
|
||||||
|
|
||||||
<DialogContainer />
|
<DialogContainer />
|
||||||
|
{ filmStripOnly ? null : <NotificationsContainer /> }
|
||||||
<OverlayContainer />
|
<OverlayContainer />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|
|
@ -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