From 510334fa7f14ab1e81371d70873960d29df2050f Mon Sep 17 00:00:00 2001 From: virtuacoplenny Date: Fri, 3 Nov 2017 12:05:03 -0700 Subject: [PATCH] =?UTF-8?q?ref(notifications):=20convert=20some=20dialogs?= =?UTF-8?q?=20to=20error=20or=20warning=20notifica=E2=80=A6=20(#1991)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- conference.js | 26 ++- interface_config.js | 2 +- lang/main.json | 66 +++--- modules/UI/UI.js | 102 ++++++---- modules/UI/authentication/LoginDialog.js | 5 +- modules/UI/recording/Recording.js | 34 ++-- modules/UI/shared_video/SharedVideo.js | 12 +- modules/UI/util/MessageHandler.js | 31 ++- react/features/base/tracks/functions.js | 3 +- react/features/conference/route.js | 10 +- react/features/notifications/actions.js | 33 ++- .../components/Notification.web.js | 190 ++++++++++++++++-- .../components/NotificationWithToggle.web.js | 75 ++----- react/features/notifications/constants.js | 12 ++ react/features/room-lock/middleware.js | 17 +- 15 files changed, 408 insertions(+), 210 deletions(-) create mode 100644 react/features/notifications/constants.js diff --git a/conference.js b/conference.js index 6eb558969..eb69d17b9 100644 --- a/conference.js +++ b/conference.js @@ -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 + }); }, /** diff --git a/interface_config.js b/interface_config.js index 1639e7711..f894cfc35 100644 --- a/interface_config.js +++ b/interface_config.js @@ -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 diff --git a/lang/main.json b/lang/main.json index 9b8eae87b..591f2b5e6 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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 __room__ 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" }, diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 6f310e403..0d89cf202 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -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) { diff --git a/modules/UI/authentication/LoginDialog.js b/modules/UI/authentication/LoginDialog.js index 5ef5b1472..55e0c6f49 100644 --- a/modules/UI/authentication/LoginDialog.js +++ b/modules/UI/authentication/LoginDialog.js @@ -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; diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 7dc0cc072..af77e97ad 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -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 + }); } } }, diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 77b26fdfa..dea88da83 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -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'); } } diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index dbeedf680..bc5fc99f6 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -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)); diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index add0f5b52..9e7af72c7 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -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; diff --git a/react/features/conference/route.js b/react/features/conference/route.js index 3e208f25e..3478b0f8f 100644 --- a/react/features/conference/route.js +++ b/react/features/conference/route.js @@ -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); diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js index bea4ee066..52fc40250 100644 --- a/react/features/notifications/actions.js +++ b/react/features/notifications/actions.js @@ -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". diff --git a/react/features/notifications/components/Notification.web.js b/react/features/notifications/components/Notification.web.js index 0f31999c4..a1c542c60 100644 --- a/react/features/notifications/components/Notification.web.js +++ b/react/features/notifications/components/Notification.web.js @@ -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 ( - ) } + 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 ( + + ); + + case NOTIFICATION_TYPE.WARNING: + return ( + + ); + + default: + return ( + + ); + } + } } export default translate(Notification); diff --git a/react/features/notifications/components/NotificationWithToggle.web.js b/react/features/notifications/components/NotificationWithToggle.web.js index ef6121b6a..9215f4c7d 100644 --- a/react/features/notifications/components/NotificationWithToggle.web.js +++ b/react/features/notifications/components/NotificationWithToggle.web.js @@ -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 ( - - ) } - id = { uid } - isDismissAllowed = { isDismissAllowed } - onDismissed = { this._onDismissed } - title = { t(titleKey) } /> + ); } @@ -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 (
{ t(subtitleKey) }
- { messageKey ?
{ t(messageKey) }
: null } + { descriptionKey ?
{ t(descriptionKey) }
: null } { additionalMessage ?
{ additionalMessage }
: null } { showToggle diff --git a/react/features/notifications/constants.js b/react/features/notifications/constants.js new file mode 100644 index 000000000..7d01cd10f --- /dev/null +++ b/react/features/notifications/constants.js @@ -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' +}; diff --git a/react/features/room-lock/middleware.js b/react/features/room-lock/middleware.js index 9ba58b539..b0b82356f 100644 --- a/react/features/room-lock/middleware.js +++ b/react/features/room-lock/middleware.js @@ -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);