feat(device-errors): move device error dialogs to notifications
- Create a notification component for displaying a toggle. - Create an action for showing the component if allowed by the local storage setting and for saving the setting to local storage. - Remove all notifications having a timeout by default so the device error notification must be dismissed manually. - Split the camera and mic error dialog into two separate notifications.
This commit is contained in:
parent
1ad8436cb5
commit
74ddae4a6a
|
@ -625,13 +625,13 @@ export default {
|
||||||
// If both requests for 'audio' + 'video' and 'audio'
|
// If both requests for 'audio' + 'video' and 'audio'
|
||||||
// only failed, we assume that there are some problems
|
// only failed, we assume that there are some problems
|
||||||
// with user's microphone and show corresponding dialog.
|
// with user's microphone and show corresponding dialog.
|
||||||
APP.UI.showDeviceErrorDialog(
|
APP.UI.showMicErrorNotification(audioOnlyError);
|
||||||
audioOnlyError, videoOnlyError);
|
APP.UI.showCameraErrorNotification(videoOnlyError);
|
||||||
} else {
|
} else {
|
||||||
// If request for 'audio' + 'video' failed, but request
|
// If request for 'audio' + 'video' failed, but request
|
||||||
// for 'audio' only was OK, we assume that we had
|
// for 'audio' only was OK, we assume that we had
|
||||||
// problems with camera and show corresponding dialog.
|
// 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) => {
|
const maybeShowErrorDialog = (error) => {
|
||||||
if (showUI) {
|
if (showUI) {
|
||||||
APP.UI.showDeviceErrorDialog(error, null);
|
APP.UI.showMicErrorNotification(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -830,7 +830,7 @@ export default {
|
||||||
|
|
||||||
const maybeShowErrorDialog = (error) => {
|
const maybeShowErrorDialog = (error) => {
|
||||||
if (showUI) {
|
if (showUI) {
|
||||||
APP.UI.showDeviceErrorDialog(null, error);
|
APP.UI.showCameraErrorNotification(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2028,7 +2028,7 @@ export default {
|
||||||
APP.settings.setCameraDeviceId(cameraDeviceId, true);
|
APP.settings.setCameraDeviceId(cameraDeviceId, true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
APP.UI.showDeviceErrorDialog(null, err);
|
APP.UI.showCameraErrorNotification(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -2049,7 +2049,7 @@ export default {
|
||||||
APP.settings.setMicDeviceId(micDeviceId, true);
|
APP.settings.setMicDeviceId(micDeviceId, true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
APP.UI.showDeviceErrorDialog(err, null);
|
APP.UI.showMicErrorNotification(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -474,5 +474,9 @@
|
||||||
"retry": "Try again",
|
"retry": "Try again",
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"supportMsg": "If this keeps happening, reach out to"
|
"supportMsg": "If this keeps happening, reach out to"
|
||||||
|
},
|
||||||
|
"deviceError": {
|
||||||
|
"cameraPermission": "Error obtaining camera permission",
|
||||||
|
"microphonePermission": "Error obtaining microphone permission"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
166
modules/UI/UI.js
166
modules/UI/UI.js
|
@ -39,6 +39,9 @@ import {
|
||||||
showDialOutButton,
|
showDialOutButton,
|
||||||
showToolbox
|
showToolbox
|
||||||
} from '../../react/features/toolbox';
|
} from '../../react/features/toolbox';
|
||||||
|
import {
|
||||||
|
maybeShowNotificationWithDoNotDisplay
|
||||||
|
} from '../../react/features/notifications';
|
||||||
|
|
||||||
var EventEmitter = require("events");
|
var EventEmitter = require("events");
|
||||||
UI.messageHandler = messageHandler;
|
UI.messageHandler = messageHandler;
|
||||||
|
@ -64,8 +67,6 @@ let sharedVideoManager;
|
||||||
|
|
||||||
let followMeHandler;
|
let followMeHandler;
|
||||||
|
|
||||||
let deviceErrorDialog;
|
|
||||||
|
|
||||||
const TrackErrors = JitsiMeetJS.errors.track;
|
const TrackErrors = JitsiMeetJS.errors.track;
|
||||||
|
|
||||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||||
|
@ -1189,115 +1190,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.
|
* Shows a notifications about the passed in camera error.
|
||||||
* @param {JitsiTrackError} micError
|
*
|
||||||
* @param {JitsiTrackError} cameraError
|
* @param {JitsiTrackError} cameraError - An error object related to using or
|
||||||
|
* acquiring a video stream.
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
UI.showDeviceErrorDialog = function (micError, cameraError) {
|
UI.showCameraErrorNotification = function (cameraError) {
|
||||||
let dontShowAgain = {
|
if (!cameraError) {
|
||||||
id: "doNotShowWarningAgain",
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (micError) {
|
const { message, name } = cameraError;
|
||||||
dontShowAgain.localStorageKey += "-mic-" + micError.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cameraError) {
|
const persistenceKey = `doNotShowErrorAgain-camera-${name}`;
|
||||||
dontShowAgain.localStorageKey += "-camera-" + cameraError.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cameraJitsiTrackErrorMsg = cameraError
|
const cameraJitsiTrackErrorMsg =
|
||||||
? JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[cameraError.name]
|
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||||
: undefined;
|
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||||
let micJitsiTrackErrorMsg = micError
|
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.GENERAL];
|
||||||
? JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[micError.name]
|
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||||
: 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
|
|
||||||
? `<div>${cameraError.message}</div>`
|
|
||||||
: ``;
|
|
||||||
let additionalMicErrorMsg = !micJitsiTrackErrorMsg && micError &&
|
|
||||||
micError.message
|
|
||||||
? `<div>${micError.message}</div>`
|
|
||||||
: ``;
|
|
||||||
let message = '';
|
|
||||||
|
|
||||||
if (micError) {
|
APP.store.dispatch(maybeShowNotificationWithDoNotDisplay(
|
||||||
message = `
|
persistenceKey,
|
||||||
${message}
|
{
|
||||||
<h3 data-i18n='dialog.micErrorPresent'></h3>
|
additionalMessage: additionalCameraErrorMsg,
|
||||||
<h4 data-i18n='${micErrorMsg}'></h4>
|
messageKey: cameraErrorMsg,
|
||||||
${additionalMicErrorMsg}`;
|
showToggle: Boolean(cameraJitsiTrackErrorMsg),
|
||||||
}
|
subtitleKey: 'dialog.cameraErrorPresent',
|
||||||
|
titleKey: name === TrackErrors.PERMISSION_DENIED
|
||||||
if (cameraError) {
|
? 'deviceError.cameraPermission' : 'dialog.error',
|
||||||
message = `
|
toggleLabelKey: 'dialog.doNotShowWarningAgain'
|
||||||
${message}
|
}));
|
||||||
<h3 data-i18n='dialog.cameraErrorPresent'></h3>
|
|
||||||
<h4 data-i18n='${cameraErrorMsg}'></h4>
|
|
||||||
${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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -455,7 +455,7 @@ var messageHandler = {
|
||||||
* @param optional configurations for the notification (e.g. timeout)
|
* @param optional configurations for the notification (e.g. timeout)
|
||||||
*/
|
*/
|
||||||
participantNotification: function(displayName, displayNameKey, cls,
|
participantNotification: function(displayName, displayNameKey, cls,
|
||||||
messageKey, messageArguments, timeout) {
|
messageKey, messageArguments, timeout = 2500) {
|
||||||
// If we're in ringing state we skip all notifications.
|
// If we're in ringing state we skip all notifications.
|
||||||
if (!notificationsEnabled || APP.UI.isOverlayVisible()) {
|
if (!notificationsEnabled || APP.UI.isOverlayVisible()) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -192,9 +192,12 @@ export default {
|
||||||
createVideoTrack(false).then(([stream]) => stream)
|
createVideoTrack(false).then(([stream]) => stream)
|
||||||
]))
|
]))
|
||||||
.then(tracks => {
|
.then(tracks => {
|
||||||
if (audioTrackError || videoTrackError) {
|
if (audioTrackError) {
|
||||||
APP.UI.showDeviceErrorDialog(
|
APP.UI.showMicErrorNotification(audioTrackError);
|
||||||
audioTrackError, videoTrackError);
|
}
|
||||||
|
|
||||||
|
if (videoTrackError) {
|
||||||
|
APP.UI.showCameraErrorNotification(videoTrackError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tracks.filter(t => typeof t !== 'undefined');
|
return tracks.filter(t => typeof t !== 'undefined');
|
||||||
|
@ -215,7 +218,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
audioTrackError = err;
|
audioTrackError = err;
|
||||||
showError && APP.UI.showDeviceErrorDialog(err, null);
|
showError && APP.UI.showMicErrorNotification(err);
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -228,7 +231,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
videoTrackError = err;
|
videoTrackError = err;
|
||||||
showError && APP.UI.showDeviceErrorDialog(null, err);
|
showError && APP.UI.showCameraErrorNotification(err);
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"@atlaskit/multi-select": "6.2.0",
|
"@atlaskit/multi-select": "6.2.0",
|
||||||
"@atlaskit/spinner": "2.2.3",
|
"@atlaskit/spinner": "2.2.3",
|
||||||
"@atlaskit/tabs": "2.0.0",
|
"@atlaskit/tabs": "2.0.0",
|
||||||
|
"@atlaskit/toggle": "2.0.4",
|
||||||
"@atlassian/aui": "6.0.6",
|
"@atlassian/aui": "6.0.6",
|
||||||
"async": "0.9.0",
|
"async": "0.9.0",
|
||||||
"autosize": "1.18.13",
|
"autosize": "1.18.13",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* type: HIDE_NOTIFICATION,
|
* type: HIDE_NOTIFICATION,
|
||||||
* uid: string
|
* uid: number
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION');
|
export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION');
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import jitsiLocalStorage from '../../../modules/util/JitsiLocalStorage';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HIDE_NOTIFICATION,
|
HIDE_NOTIFICATION,
|
||||||
SHOW_NOTIFICATION
|
SHOW_NOTIFICATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { NotificationWithToggle } from './components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the notification with the passed in id.
|
* Removes the notification with the passed in id.
|
||||||
|
@ -10,7 +13,7 @@ import {
|
||||||
* removed.
|
* removed.
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: HIDE_NOTIFICATION,
|
* type: HIDE_NOTIFICATION,
|
||||||
* uid: string
|
* uid: number
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function hideNotification(uid) {
|
export function hideNotification(uid) {
|
||||||
|
@ -45,3 +48,33 @@ export function showNotification(component, props = {}, timeout) {
|
||||||
uid: window.Date.now()
|
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()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Flag
|
||||||
|
actions = { [
|
||||||
|
{
|
||||||
|
content: t('dialog.Ok'),
|
||||||
|
onClick: this._onDismissed
|
||||||
|
}
|
||||||
|
] }
|
||||||
|
appearance = 'warning'
|
||||||
|
description = { this._renderDescription() }
|
||||||
|
icon = { (
|
||||||
|
<WarningIcon
|
||||||
|
label = 'Warning'
|
||||||
|
size = 'medium' />
|
||||||
|
) }
|
||||||
|
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 (
|
||||||
|
<div className = 'notification-with-toggle'>
|
||||||
|
<div>{ t(subtitleKey) }</div>
|
||||||
|
{ messageKey ? <div>{ t(messageKey) }</div> : null }
|
||||||
|
{ additionalMessage ? <div>{ additionalMessage }</div>
|
||||||
|
: null }
|
||||||
|
{ showToggle
|
||||||
|
? <div>
|
||||||
|
{ t(toggleLabelKey) }
|
||||||
|
<ToggleStateless
|
||||||
|
isChecked
|
||||||
|
= { this.state.isToggleChecked }
|
||||||
|
onChange = { this._onToggleChange } />
|
||||||
|
</div>
|
||||||
|
: null }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(NotificationWithToggle);
|
|
@ -4,14 +4,6 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { hideNotification } from '../actions';
|
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
|
* Implements a React {@link Component} which displays notifications and handles
|
||||||
* automatic dismissmal after a notification is shown for a defined timeout
|
* automatic dismissmal after a notification is shown for a defined timeout
|
||||||
|
@ -74,8 +66,11 @@ class NotificationsContainer extends Component {
|
||||||
const { timeout, uid } = notification;
|
const { timeout, uid } = notification;
|
||||||
|
|
||||||
this._notificationDismissTimeout = setTimeout(() => {
|
this._notificationDismissTimeout = setTimeout(() => {
|
||||||
|
// Perform a no-op if a timeout is not specified.
|
||||||
|
if (Number.isInteger(timeout)) {
|
||||||
this._onDismissed(uid);
|
this._onDismissed(uid);
|
||||||
}, timeout || DEFAULT_NOTIFICATION_TIMEOUT);
|
}
|
||||||
|
}, timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as Notification } from './Notification';
|
export { default as Notification } from './Notification';
|
||||||
export { default as NotificationsContainer } from './NotificationsContainer';
|
export { default as NotificationsContainer } from './NotificationsContainer';
|
||||||
|
export { default as NotificationWithToggle } from './NotificationWithToggle';
|
||||||
|
|
Loading…
Reference in New Issue