diff --git a/app.js b/app.js
index 1d2124a2ce8b..51cd2cc9ed3d 100644
--- a/app.js
+++ b/app.js
@@ -13,8 +13,6 @@ import 'aui-experimental';
import 'aui-css';
import 'aui-experimental-css';
-window.toastr = require('toastr');
-
import conference from './conference';
import API from './modules/API';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
diff --git a/css/_toastr.scss b/css/_toastr.scss
deleted file mode 100644
index 663240686e72..000000000000
--- a/css/_toastr.scss
+++ /dev/null
@@ -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;
-}
diff --git a/css/_variables.scss b/css/_variables.scss
index ccd1826b3811..2cf78565f71a 100644
--- a/css/_variables.scss
+++ b/css/_variables.scss
@@ -85,20 +85,6 @@ $modalMockAKInputBackground: #fafbfc;
$modalMockAKInputBorder: 1px solid #f4f5f7;
$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.
*/
@@ -126,7 +112,6 @@ $tooltipsZ: 401;
$dropdownMaskZ: 900;
$dropdownZ: 901;
$centeredVideoLabelZ: 1010;
-$notificationZ: 1011;
$jitsipopoverZ: 1012;
$popoverZ: 1015;
$overlayZ: 1016;
diff --git a/css/_vertical_filmstrip_overrides.scss b/css/_vertical_filmstrip_overrides.scss
index d15f58278897..434931d49c9c 100644
--- a/css/_vertical_filmstrip_overrides.scss
+++ b/css/_vertical_filmstrip_overrides.scss
@@ -159,15 +159,4 @@
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;
- }
- }
}
diff --git a/css/main.scss b/css/main.scss
index 676f4c5c9e1a..6b7c755b4f42 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -33,7 +33,6 @@
/* Modules BEGIN */
@import 'dial-out';
-@import 'toastr';
@import 'base';
@import 'utils';
@import 'overlay/overlay';
diff --git a/debian/patches/jquery-package b/debian/patches/jquery-package
index ef0750c72525..5a89baed516c 100644
--- a/debian/patches/jquery-package
+++ b/debian/patches/jquery-package
@@ -3,7 +3,7 @@ Index: jitsi-meet/index.html
===================================================================
--- jitsi-meet.orig/index.html
+++ jitsi-meet/index.html
-@@ -10,14 +10,14 @@
+@@ -10,13 +10,13 @@
@@ -19,4 +19,4 @@ Index: jitsi-meet/index.html
+
-
+-
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 4ee560482446..39e3fb3a0526 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -1,4 +1,4 @@
-/* global APP, JitsiMeetJS, $, config, interfaceConfig, toastr */
+/* global APP, JitsiMeetJS, $, config, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename);
@@ -343,26 +343,6 @@ UI.start = function () {
}
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": ''
- };
- }
};
/**
@@ -868,7 +848,7 @@ UI.notifyInitiallyMuted = function () {
"connected",
"notify.muted",
null,
- { timeOut: 120000 });
+ 120000);
};
/**
diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js
index 9672ada6f657..57ca94cec3e2 100644
--- a/modules/UI/util/MessageHandler.js
+++ b/modules/UI/util/MessageHandler.js
@@ -1,9 +1,13 @@
-/* global $, APP, toastr */
+/* global $, APP */
const logger = require("jitsi-meet-logger").getLogger(__filename);
-import UIUtil from './UIUtil';
import jitsiLocalStorage from '../../util/JitsiLocalStorage';
+import {
+ Notification,
+ showNotification
+} from '../../../react/features/notifications';
+
/**
* Flag for enable/disable of the notifications.
* @type {boolean}
@@ -448,31 +452,25 @@ var messageHandler = {
* @param messageKey the key from the language file for the text of 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,
- messageKey, messageArguments, options) {
-
- // If we're in ringing state we skip all toaster notifications.
- if(!notificationsEnabled || APP.UI.isOverlayVisible())
+ messageKey, messageArguments, timeout) {
+ // If we're in ringing state we skip all notifications.
+ if (!notificationsEnabled || APP.UI.isOverlayVisible()) {
return;
-
- var displayNameSpan = '" + UIUtil.escapeHtml(displayName);
- } else {
- displayNameSpan += "data-i18n='" + displayNameKey + "'>";
}
- displayNameSpan += "";
- let element = toastr.info(
- displayNameSpan + '
' +
- '", null, options);
- APP.translation.translateElement(element);
- return element;
+
+ APP.store.dispatch(
+ showNotification(
+ Notification,
+ {
+ defaultTitleKey: displayNameKey,
+ descriptionArguments: messageArguments,
+ descriptionKey: messageKey,
+ title: displayName
+ },
+ timeout));
},
/**
@@ -488,28 +486,12 @@ var messageHandler = {
*/
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())
return;
- const options = messageArguments
- ? `data-i18n-options='${JSON.stringify(messageArguments)}'` : "";
- let element = toastr.info(`
-
-
-
- `
- );
- APP.translation.translateElement(element);
- return element;
- },
-
- /**
- * Removes the toaster.
- * @param toasterElement
- */
- remove: function(toasterElement) {
- toasterElement.remove();
+ this.participantNotification(
+ null, titleKey, null, messageKey, messageArguments);
},
/**
diff --git a/package.json b/package.json
index b8fbe733f04d..76929cc99fb3 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@atlaskit/button-group": "1.1.3",
"@atlaskit/dropdown-menu": "1.4.0",
"@atlaskit/field-text": "2.7.0",
+ "@atlaskit/flag": "3.0.0",
"@atlaskit/icon": "7.0.0",
"@atlaskit/inline-dialog": "3.2.0",
"@atlaskit/inline-message": "2.1.1",
@@ -67,7 +68,6 @@
"strophe": "1.2.4",
"strophejs-plugins": "0.0.7",
"styled-components": "1.3.0",
- "toastr": "2.1.2",
"url-polyfill": "github/url-polyfill",
"xmldom": "0.1.27"
},
diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js
index bc558d6c7e4c..a4ead621326a 100644
--- a/react/features/conference/components/Conference.web.js
+++ b/react/features/conference/components/Conference.web.js
@@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog';
import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video';
+import { NotificationsContainer } from '../../notifications';
import { OverlayContainer } from '../../overlay';
import { Toolbox } from '../../toolbox';
import { HideNotificationBarStyle } from '../../unsupported-browser';
@@ -78,6 +79,7 @@ class Conference extends Component {
{ filmStripOnly ? null : }
+ { filmStripOnly ? null : }
{/*
diff --git a/react/features/notifications/actionTypes.js b/react/features/notifications/actionTypes.js
new file mode 100644
index 000000000000..4b8593a6d1c1
--- /dev/null
+++ b/react/features/notifications/actionTypes.js
@@ -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');
diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js
new file mode 100644
index 000000000000..ce56b432deb0
--- /dev/null
+++ b/react/features/notifications/actions.js
@@ -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()
+ };
+}
diff --git a/react/features/notifications/components/Notification.native.js b/react/features/notifications/components/Notification.native.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/react/features/notifications/components/Notification.web.js b/react/features/notifications/components/Notification.web.js
new file mode 100644
index 000000000000..e49520c69efb
--- /dev/null
+++ b/react/features/notifications/components/Notification.web.js
@@ -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 (
+
+ ) }
+ id = { uid }
+ isDismissAllowed = { isDismissAllowed }
+ onDismissed = { onDismissed }
+ title = { title || t(defaultTitleKey) } />
+ );
+ }
+}
+
+export default translate(Notification);
diff --git a/react/features/notifications/components/NotificationsContainer.native.js b/react/features/notifications/components/NotificationsContainer.native.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/react/features/notifications/components/NotificationsContainer.web.js b/react/features/notifications/components/NotificationsContainer.web.js
new file mode 100644
index 000000000000..32447f00ee71
--- /dev/null
+++ b/react/features/notifications/components/NotificationsContainer.web.js
@@ -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 (
+
+
+ );
+ });
+
+ return (
+
+ { flags }
+
+ );
+ }
+
+ /**
+ * 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);
diff --git a/react/features/notifications/components/index.js b/react/features/notifications/components/index.js
new file mode 100644
index 000000000000..642f2f2837cc
--- /dev/null
+++ b/react/features/notifications/components/index.js
@@ -0,0 +1,2 @@
+export { default as Notification } from './Notification';
+export { default as NotificationsContainer } from './NotificationsContainer';
diff --git a/react/features/notifications/index.js b/react/features/notifications/index.js
new file mode 100644
index 000000000000..b2e9c09f6caf
--- /dev/null
+++ b/react/features/notifications/index.js
@@ -0,0 +1,5 @@
+export * from './actions';
+export * from './actionTypes';
+export * from './components';
+
+import './reducer';
diff --git a/react/features/notifications/reducer.js b/react/features/notifications/reducer.js
new file mode 100644
index 000000000000..a510806c6247
--- /dev/null
+++ b/react/features/notifications/reducer.js
@@ -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;
+ });