ref(notifications): convert some dialogs to error or warning notifica… (#1991)

* ref(notifications): convert some dialogs to error or warning notifications

- Expand the configurability of the Notification component so warnings
  and errors can be displayed.
- Allow Notification to take in arbitrary text for the body.
- Rename defaultTitleKey to titleKey for consistency with descriptionKey.

* ref(notifications): remove openReportDialog method

openReportDialog is a wrapper around showError that adds
a logger statement. It is being called in one place only
so remove the method and have that one place call logger.

* ref(notifications): UI.showTrackNotWorkingDialog takes a boolean

Change UI.showTrackNotWorkingDialog so it takes a boolean
arguments instead of the entire track. A small refactor so
the method needs to know less.

* [squash] Fixes eslint errors

* WiP: Fixes desktop sharing error strings and adds support button

* [squash] Fix icons appearances

* [squash] Fix translate titles and messages

* [squash] fix(translation): Fixes incorrect password string

* [squash] fix(recording): Fixes recording message

* [squash] fix(warning): Turns some warnings to errors and makes support link optional.

* [squash] fix(translation): Addressing language comments

* [squash] Fixes jsdoc and formatting

* [squash] fix(noopener): Fixes window.open noopener

* [squash] fix(constants): Extract constants and refactor NotificationWithToggle

* [squash] fix(lang): Fixes camera and mic error titles

* [squash] fix(supportLink): Renames addSupportLink to hideErrorSupportLink
This commit is contained in:
virtuacoplenny 2017-11-03 12:05:03 -07:00 committed by hristoterezov
parent c3efa4f088
commit 510334fa7f
15 changed files with 408 additions and 210 deletions

View File

@ -319,9 +319,12 @@ class ConferenceConnector {
APP.UI.notifyGracefulShutdown();
break;
case JitsiConferenceErrors.JINGLE_FATAL_ERROR:
APP.UI.notifyInternalError();
case JitsiConferenceErrors.JINGLE_FATAL_ERROR: {
const [ error ] = params;
APP.UI.notifyInternalError(error);
break;
}
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = params;
@ -1680,20 +1683,21 @@ export default {
// JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR
// JitsiTrackErrors.GENERAL
// and any other
let dialogTxt;
let dialogTitleKey;
let descriptionKey;
let titleKey;
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
dialogTxt = APP.translation.generateTranslationHTML(
'dialog.screenSharingPermissionDeniedError');
dialogTitleKey = 'dialog.error';
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedToInstallTitle';
} else {
dialogTxt = APP.translation.generateTranslationHTML(
'dialog.failtoinstall');
dialogTitleKey = 'dialog.permissionDenied';
descriptionKey = 'dialog.screenSharingFailedToInstall';
titleKey = 'dialog.screenSharingFailedToInstallTitle';
}
APP.UI.messageHandler.openDialog(dialogTitleKey, dialogTxt, false);
APP.UI.messageHandler.showError({
descriptionKey,
titleKey
});
},
/**

View File

@ -117,7 +117,7 @@ var interfaceConfig = {
* If indicated some of the error dialogs may point to the support URL for
* help.
*/
// SUPPORT_URL: "",
SUPPORT_URL: 'https://github.com/jitsi/jitsi-meet/issues/new',
/**
* Whether the connection indicator icon should hide itself based on

View File

@ -226,32 +226,36 @@
"add": "Add",
"allow": "Allow",
"kickMessage": "Ouch! You have been kicked out of the meet!",
"popupError": "Your browser is blocking popup windows from this site. Please enable popups in your browser's security settings and try again.",
"popupErrorTitle": "Pop-up blocked",
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
"passwordErrorTitle": "Password Error",
"passwordError": "This conversation is currently protected by a password. Only the owner of the conference can set a password.",
"passwordError2": "This conversation isn't currently protected by a password. Only the owner of the conference can set a password.",
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
"connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
"incorrectPassword": "Password is incorrect",
"incorrectPassword": "Incorrect username or password",
"connecting": "Connecting",
"copy": "Copy",
"contactSupport": "Contact support",
"error": "Error",
"createPassword": "Create password",
"detectext": "Error when trying to detect desktopsharing extension.",
"failtoinstall": "Failed to install desktop sharing extension",
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
"conferenceReloadTitle": "Unfortunately, something went wrong.",
"conferenceReloadMsg": "We're trying to fix this. Reconnecting in __seconds__ sec...",
"conferenceDisconnectTitle": "You have been disconnected.",
"conferenceDisconnectMsg": "You may want to check your network connection. Reconnecting in __seconds__ sec...",
"dismiss": "Dismiss",
"rejoinNow": "Rejoin now",
"maxUsersLimitReached": "The limit for maximum number of members in the conference has been reached. The conference is full. Please try again later!",
"maxUsersLimitReachedTitle": "Maximum members limit reached",
"maxUsersLimitReached": "The limit for maximum number of members has been reached. The conference is full. Please contact the meeting owner or try again later!",
"lockTitle": "Lock failed",
"lockMessage": "Failed to lock the conference.",
"warning": "Warning",
"passwordNotSupported": "Room passwords are currently not supported.",
"passwordNotSupportedTitle": "Password not supported",
"passwordNotSupported": "Setting a meeting password is not supported.",
"internalErrorTitle": "Internal error",
"internalError": "Oups! Something went wrong. The following error occurred: [setRemoteDescription]",
"internalError": "Oops! Something went wrong. The following error occurred: __error__",
"unableToSwitch": "Unable to switch video stream.",
"SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
"SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
@ -268,7 +272,8 @@
"shareVideoLinkError": "Please provide a correct youtube link.",
"removeSharedVideoTitle": "Remove shared video",
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
"alreadySharedVideoMsg": "Another member is already sharing video. This conference allows only one shared video at a time.",
"alreadySharedVideoMsg": "Another member is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"WaitingForHost": "Waiting for the host ...",
"WaitForHostMsg": "The conference <b>__room__ </b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"IamHost": "I am the host",
@ -277,7 +282,7 @@
"retry": "Retry",
"logoutTitle" : "Logout",
"logoutQuestion" : "Are you sure you want to logout and stop the conference?",
"sessTerminated": "Session Terminated",
"sessTerminated": "Call terminated",
"hungUp": "You hung up",
"joinAgain": "Join again",
"Share": "Share",
@ -297,7 +302,7 @@
"password": "Enter password",
"userPassword": "user password",
"token": "token",
"tokenAuthFailedTitle": "Authentication problem",
"tokenAuthFailedTitle": "Authentication failed",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"displayNameRequired": "Display name is required",
"enterDisplayName": "Please enter your display name",
@ -317,7 +322,9 @@
"doNotShowWarningAgain": "Don't show this warning again",
"doNotShowMessageAgain": "Don't show this message again",
"permissionDenied": "Permission Denied",
"screenSharingPermissionDeniedError": "You have not granted permission to share your screen.",
"screenSharingFailedToInstall": "Oops! Your screen sharing extension failed to install.",
"screenSharingFailedToInstallTitle": "Screen sharing extension failed to install",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
"micErrorPresent": "There was an error connecting to your microphone.",
"cameraErrorPresent": "There was an error connecting to your camera.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
@ -329,8 +336,10 @@
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micNotFoundError": "Microphone was not found.",
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to restart the application.",
"micNotSendingDataTitle": "Unable to access microphone",
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to reload the application.",
"cameraNotSendingDataTitle": "Unable to access camera",
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
"goToStore": "Go to the webstore",
"externalInstallationTitle": "Extension required",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
@ -401,26 +410,31 @@
},
"recording":
{
"pending": "Recording waiting for a member to join...",
"on": "Recording",
"off": "Recording stopped",
"failedToStart": "Recording failed to start",
"busy": "We're working on freeing recording resources. Please try again in a few minutes.",
"busyTitle": "All recorders are currently busy",
"buttonTooltip": "Start / Stop recording",
"error": "Recording failed. Please try again.",
"unavailable": "The recording service is currently unavailable. Please try again later."
"failedToStart": "Recording failed to start",
"off": "Recording stopped",
"on": "Recording",
"pending": "Recording waiting for a member to join...",
"unavailable": "Oops! The recording service is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
},
"liveStreaming":
{
"pending": "Starting Live Stream...",
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"buttonTooltip": "Start / Stop Live Stream",
"error": "Live Streaming failed. Please try again.",
"failedToStart": "Live Streaming failed to start",
"off": "Live Streaming stopped",
"on": "Live Streaming",
"off": "Live Streaming Stopped",
"unavailable": "The live streaming service is currently unavailable. Please try again later.",
"failedToStart": "Live streaming failed to start",
"buttonTooltip": "Start / Stop live stream",
"streamIdRequired": "Please fill in the stream id in order to launch the live streaming.",
"pending": "Starting Live Stream...",
"streamIdRequired": "Please fill in the stream id in order to launch the Live Streaming.",
"streamIdHelp": "Where do I find this?",
"error": "Live streaming failed. Please try again.",
"busy": "All recorders are currently busy. Please try again later."
"unavailable": "Oops! The Live Streaming service is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Live Streaming unavailable"
},
"speakerStats":
{
@ -487,6 +501,8 @@
"supportMsg": "If this keeps happening, reach out to"
},
"deviceError": {
"cameraError": "Failed to access your camera",
"microphoneError": "Failed to access your microphone",
"cameraPermission": "Error obtaining camera permission",
"microphonePermission": "Error obtaining microphone permission"
},

View File

@ -144,35 +144,35 @@ UI.toggleFullScreen = function() {
* Notify user that server has shut down.
*/
UI.notifyGracefulShutdown = function() {
messageHandler.openMessageDialog(
'dialog.serviceUnavailable',
'dialog.gracefulShutdown'
);
messageHandler.showError({
descriptionKey: 'dialog.gracefulShutdown',
titleKey: 'dialog.serviceUnavailable'
});
};
/**
* Notify user that reservation error happened.
*/
UI.notifyReservationError = function(code, msg) {
const message
= APP.translation.generateTranslationHTML(
'dialog.reservationErrorMsg',
{
code,
msg
});
messageHandler.openDialog(
'dialog.reservationError', message, true, {}, () => false);
messageHandler.showError({
descriptionArguments: {
code,
msg
},
descriptionKey: 'dialog.reservationErrorMsg',
titleKey: 'dialog.reservationError'
});
};
/**
* Notify user that he has been kicked from the server.
*/
UI.notifyKicked = function() {
messageHandler.openMessageDialog(
'dialog.sessTerminated',
'dialog.kickMessage');
messageHandler.showError({
hideErrorSupportLink: true,
descriptionKey: 'dialog.kickMessage',
titleKey: 'dialog.sessTerminated'
});
};
/**
@ -182,8 +182,10 @@ UI.notifyKicked = function() {
UI.notifyConferenceDestroyed = function(reason) {
// FIXME: use Session Terminated from translation, but
// 'reason' text comes from XMPP packet and is not translated
messageHandler.openDialog(
'dialog.sessTerminated', reason, true, {}, () => false);
messageHandler.showError({
description: reason,
titleKey: 'dialog.sessTerminated'
});
};
/**
@ -833,17 +835,21 @@ UI.setUserAvatarUrl = function(id, url) {
* @param {string} stropheErrorMsg raw Strophe error message
*/
UI.notifyConnectionFailed = function(stropheErrorMsg) {
let message;
let descriptionKey;
let descriptionArguments;
if (stropheErrorMsg) {
message = APP.translation.generateTranslationHTML(
'dialog.connectErrorWithMsg', { msg: stropheErrorMsg });
descriptionKey = 'dialog.connectErrorWithMsg';
descriptionArguments = { msg: stropheErrorMsg };
} else {
message = APP.translation.generateTranslationHTML(
'dialog.connectError');
descriptionKey = 'dialog.connectError';
}
messageHandler.openDialog('dialog.error', message, true, {}, () => false);
messageHandler.showError({
descriptionArguments,
descriptionKey,
titleKey: 'connection.CONNFAIL'
});
};
@ -851,10 +857,11 @@ UI.notifyConnectionFailed = function(stropheErrorMsg) {
* Notify user that maximum users limit has been reached.
*/
UI.notifyMaxUsersLimitReached = function() {
const message = APP.translation.generateTranslationHTML(
'dialog.maxUsersLimitReached');
messageHandler.openDialog('dialog.error', message, true, {}, () => false);
messageHandler.showError({
hideErrorSupportLink: true,
descriptionKey: 'dialog.maxUsersLimitReached',
titleKey: 'dialog.maxUsersLimitReachedTitle'
});
};
/**
@ -955,13 +962,18 @@ UI.updateRecordingState = function(state) {
};
UI.notifyTokenAuthFailed = function() {
messageHandler.showError('dialog.tokenAuthFailedTitle',
'dialog.tokenAuthFailed');
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
});
};
UI.notifyInternalError = function() {
messageHandler.showError('dialog.internalErrorTitle',
'dialog.internalError');
UI.notifyInternalError = function(error) {
messageHandler.showError({
descriptionArguments: { error },
descriptionKey: 'dialog.internalError',
titleKey: 'dialog.internalErrorTitle'
});
};
UI.notifyFocusDisconnected = function(focus, retrySec) {
@ -1158,7 +1170,8 @@ UI.showMicErrorNotification = function(micError) {
showToggle: Boolean(micJitsiTrackErrorMsg),
subtitleKey: 'dialog.micErrorPresent',
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.microphonePermission' : 'dialog.error',
? 'deviceError.microphonePermission'
: 'deviceError.microphoneError',
toggleLabelKey: 'dialog.doNotShowWarningAgain'
}));
};
@ -1194,7 +1207,7 @@ UI.showCameraErrorNotification = function(cameraError) {
showToggle: Boolean(cameraJitsiTrackErrorMsg),
subtitleKey: 'dialog.cameraErrorPresent',
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.cameraPermission' : 'dialog.error',
? 'deviceError.cameraPermission' : 'deviceError.cameraError',
toggleLabelKey: 'dialog.doNotShowWarningAgain'
}));
};
@ -1202,12 +1215,19 @@ UI.showCameraErrorNotification = function(cameraError) {
/**
* Shows error dialog that informs the user that no data is received from the
* device.
*
* @param {boolean} isAudioTrack - Whether or not the dialog is for an audio
* track error.
* @returns {void}
*/
UI.showTrackNotWorkingDialog = function(stream) {
messageHandler.openMessageDialog(
'dialog.error',
stream.isAudioTrack() ? 'dialog.micNotSendingData'
: 'dialog.cameraNotSendingData');
UI.showTrackNotWorkingDialog = function(isAudioTrack) {
messageHandler.showError({
descriptionKey: isAudioTrack
? 'dialog.micNotSendingData' : 'dialog.cameraNotSendingData',
titleKey: isAudioTrack
? 'dialog.micNotSendingDataTitle'
: 'dialog.cameraNotSendingDataTitle'
});
};
UI.updateDevicesAvailability = function(id, devices) {

View File

@ -203,7 +203,10 @@ export default {
);
if (!dialog) {
APP.UI.messageHandler.openMessageDialog(null, 'dialog.popupError');
APP.UI.messageHandler.showWarning({
descriptionKey: 'dialog.popupError',
titleKey: 'dialog.popupErrorTitle'
});
}
return dialog;

View File

@ -42,14 +42,16 @@ import {
*/
export const RECORDING_TRANSLATION_KEYS = {
failedToStartKey: 'recording.failedToStart',
recordingBusy: 'liveStreaming.busy',
recordingBusy: 'recording.busy',
recordingBusyTitle: 'recording.busyTitle',
recordingButtonTooltip: 'recording.buttonTooltip',
recordingErrorKey: 'recording.error',
recordingOffKey: 'recording.off',
recordingOnKey: 'recording.on',
recordingPendingKey: 'recording.pending',
recordingTitle: 'dialog.recording',
recordingUnavailable: 'recording.unavailable'
recordingUnavailable: 'recording.unavailable',
recordingUnavailableTitle: 'recording.unavailableTitle'
};
/**
@ -62,13 +64,15 @@ export const RECORDING_TRANSLATION_KEYS = {
export const STREAMING_TRANSLATION_KEYS = {
failedToStartKey: 'liveStreaming.failedToStart',
recordingBusy: 'liveStreaming.busy',
recordingBusyTitle: 'liveStreaming.busyTitle',
recordingButtonTooltip: 'liveStreaming.buttonTooltip',
recordingErrorKey: 'liveStreaming.error',
recordingOffKey: 'liveStreaming.off',
recordingOnKey: 'liveStreaming.on',
recordingPendingKey: 'liveStreaming.pending',
recordingTitle: 'dialog.liveStreaming',
recordingUnavailable: 'liveStreaming.unavailable'
recordingUnavailable: 'liveStreaming.unavailable',
recordingUnavailableTitle: 'liveStreaming.unavailableTitle'
};
/**
@ -513,25 +517,17 @@ const Recording = {
break;
}
case JitsiRecordingStatus.BUSY: {
dialog = APP.UI.messageHandler.openMessageDialog(
this.recordingTitle,
this.recordingBusy,
null,
() => {
dialog = null;
}
);
APP.UI.messageHandler.showWarning({
descriptionKey: this.recordingBusy,
titleKey: this.recordingBusyTitle
});
break;
}
default: {
dialog = APP.UI.messageHandler.openMessageDialog(
this.recordingTitle,
this.recordingUnavailable,
null,
() => {
dialog = null;
}
);
APP.UI.messageHandler.showError({
descriptionKey: this.recordingUnavailable,
titleKey: this.recordingUnavailableTitle
});
}
}
},

View File

@ -111,14 +111,10 @@ export default class SharedVideoManager {
},
() => {}); // eslint-disable-line no-empty-function
} else {
dialog = APP.UI.messageHandler.openMessageDialog(
'dialog.shareVideoTitle',
'dialog.alreadySharedVideoMsg',
null,
() => {
dialog = null;
}
);
APP.UI.messageHandler.showWarning({
descriptionKey: 'dialog.alreadySharedVideoMsg',
titleKey: 'dialog.alreadySharedVideoTitle'
});
sendAnalyticsEvent('sharedvideo.alreadyshared');
}
}

View File

@ -5,7 +5,9 @@ import jitsiLocalStorage from '../../util/JitsiLocalStorage';
import {
Notification,
showNotification
showErrorNotification,
showNotification,
showWarningNotification
} from '../../../react/features/notifications';
/**
@ -453,26 +455,23 @@ const messageHandler = {
},
/**
* Shows a dialog prompting the user to send an error report.
* Shows an error dialog to the user.
*
* @param titleKey the title of the message
* @param msgKey the text of the message
* @param error the error that is being reported
* @param {object} props - The properties to pass to the
* showErrorNotification action.
*/
openReportDialog(titleKey, msgKey, error) {
this.openMessageDialog(titleKey, msgKey);
logger.log(error);
// FIXME send the error to the server
showError(props) {
APP.store.dispatch(showErrorNotification(props));
},
/**
* Shows an error dialog to the user.
* @param titleKey the title of the message.
* @param msgKey the text of the message.
* Shows a warning dialog to the user.
*
* @param {object} props - The properties to pass to the
* showWarningNotification action.
*/
showError(titleKey = 'dialog.oops', msgKey = 'dialog.defaultError') {
messageHandler.openMessageDialog(titleKey, msgKey);
showWarning(props) {
APP.store.dispatch(showWarningNotification(props));
},
/**
@ -498,9 +497,9 @@ const messageHandler = {
showNotification(
Notification,
{
defaultTitleKey: displayNameKey,
descriptionArguments: messageArguments,
descriptionKey: messageKey,
titleKey: displayNameKey,
title: displayName
},
timeout));

View File

@ -75,7 +75,8 @@ export function createLocalTracksF(
tracks.forEach(track =>
track.on(
JitsiTrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(null, track)));
APP.UI.showTrackNotWorkingDialog.bind(
null, track.isAudioTrack())));
}
return tracks;

View File

@ -77,11 +77,13 @@ function _obtainConfigAndInit() {
_initConference();
})
.catch(err => {
logger.log(err);
// Show obtain config error.
APP.UI.messageHandler.openReportDialog(
null,
'dialog.connectError',
err);
APP.UI.messageHandler.showError({
titleKey: 'connection.CONNFAIL',
descriptionKey: 'dialog.connectError'
});
});
} else {
chooseBOSHAddress(config, room);

View File

@ -5,7 +5,12 @@ import {
SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION
} from './actionTypes';
import { NotificationWithToggle } from './components';
import {
Notification,
NotificationWithToggle
} from './components';
import { NOTIFICATION_TYPE } from './constants';
/**
* Removes the notification with the passed in id.
@ -40,6 +45,19 @@ export function setNotificationsEnabled(enabled) {
};
}
/**
* Queues an error notification for display.
*
* @param {Object} props - The props needed to show the notification component.
* @returns {Object}
*/
export function showErrorNotification(props) {
return showNotification(Notification, {
...props,
appearance: NOTIFICATION_TYPE.ERROR
});
}
/**
* Queues a notification for display.
*
@ -66,6 +84,19 @@ export function showNotification(component, props = {}, timeout) {
};
}
/**
* Queues a warning notification for display.
*
* @param {Object} props - The props needed to show the notification component.
* @returns {Object}
*/
export function showWarningNotification(props) {
return showNotification(Notification, {
...props,
appearance: NOTIFICATION_TYPE.WARNING
});
}
/**
* Displays a notification unless the passed in persistenceKey value exists in
* local storage and has been set to "true".

View File

@ -1,16 +1,45 @@
import Flag from '@atlaskit/flag';
import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
import PropTypes from 'prop-types';
import ErrorIcon from '@atlaskit/icon/glyph/error';
import WarningIcon from '@atlaskit/icon/glyph/warning';
import { colors } from '@atlaskit/theme';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { NOTIFICATION_TYPE } from '../constants';
declare var interfaceConfig: Object;
/**
* Secondary colors for notification icons.
*
* @type {{error, info, normal, success, warning}}
*/
const ICON_COLOR = {
error: colors.R400,
info: colors.N500,
normal: colors.N0,
success: colors.G400,
warning: colors.Y200
};
/**
* Implements a React {@link Component} to display a notification.
*
* @extends Component
*/
class Notification extends Component {
/**
* Default values for {@code Notification} component's properties.
*
* @static
*/
static defaultProps = {
appearance: NOTIFICATION_TYPE.NORMAL
};
/**
* {@code Notification} component's property types.
*
@ -18,11 +47,22 @@ class Notification extends Component {
*/
static propTypes = {
/**
* The translation key to display as the title of the notification if
* no title is provided.
* Display appearance for the component, passed directly to
* {@code Flag}.
*/
appearance: PropTypes.string,
/**
* The text to display in the body of the notification. If not passed
* in, the passed in descriptionKey will be used.
*/
defaultTitleKey: PropTypes.string,
/**
* The description string.
*/
description: PropTypes.string,
/**
* The translation arguments that may be necessary for the description.
*/
@ -33,6 +73,12 @@ class Notification extends Component {
*/
descriptionKey: PropTypes.string,
/**
* Whether the support link should be hidden in the case of an error
* message.
*/
hideErrorSupportLink: PropTypes.bool,
/**
* Whether or not the dismiss button should be displayed. This is passed
* in by {@code FlagGroup}.
@ -52,10 +98,16 @@ class Notification extends Component {
/**
* The text to display at the top of the notification. If not passed in,
* the passed in defaultTitleKey will be used.
* the passed in titleKey will be used.
*/
title: PropTypes.string,
/**
* The translation key to display as the title of the notification if
* no title is provided.
*/
titleKey: PropTypes.string,
/**
* The unique identifier for the notification. Passed back by the
* {@code Flag} component in the onDismissed callback.
@ -63,6 +115,19 @@ class Notification extends Component {
uid: PropTypes.number
};
/**
* Initializes a new {@code Notification} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@ -71,9 +136,12 @@ class Notification extends Component {
*/
render() {
const {
defaultTitleKey,
hideErrorSupportLink,
appearance,
titleKey,
descriptionArguments,
descriptionKey,
description,
isDismissAllowed,
onDismissed,
t,
@ -83,19 +151,117 @@ class Notification extends Component {
return (
<Flag
appearance = 'normal'
description = { t(descriptionKey, descriptionArguments) }
icon = { (
<EditorInfoIcon
label = 'info'
size = 'medium' />
) }
actions = { this._mapAppearanceToButtons(hideErrorSupportLink) }
appearance = { appearance }
description = { description
|| t(descriptionKey, descriptionArguments) }
icon = { this._mapAppearanceToIcon() }
id = { uid }
isDismissAllowed = { isDismissAllowed }
onDismissed = { onDismissed }
title = { title || t(defaultTitleKey) } />
title = { title || t(titleKey) } />
);
}
/**
* Calls back into {@code FlagGroup} to dismiss the notification.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.onDismissed(this.props.uid);
}
/**
* Opens the support page.
*
* @returns {void}
* @private
*/
_onOpenSupportLink() {
window.open(interfaceConfig.SUPPORT_URL, '_blank', 'noopener');
}
/**
* Creates action button configurations for the notification based on
* notification appearance.
*
* @param {boolean} hideErrorSupportLink - Indicates if the support link
* should be hidden in the error messages.
* @private
* @returns {Object[]}
*/
_mapAppearanceToButtons(hideErrorSupportLink) {
switch (this.props.appearance) {
case NOTIFICATION_TYPE.ERROR: {
const buttons = [
{
content: this.props.t('dialog.dismiss'),
onClick: this._onDismissed
}
];
if (!hideErrorSupportLink) {
buttons.push({
content: this.props.t('dialog.contactSupport'),
onClick: this._onOpenSupportLink
});
}
return buttons;
}
case NOTIFICATION_TYPE.WARNING:
return [
{
content: this.props.t('dialog.Ok'),
onClick: this._onDismissed
}
];
default:
return [];
}
}
/**
* Creates an icon component depending on the configured notification
* appearance.
*
* @private
* @returns {ReactElement}
*/
_mapAppearanceToIcon() {
const appearance = this.props.appearance;
const secIconColor = ICON_COLOR[this.props.appearance];
const iconSize = 'medium';
switch (appearance) {
case NOTIFICATION_TYPE.ERROR:
return (
<ErrorIcon
label = { appearance }
secondaryColor = { secIconColor }
size = { iconSize } />
);
case NOTIFICATION_TYPE.WARNING:
return (
<WarningIcon
label = { appearance }
secondaryColor = { secIconColor }
size = { iconSize } />
);
default:
return (
<EditorInfoIcon
label = { appearance }
secondaryColor = { secIconColor }
size = { iconSize } />
);
}
}
}
export default translate(Notification);

View File

@ -1,11 +1,12 @@
import Flag from '@atlaskit/flag';
import WarningIcon from '@atlaskit/icon/glyph/warning';
import { ToggleStateless } from '@atlaskit/toggle';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { default as Notification } from './Notification';
import { NOTIFICATION_TYPE } from '../constants';
/**
* React {@code Component} for displaying a notification with a toggle element.
*
@ -18,29 +19,14 @@ class NotificationWithToggle extends Component {
* @static
*/
static propTypes = {
...Notification.propTypes,
/**
* Any additional text to display at the end of the notification message
* body.
*/
additionalMessage: PropTypes.string,
/**
* Whether or not the dismiss button should be displayed. This is passed
* in by {@code FlagGroup}.
*/
isDismissAllowed: PropTypes.bool,
/**
* The translation key to be used as the main body of the notification.
*/
messageKey: PropTypes.string,
/**
* Callback invoked when the user clicks to dismiss the notification.
* This is passed in by {@code FlagGroup}.
*/
onDismissed: PropTypes.func,
/**
* Optional callback to invoke when the notification is dismissed. The
* current value of the toggle element will be passed in.
@ -58,27 +44,11 @@ class NotificationWithToggle extends Component {
*/
subtitleKey: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* The translation key to be used as the title of the notification.
*/
titleKey: PropTypes.string,
/*
* The translation key to be used as a label describing what setting the
* toggle will change.
*/
toggleLabelKey: PropTypes.string,
/**
* The unique identifier for the notification. Passed back by the
* {@code Flag} component in the onDismissed callback.
*/
uid: PropTypes.number
toggleLabelKey: PropTypes.string
};
/**
@ -111,32 +81,11 @@ class NotificationWithToggle extends Component {
* @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) } />
<Notification
appearance = { NOTIFICATION_TYPE.WARNING }
{ ...this.props }
description = { this._renderDescription() } />
);
}
@ -181,7 +130,7 @@ class NotificationWithToggle extends Component {
_renderDescription() {
const {
additionalMessage,
messageKey,
descriptionKey,
showToggle,
subtitleKey,
t,
@ -191,7 +140,7 @@ class NotificationWithToggle extends Component {
return (
<div className = 'notification-with-toggle'>
<div>{ t(subtitleKey) }</div>
{ messageKey ? <div>{ t(messageKey) }</div> : null }
{ descriptionKey ? <div>{ t(descriptionKey) }</div> : null }
{ additionalMessage ? <div>{ additionalMessage }</div>
: null }
{ showToggle

View File

@ -0,0 +1,12 @@
/**
* The set of possible notification types.
*
* @enum {string}
*/
export const NOTIFICATION_TYPE = {
ERROR: 'error',
INFO: 'info',
NORMAL: 'normal',
SUCCESS: 'success',
WARNING: 'warning'
};

View File

@ -75,19 +75,22 @@ function _setPasswordFailed(store, next, action) {
// TODO Remove this logic when displaying of error messages on web is
// handled through react/redux.
const { error } = action;
let title;
let message;
let descriptionKey;
let titleKey;
if (error === JitsiConferenceErrors.PASSWORD_NOT_SUPPORTED) {
logger.warn('room passwords not supported');
title = 'dialog.warning';
message = 'dialog.passwordNotSupported';
descriptionKey = 'dialog.passwordNotSupported';
titleKey = 'dialog.passwordNotSupportedTitle';
} else {
logger.warn('setting password failed', error);
title = 'dialog.lockTitle';
message = 'dialog.lockMessage';
descriptionKey = 'dialog.lockMessage';
titleKey = 'dialog.lockTitle';
}
APP.UI.messageHandler.showError(title, message);
APP.UI.messageHandler.showError({
descriptionKey,
titleKey
});
}
return next(action);