diff --git a/conference.js b/conference.js
index 508c6a36a..ef47664ec 100644
--- a/conference.js
+++ b/conference.js
@@ -625,13 +625,13 @@ export default {
// If both requests for 'audio' + 'video' and 'audio'
// only failed, we assume that there are some problems
// with user's microphone and show corresponding dialog.
- APP.UI.showDeviceErrorDialog(
- audioOnlyError, videoOnlyError);
+ APP.UI.showMicErrorNotification(audioOnlyError);
+ APP.UI.showCameraErrorNotification(videoOnlyError);
} else {
// If request for 'audio' + 'video' failed, but request
// for 'audio' only was OK, we assume that we had
// problems with camera and show corresponding dialog.
- APP.UI.showDeviceErrorDialog(null, audioAndVideoError);
+ APP.UI.showCameraErrorNotification(audioAndVideoError);
}
}
@@ -770,7 +770,7 @@ export default {
const maybeShowErrorDialog = (error) => {
if (showUI) {
- APP.UI.showDeviceErrorDialog(error, null);
+ APP.UI.showMicErrorNotification(error);
}
};
@@ -830,7 +830,7 @@ export default {
const maybeShowErrorDialog = (error) => {
if (showUI) {
- APP.UI.showDeviceErrorDialog(null, error);
+ APP.UI.showCameraErrorNotification(error);
}
};
@@ -2028,7 +2028,7 @@ export default {
APP.settings.setCameraDeviceId(cameraDeviceId, true);
})
.catch((err) => {
- APP.UI.showDeviceErrorDialog(null, err);
+ APP.UI.showCameraErrorNotification(err);
});
}
);
@@ -2049,7 +2049,7 @@ export default {
APP.settings.setMicDeviceId(micDeviceId, true);
})
.catch((err) => {
- APP.UI.showDeviceErrorDialog(err, null);
+ APP.UI.showMicErrorNotification(err);
});
}
);
diff --git a/lang/main.json b/lang/main.json
index a5e1447f2..c59901a53 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -474,5 +474,9 @@
"retry": "Try again",
"support": "Support",
"supportMsg": "If this keeps happening, reach out to"
+ },
+ "deviceError": {
+ "cameraPermission": "Error obtaining camera permission",
+ "microphonePermission": "Error obtaining microphone permission"
}
}
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 39e3fb3a0..1aa3f4648 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -39,6 +39,10 @@ import {
showDialOutButton,
showToolbox
} from '../../react/features/toolbox';
+import {
+ maybeShowNotificationWithDoNotDisplay,
+ setNotificationsEnabled
+} from '../../react/features/notifications';
var EventEmitter = require("events");
UI.messageHandler = messageHandler;
@@ -48,24 +52,11 @@ import FollowMe from "../FollowMe";
var eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
-/**
- * Whether an overlay is visible or not.
- *
- * FIXME: This is temporary solution. Don't use this variable!
- * Should be removed when all the code is move to react.
- *
- * @type {boolean}
- * @public
- */
-UI.overlayVisible = false;
-
let etherpadManager;
let sharedVideoManager;
let followMeHandler;
-let deviceErrorDialog;
-
const TrackErrors = JitsiMeetJS.errors.track;
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
@@ -334,7 +325,7 @@ UI.start = function () {
$("body").addClass("filmstrip-only");
UI.showToolbar();
Filmstrip.setFilmstripOnly();
- messageHandler.enableNotifications(false);
+ APP.store.dispatch(setNotificationsEnabled(false));
JitsiPopover.enabled = false;
}
@@ -1189,115 +1180,74 @@ UI.showExtensionInlineInstallationDialog = function (callback) {
});
};
+/**
+ * Shows a notifications about the passed in microphone error.
+ *
+ * @param {JitsiTrackError} micError - An error object related to using or
+ * acquiring an audio stream.
+ * @returns {void}
+ */
+UI.showMicErrorNotification = function (micError) {
+ if (!micError) {
+ return;
+ }
+
+ const { message, name } = micError;
+
+ const persistenceKey = `doNotShowErrorAgain-mic-${name}`;
+
+ const micJitsiTrackErrorMsg
+ = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
+ const micErrorMsg = micJitsiTrackErrorMsg
+ || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL];
+ const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
+
+ APP.store.dispatch(maybeShowNotificationWithDoNotDisplay(
+ persistenceKey,
+ {
+ additionalMessage: additionalMicErrorMsg,
+ messageKey: micErrorMsg,
+ showToggle: Boolean(micJitsiTrackErrorMsg),
+ subtitleKey: 'dialog.micErrorPresent',
+ titleKey: name === TrackErrors.PERMISSION_DENIED
+ ? 'deviceError.microphonePermission' : 'dialog.error',
+ toggleLabelKey: 'dialog.doNotShowWarningAgain'
+ }));
+};
/**
- * Shows dialog with combined information about camera and microphone errors.
- * @param {JitsiTrackError} micError
- * @param {JitsiTrackError} cameraError
+ * Shows a notifications about the passed in camera error.
+ *
+ * @param {JitsiTrackError} cameraError - An error object related to using or
+ * acquiring a video stream.
+ * @returns {void}
*/
-UI.showDeviceErrorDialog = function (micError, cameraError) {
- let dontShowAgain = {
- id: "doNotShowWarningAgain",
- localStorageKey: "doNotShowErrorAgain",
- textKey: "dialog.doNotShowWarningAgain"
- };
- let isMicJitsiTrackErrorAndHasName = micError && micError.name &&
- micError instanceof JitsiMeetJS.errorTypes.JitsiTrackError;
- let isCameraJitsiTrackErrorAndHasName = cameraError && cameraError.name &&
- cameraError instanceof JitsiMeetJS.errorTypes.JitsiTrackError;
- let showDoNotShowWarning = false;
-
- if (micError && cameraError && isMicJitsiTrackErrorAndHasName &&
- isCameraJitsiTrackErrorAndHasName) {
- showDoNotShowWarning = true;
- } else if (micError && isMicJitsiTrackErrorAndHasName && !cameraError) {
- showDoNotShowWarning = true;
- } else if (cameraError && isCameraJitsiTrackErrorAndHasName && !micError) {
- showDoNotShowWarning = true;
+UI.showCameraErrorNotification = function (cameraError) {
+ if (!cameraError) {
+ return;
}
- if (micError) {
- dontShowAgain.localStorageKey += "-mic-" + micError.name;
- }
+ const { message, name } = cameraError;
- if (cameraError) {
- dontShowAgain.localStorageKey += "-camera-" + cameraError.name;
- }
+ const persistenceKey = `doNotShowErrorAgain-camera-${name}`;
- let cameraJitsiTrackErrorMsg = cameraError
- ? JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[cameraError.name]
- : undefined;
- let micJitsiTrackErrorMsg = micError
- ? JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[micError.name]
- : undefined;
- let cameraErrorMsg = cameraError
- ? cameraJitsiTrackErrorMsg ||
- JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.GENERAL]
- : "";
- let micErrorMsg = micError
- ? micJitsiTrackErrorMsg ||
- JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
- : "";
- let additionalCameraErrorMsg = !cameraJitsiTrackErrorMsg && cameraError &&
- cameraError.message
- ? `
${cameraError.message}
`
- : ``;
- let additionalMicErrorMsg = !micJitsiTrackErrorMsg && micError &&
- micError.message
- ? `${micError.message}
`
- : ``;
- let message = '';
+ const cameraJitsiTrackErrorMsg =
+ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
+ const cameraErrorMsg = cameraJitsiTrackErrorMsg
+ || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.GENERAL];
+ const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
- if (micError) {
- message = `
- ${message}
-
-
- ${additionalMicErrorMsg}`;
- }
-
- if (cameraError) {
- message = `
- ${message}
-
-
- ${additionalCameraErrorMsg}`;
- }
-
- // To make sure we don't have multiple error dialogs open at the same time,
- // we will just close the previous one if we are going to show a new one.
- deviceErrorDialog && deviceErrorDialog.close();
-
- deviceErrorDialog = messageHandler.openDialog(
- getTitleKey(),
- message,
- false,
- {Ok: true},
- function () {},
- null,
- function () {
- // Reset dialog reference to null to avoid memory leaks when
- // user closed the dialog manually.
- deviceErrorDialog = null;
- },
- showDoNotShowWarning ? dontShowAgain : undefined
- );
-
- function getTitleKey() {
- let title = "dialog.error";
-
- if (micError && micError.name === TrackErrors.PERMISSION_DENIED) {
- if (!cameraError
- || cameraError.name === TrackErrors.PERMISSION_DENIED) {
- title = "dialog.permissionDenied";
- }
- } else if (cameraError
- && cameraError.name === TrackErrors.PERMISSION_DENIED) {
- title = "dialog.permissionDenied";
- }
-
- return title;
- }
+ APP.store.dispatch(maybeShowNotificationWithDoNotDisplay(
+ persistenceKey,
+ {
+ additionalMessage: additionalCameraErrorMsg,
+ messageKey: cameraErrorMsg,
+ showToggle: Boolean(cameraJitsiTrackErrorMsg),
+ subtitleKey: 'dialog.cameraErrorPresent',
+ titleKey: name === TrackErrors.PERMISSION_DENIED
+ ? 'deviceError.cameraPermission' : 'dialog.error',
+ toggleLabelKey: 'dialog.doNotShowWarningAgain'
+ }));
};
/**
@@ -1347,19 +1297,6 @@ UI.onSharedVideoStop = function (id, attributes) {
sharedVideoManager.onSharedVideoStop(id, attributes);
};
-/**
- * Indicates if any the "top" overlays are currently visible. The check includes
- * the call/ring overlay, the suspended overlay, the GUM permissions overlay,
- * and the page-reload overlay.
- *
- * @returns {*|boolean} {true} if an overlay is visible; {false}, otherwise
- */
-UI.isOverlayVisible = function () {
- return (
- this.overlayVisible
- || APP.store.getState()['features/jwt'].callOverlayVisible);
-};
-
/**
* Handles user's features changes.
*/
diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js
index 679e06d2a..78f99d216 100644
--- a/modules/UI/recording/Recording.js
+++ b/modules/UI/recording/Recording.js
@@ -22,6 +22,7 @@ import VideoLayout from '../videolayout/VideoLayout';
import Feedback from '../feedback/Feedback.js';
import { setToolboxEnabled } from '../../../react/features/toolbox';
+import { setNotificationsEnabled } from '../../../react/features/notifications';
/**
* The dialog for user input.
@@ -309,7 +310,7 @@ var Recording = {
VideoLayout.setLocalVideoVisible(false);
Feedback.enableFeedback(false);
APP.store.dispatch(setToolboxEnabled(false));
- APP.UI.messageHandler.enableNotifications(false);
+ APP.store.dispatch(setNotificationsEnabled(false));
APP.UI.messageHandler.enablePopups(false);
}
diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js
index 57ca94cec..0392d6f01 100644
--- a/modules/UI/util/MessageHandler.js
+++ b/modules/UI/util/MessageHandler.js
@@ -8,12 +8,6 @@ import {
showNotification
} from '../../../react/features/notifications';
-/**
- * Flag for enable/disable of the notifications.
- * @type {boolean}
- */
-let notificationsEnabled = true;
-
/**
* Flag for enabling/disabling popups.
* @type {boolean}
@@ -455,12 +449,7 @@ var messageHandler = {
* @param optional configurations for the notification (e.g. timeout)
*/
participantNotification: function(displayName, displayNameKey, cls,
- messageKey, messageArguments, timeout) {
- // If we're in ringing state we skip all notifications.
- if (!notificationsEnabled || APP.UI.isOverlayVisible()) {
- return;
- }
-
+ messageKey, messageArguments, timeout = 2500) {
APP.store.dispatch(
showNotification(
Notification,
@@ -485,22 +474,10 @@ var messageHandler = {
* @returns {void}
*/
notify: function(titleKey, messageKey, messageArguments) {
-
- // If we're in ringing state we skip all notifications.
- if(!notificationsEnabled || APP.UI.isOverlayVisible())
- return;
-
this.participantNotification(
null, titleKey, null, messageKey, messageArguments);
},
- /**
- * Enables / disables notifications.
- */
- enableNotifications: function (enable) {
- notificationsEnabled = enable;
- },
-
enablePopups: function (enable) {
popupEnabled = enable;
},
diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js
index cf03c107b..1c83bbef5 100644
--- a/modules/devices/mediaDeviceHelper.js
+++ b/modules/devices/mediaDeviceHelper.js
@@ -192,9 +192,12 @@ export default {
createVideoTrack(false).then(([stream]) => stream)
]))
.then(tracks => {
- if (audioTrackError || videoTrackError) {
- APP.UI.showDeviceErrorDialog(
- audioTrackError, videoTrackError);
+ if (audioTrackError) {
+ APP.UI.showMicErrorNotification(audioTrackError);
+ }
+
+ if (videoTrackError) {
+ APP.UI.showCameraErrorNotification(videoTrackError);
}
return tracks.filter(t => typeof t !== 'undefined');
@@ -215,7 +218,7 @@ export default {
})
.catch(err => {
audioTrackError = err;
- showError && APP.UI.showDeviceErrorDialog(err, null);
+ showError && APP.UI.showMicErrorNotification(err);
return [];
});
}
@@ -228,7 +231,7 @@ export default {
})
.catch(err => {
videoTrackError = err;
- showError && APP.UI.showDeviceErrorDialog(null, err);
+ showError && APP.UI.showCameraErrorNotification(err);
return [];
});
}
diff --git a/package.json b/package.json
index 76929cc99..eb3645aa0 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@atlaskit/multi-select": "6.2.0",
"@atlaskit/spinner": "2.2.3",
"@atlaskit/tabs": "2.0.0",
+ "@atlaskit/toggle": "2.0.4",
"@atlassian/aui": "6.0.6",
"async": "0.9.0",
"autosize": "1.18.13",
diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js
index a4ead6213..55ba31cac 100644
--- a/react/features/conference/components/Conference.web.js
+++ b/react/features/conference/components/Conference.web.js
@@ -79,7 +79,7 @@ class Conference extends Component {
{ filmStripOnly ? null : }
- { filmStripOnly ? null : }
+
{/*
diff --git a/react/features/notifications/actionTypes.js b/react/features/notifications/actionTypes.js
index 4b8593a6d..e9860c739 100644
--- a/react/features/notifications/actionTypes.js
+++ b/react/features/notifications/actionTypes.js
@@ -1,15 +1,15 @@
-/*
+/**
* The type of (redux) action which signals that a specific notification should
* not be displayed anymore.
*
* {
* type: HIDE_NOTIFICATION,
- * uid: string
+ * uid: number
* }
*/
export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION');
-/*
+/**
* The type of (redux) action which signals that a notification component should
* be displayed.
*
@@ -22,3 +22,14 @@ export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION');
* }
*/
export const SHOW_NOTIFICATION = Symbol('SHOW_NOTIFICATION');
+
+/**
+ * The type of (redux) action which signals that notifications should not
+ * display.
+ *
+ * {
+ * type: SET_NOTIFICATIONS_ENABLED,
+ * enabled: Boolean
+ * }
+ */
+export const SET_NOTIFICATIONS_ENABLED = Symbol('SET_NOTIFICATIONS_ENABLED');
diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js
index ce56b432d..bea4ee066 100644
--- a/react/features/notifications/actions.js
+++ b/react/features/notifications/actions.js
@@ -1,7 +1,11 @@
+import jitsiLocalStorage from '../../../modules/util/JitsiLocalStorage';
+
import {
HIDE_NOTIFICATION,
+ SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION
} from './actionTypes';
+import { NotificationWithToggle } from './components';
/**
* Removes the notification with the passed in id.
@@ -10,7 +14,7 @@ import {
* removed.
* @returns {{
* type: HIDE_NOTIFICATION,
- * uid: string
+ * uid: number
* }}
*/
export function hideNotification(uid) {
@@ -20,6 +24,22 @@ export function hideNotification(uid) {
};
}
+/**
+ * Stops notifications from being displayed.
+ *
+ * @param {boolean} enabled - Whether or not notifications should display.
+ * @returns {{
+ * type: SET_NOTIFICATIONS_ENABLED,
+ * enabled: boolean
+ * }}
+ */
+export function setNotificationsEnabled(enabled) {
+ return {
+ type: SET_NOTIFICATIONS_ENABLED,
+ enabled
+ };
+}
+
/**
* Queues a notification for display.
*
@@ -45,3 +65,33 @@ export function showNotification(component, props = {}, timeout) {
uid: window.Date.now()
};
}
+
+/**
+ * Displays a notification unless the passed in persistenceKey value exists in
+ * local storage and has been set to "true".
+ *
+ * @param {string} persistenceKey - The local storage key to look up for whether
+ * or not the notification should display.
+ * @param {Object} props - The props needed to show the notification component.
+ * @returns {Function}
+ */
+export function maybeShowNotificationWithDoNotDisplay(persistenceKey, props) {
+ return dispatch => {
+ if (jitsiLocalStorage.getItem(persistenceKey) === 'true') {
+ return;
+ }
+
+ const newProps = Object.assign({}, props, {
+ onToggleSubmit: isToggled => {
+ jitsiLocalStorage.setItem(persistenceKey, isToggled);
+ }
+ });
+
+ dispatch({
+ type: SHOW_NOTIFICATION,
+ component: NotificationWithToggle,
+ props: newProps,
+ uid: window.Date.now()
+ });
+ };
+}
diff --git a/react/features/notifications/components/NotificationWithToggle.native.js b/react/features/notifications/components/NotificationWithToggle.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/notifications/components/NotificationWithToggle.web.js b/react/features/notifications/components/NotificationWithToggle.web.js
new file mode 100644
index 000000000..3112af279
--- /dev/null
+++ b/react/features/notifications/components/NotificationWithToggle.web.js
@@ -0,0 +1,210 @@
+import Flag from '@atlaskit/flag';
+import WarningIcon from '@atlaskit/icon/glyph/warning';
+import { ToggleStateless } from '@atlaskit/toggle';
+import React, { Component } from 'react';
+
+import { translate } from '../../base/i18n';
+
+/**
+ * React {@code Component} for displaying a notification with a toggle element.
+ *
+ * @extends Component
+ */
+class NotificationWithToggle extends Component {
+ /**
+ * {@code NotificationWithToggle} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Any additional text to display at the end of the notification message
+ * body.
+ */
+ additionalMessage: React.PropTypes.string,
+
+ /**
+ * Whether or not the dismiss button should be displayed. This is passed
+ * in by {@code FlagGroup}.
+ */
+ isDismissAllowed: React.PropTypes.bool,
+
+ /**
+ * The translation key to be used as the main body of the notification.
+ */
+ messageKey: React.PropTypes.string,
+
+ /**
+ * Callback invoked when the user clicks to dismiss the notification.
+ * This is passed in by {@code FlagGroup}.
+ */
+ onDismissed: React.PropTypes.func,
+
+ /**
+ * Optional callback to invoke when the notification is dismissed. The
+ * current value of the toggle element will be passed in.
+ */
+ onToggleSubmit: React.PropTypes.func,
+
+ /**
+ * Whether or not the toggle element should be displayed.
+ */
+ showToggle: React.PropTypes.bool,
+
+ /**
+ * Translation key for a message to display at the top of the
+ * notification body.
+ */
+ subtitleKey: React.PropTypes.string,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: React.PropTypes.func,
+
+ /**
+ * The translation key to be used as the title of the notification.
+ */
+ titleKey: React.PropTypes.string,
+
+ /*
+ * The translation key to be used as a label describing what setting the
+ * toggle will change.
+ */
+ toggleLabelKey: React.PropTypes.string,
+
+ /**
+ * The unique identifier for the notification. Passed back by the
+ * {@code Flag} component in the onDismissed callback.
+ */
+ uid: React.PropTypes.number
+ };
+
+ /**
+ * Initializes a new {@code NotificationWithToggle} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ /**
+ * Whether or not the toggle element is active/checked/selected.
+ *
+ * @type {boolean}
+ */
+ isToggleChecked: false
+ };
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onDismissed = this._onDismissed.bind(this);
+ this._onToggleChange = this._onToggleChange.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ isDismissAllowed,
+ t,
+ titleKey,
+ uid
+ } = this.props;
+
+ return (
+
+ ) }
+ id = { uid }
+ isDismissAllowed = { isDismissAllowed }
+ onDismissed = { this._onDismissed }
+ title = { t(titleKey) } />
+ );
+ }
+
+ /**
+ * Calls back into {@code FlagGroup} to dismiss the notification. Optionally
+ * will execute a passed in onToggleSubmit callback with the current state
+ * of the toggle element.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onDismissed() {
+ const { onDismissed, onToggleSubmit, showToggle, uid } = this.props;
+
+ if (showToggle && onToggleSubmit) {
+ onToggleSubmit(this.state.isToggleChecked);
+ }
+
+ onDismissed(uid);
+ }
+
+ /**
+ * Updates the current known state of the toggle selection.
+ *
+ * @param {Object} event - The DOM event from changing the toggle selection.
+ * @private
+ * @returns {void}
+ */
+ _onToggleChange(event) {
+ this.setState({
+ isToggleChecked: event.target.checked
+ });
+ }
+
+ /**
+ * Creates a React Element for displaying the notification message as well
+ * as a toggle.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderDescription() {
+ const {
+ additionalMessage,
+ messageKey,
+ showToggle,
+ subtitleKey,
+ t,
+ toggleLabelKey
+ } = this.props;
+
+ return (
+
+
{ t(subtitleKey) }
+ { messageKey ?
{ t(messageKey) }
: null }
+ { additionalMessage ?
{ additionalMessage }
+ : null }
+ { showToggle
+ ?
+ { t(toggleLabelKey) }
+
+
+ : null }
+
+ );
+ }
+}
+
+export default translate(NotificationWithToggle);
diff --git a/react/features/notifications/components/NotificationsContainer.web.js b/react/features/notifications/components/NotificationsContainer.web.js
index 32447f00e..2e7e1f37b 100644
--- a/react/features/notifications/components/NotificationsContainer.web.js
+++ b/react/features/notifications/components/NotificationsContainer.web.js
@@ -4,14 +4,6 @@ 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
@@ -32,6 +24,12 @@ class NotificationsContainer extends Component {
*/
_notifications: React.PropTypes.array,
+ /**
+ * Whether or not notifications should be displayed at all. If not,
+ * notifications will be dismissed immediately.
+ */
+ _showNotifications: React.PropTypes.bool,
+
/**
* Invoked to update the redux store in order to remove notifications.
*/
@@ -67,15 +65,27 @@ class NotificationsContainer extends Component {
* returns {void}
*/
componentDidUpdate() {
- const { _notifications } = this.props;
+ const { _notifications, _showNotifications } = this.props;
- if (_notifications.length && !this._notificationDismissTimeout) {
+ if (_notifications.length) {
const notification = _notifications[0];
- const { timeout, uid } = notification;
- this._notificationDismissTimeout = setTimeout(() => {
- this._onDismissed(uid);
- }, timeout || DEFAULT_NOTIFICATION_TIMEOUT);
+ if (!_showNotifications) {
+ this._onDismissed(notification.uid);
+ } else if (this._notificationDismissTimeout) {
+
+ // No-op because there should already be a notification that
+ // is waiting for dismissal.
+ } else {
+ const { timeout, uid } = notification;
+
+ this._notificationDismissTimeout = setTimeout(() => {
+ // Perform a no-op if a timeout is not specified.
+ if (Number.isInteger(timeout)) {
+ this._onDismissed(uid);
+ }
+ }, timeout);
+ }
}
}
@@ -96,28 +106,9 @@ class NotificationsContainer extends Component {
* @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 }
+ { this._renderFlags() }
);
}
@@ -136,6 +127,38 @@ class NotificationsContainer extends Component {
this.props.dispatch(hideNotification(flagUid));
}
+
+ /**
+ * Renders notifications to display as ReactElements. An empty array will
+ * be returned if notifications are disabled.
+ *
+ * @private
+ * @returns {ReactElement[]}
+ */
+ _renderFlags() {
+ const { _notifications, _showNotifications } = this.props;
+
+ if (!_showNotifications) {
+ return [];
+ }
+
+ return _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 (
+
+
+ );
+ });
+ }
}
/**
@@ -149,8 +172,25 @@ class NotificationsContainer extends Component {
* }}
*/
function _mapStateToProps(state) {
+ // TODO: Per existing behavior, notifications should not display when an
+ // overlay is visible. This logic for checking overlay display can likely be
+ // simplified.
+ const {
+ connectionEstablished,
+ haveToReload,
+ isMediaPermissionPromptVisible,
+ suspendDetected
+ } = state['features/overlay'];
+ const isAnyOverlayVisible = (connectionEstablished && haveToReload)
+ || isMediaPermissionPromptVisible
+ || suspendDetected
+ || state['features/jwt'].callOverlayVisible;
+
+ const { enabled, notifications } = state['features/notifications'];
+
return {
- _notifications: state['features/notifications']
+ _notifications: notifications,
+ _showNotifications: enabled && !isAnyOverlayVisible
};
}
diff --git a/react/features/notifications/components/index.js b/react/features/notifications/components/index.js
index 642f2f283..eb71c3bc7 100644
--- a/react/features/notifications/components/index.js
+++ b/react/features/notifications/components/index.js
@@ -1,2 +1,3 @@
export { default as Notification } from './Notification';
export { default as NotificationsContainer } from './NotificationsContainer';
+export { default as NotificationWithToggle } from './NotificationWithToggle';
diff --git a/react/features/notifications/reducer.js b/react/features/notifications/reducer.js
index a510806c6..105d4eb18 100644
--- a/react/features/notifications/reducer.js
+++ b/react/features/notifications/reducer.js
@@ -2,6 +2,7 @@ import { ReducerRegistry } from '../base/redux';
import {
HIDE_NOTIFICATION,
+ SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION
} from './actionTypes';
@@ -10,7 +11,10 @@ import {
*
* @type {array}
*/
-const DEFAULT_STATE = [];
+const DEFAULT_STATE = {
+ enabled: true,
+ notifications: []
+};
/**
* Reduces redux actions which affect the display of notifications.
@@ -24,19 +28,31 @@ ReducerRegistry.register('features/notifications',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case HIDE_NOTIFICATION:
- return state.filter(
- notification => notification.uid !== action.uid);
+ return {
+ ...state,
+ notifications: state.notifications.filter(
+ notification => notification.uid !== action.uid)
+ };
+
+ case SET_NOTIFICATIONS_ENABLED:
+ return {
+ ...state,
+ enabled: action.enabled
+ };
case SHOW_NOTIFICATION:
- return [
+ return {
...state,
- {
- component: action.component,
- props: action.props,
- timeout: action.timeout,
- uid: action.uid
- }
- ];
+ notifications: [
+ ...state.notifications,
+ {
+ component: action.component,
+ props: action.props,
+ timeout: action.timeout,
+ uid: action.uid
+ }
+ ]
+ };
}
return state;
diff --git a/react/features/overlay/components/OverlayContainer.js b/react/features/overlay/components/OverlayContainer.js
index c12638389..b9381da55 100644
--- a/react/features/overlay/components/OverlayContainer.js
+++ b/react/features/overlay/components/OverlayContainer.js
@@ -11,7 +11,6 @@ import UserMediaPermissionsFilmstripOnlyOverlay
from './UserMediaPermissionsFilmstripOnlyOverlay';
import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay';
-declare var APP: Object;
declare var interfaceConfig: Object;
/**
@@ -133,23 +132,6 @@ class OverlayContainer extends Component {
};
}
- /**
- * React Component method that executes once component is updated.
- *
- * @inheritdoc
- * @returns {void}
- * @protected
- */
- componentDidUpdate() {
- if (typeof APP === 'object') {
- APP.UI.overlayVisible
- = (this.props._connectionEstablished
- && this.props._haveToReload)
- || this.props._suspendDetected
- || this.props._isMediaPermissionPromptVisible;
- }
- }
-
/**
* Implements React's {@link Component#render()}.
*