diff --git a/app.js b/app.js index 1d2124a2ce8b..51cd2cc9ed3d 100644 --- a/app.js +++ b/app.js @@ -13,8 +13,6 @@ import 'aui-experimental'; import 'aui-css'; import 'aui-experimental-css'; -window.toastr = require('toastr'); - import conference from './conference'; import API from './modules/API'; import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut'; diff --git a/css/_toastr.scss b/css/_toastr.scss deleted file mode 100644 index 663240686e72..000000000000 --- a/css/_toastr.scss +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Toastr - * Copyright 2012-2014 John Papa and Hans Fjällemark. - * All Rights Reserved. - * Use, reproduction, distribution, and modification of this code is subject to the terms and - * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php - * - * Author: John Papa and Hans Fjällemark - * Project: https://github.com/CodeSeven/toastr - * - * Last updated: October 13, 2016 - */ - -.toast-title, -.toast-message .title { - font-weight: bold; - margin: 0 0 3px; - @include text-truncate; -} - -.toast-message { - -ms-word-wrap: break-word; - word-wrap: break-word; -} - -.toast-message a, -.toast-message label, -.toast-message .connected, -.toast-message .disconnected { - color: $notificationLinkColor; - text-decoration: none; -} - -.toast-message a:hover { - text-decoration: underline; -} - -.toast-message br { - display: none; -} - -// close button -.toast-close-button { - color: $notificationColor; - background: transparent; - - font-size: 15px; - line-height: 1.2; - - height: 20px; - width: 20px; - padding: 0; - border: 0; - margin: -6px -10px 0 0; - float: right; - - cursor: pointer; - @include opacity(0.4); - /* Additional properties for button version - iOS requires the button element instead of an anchor tag. - If you want the anchor version, it requires `href="#"`. */ - -webkit-appearance: none; -} - -.toast-close-button:hover, -.toast-close-button:focus { - @include opacity(1); -} - - -.toast { - color: $notificationColor; - background-color: $notificationBackground; - - font-size: $notificationFontSize; - - padding: $notificationPadding; - border: 1px solid lighten($notificationBackground, 10%); - - @include border-radius($notificationBorderRadius); - @include box-shadow(1px, 1px, 2px, rgba(0,0,0,0.3)); - @include opacity($notificationOpacity); -} - -.toast:hover { - @include opacity(1); -} - -#toast-container { - position: fixed; - z-index: $notificationZ; -} - -#toast-container.notification-bottom-right { - $videoOffset: 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder; - bottom: 135px; - right: $filmstripToggleButtonWidth + $videoOffset; -} - -#toast-container * { - @include box-sizing(border-box); -} - -#toast-container .toast { - width: $notificationWidth; - margin: 0 0 8px; -} diff --git a/css/_variables.scss b/css/_variables.scss index ccd1826b3811..2cf78565f71a 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -85,20 +85,6 @@ $modalMockAKInputBackground: #fafbfc; $modalMockAKInputBorder: 1px solid #f4f5f7; $modalTextColor: #333; -/** - * Notifications - */ -$notificationFontSize: 13px; -$notificationColor: #FFFFFF; -$notificationBackground: $tooltipBg; -$notificationTitleColor: $notificationColor; -$notificationMessageColor: $notificationColor; -$notificationLinkColor: $notificationColor; -$notificationOpacity: 0.9; -$notificationPadding: 15px 20px; -$notificationBorderRadius: 4px; -$notificationWidth: 215px; - /** * Misc. */ @@ -126,7 +112,6 @@ $tooltipsZ: 401; $dropdownMaskZ: 900; $dropdownZ: 901; $centeredVideoLabelZ: 1010; -$notificationZ: 1011; $jitsipopoverZ: 1012; $popoverZ: 1015; $overlayZ: 1016; diff --git a/css/_vertical_filmstrip_overrides.scss b/css/_vertical_filmstrip_overrides.scss index d15f58278897..434931d49c9c 100644 --- a/css/_vertical_filmstrip_overrides.scss +++ b/css/_vertical_filmstrip_overrides.scss @@ -159,15 +159,4 @@ transition-delay: 0.1s; } } - - /** - * Move toastr closer to the bottom of the screen and move left to avoid - * overlapping of videos when they are configured at default height. - */ - #toast-container { - &.notification-bottom-right { - bottom: 25px; - right: 130 + 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder; - } - } } diff --git a/css/main.scss b/css/main.scss index 676f4c5c9e1a..6b7c755b4f42 100644 --- a/css/main.scss +++ b/css/main.scss @@ -33,7 +33,6 @@ /* Modules BEGIN */ @import 'dial-out'; -@import 'toastr'; @import 'base'; @import 'utils'; @import 'overlay/overlay'; diff --git a/debian/patches/jquery-package b/debian/patches/jquery-package index ef0750c72525..5a89baed516c 100644 --- a/debian/patches/jquery-package +++ b/debian/patches/jquery-package @@ -3,7 +3,7 @@ Index: jitsi-meet/index.html =================================================================== --- jitsi-meet.orig/index.html +++ jitsi-meet/index.html -@@ -10,14 +10,14 @@ +@@ -10,13 +10,13 @@ @@ -19,4 +19,4 @@ Index: jitsi-meet/index.html + - +- diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 4ee560482446..39e3fb3a0526 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1,4 +1,4 @@ -/* global APP, JitsiMeetJS, $, config, interfaceConfig, toastr */ +/* global APP, JitsiMeetJS, $, config, interfaceConfig */ const logger = require("jitsi-meet-logger").getLogger(__filename); @@ -343,26 +343,6 @@ UI.start = function () { } document.title = interfaceConfig.APP_NAME; - - if (!interfaceConfig.filmStripOnly) { - toastr.options = { - "closeButton": true, - "debug": false, - "positionClass": "notification-bottom-right", - "onclick": null, - "showDuration": "300", - "hideDuration": "1000", - "timeOut": "2000", - "extendedTimeOut": "1000", - "showEasing": "swing", - "hideEasing": "linear", - "showMethod": "fadeIn", - "hideMethod": "fadeOut", - "newestOnTop": false, - // this is the default toastr close button html, just adds tabIndex - "closeHtml": '' - }; - } }; /** @@ -868,7 +848,7 @@ UI.notifyInitiallyMuted = function () { "connected", "notify.muted", null, - { timeOut: 120000 }); + 120000); }; /** diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index 9672ada6f657..57ca94cec3e2 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -1,9 +1,13 @@ -/* global $, APP, toastr */ +/* global $, APP */ const logger = require("jitsi-meet-logger").getLogger(__filename); -import UIUtil from './UIUtil'; import jitsiLocalStorage from '../../util/JitsiLocalStorage'; +import { + Notification, + showNotification +} from '../../../react/features/notifications'; + /** * Flag for enable/disable of the notifications. * @type {boolean} @@ -448,31 +452,25 @@ var messageHandler = { * @param messageKey the key from the language file for the text of the * message. * @param messageArguments object with the arguments for the message. - * @param options passed to toastr (e.g. timeOut) + * @param optional configurations for the notification (e.g. timeout) */ participantNotification: function(displayName, displayNameKey, cls, - messageKey, messageArguments, options) { - - // If we're in ringing state we skip all toaster notifications. - if(!notificationsEnabled || APP.UI.isOverlayVisible()) + messageKey, messageArguments, timeout) { + // If we're in ringing state we skip all notifications. + if (!notificationsEnabled || APP.UI.isOverlayVisible()) { return; - - var displayNameSpan = '"; } - displayNameSpan += ""; - let element = toastr.info( - displayNameSpan + '
' + - '", null, options); - APP.translation.translateElement(element); - return element; + + APP.store.dispatch( + showNotification( + Notification, + { + defaultTitleKey: displayNameKey, + descriptionArguments: messageArguments, + descriptionKey: messageKey, + title: displayName + }, + timeout)); }, /** @@ -488,28 +486,12 @@ var messageHandler = { */ notify: function(titleKey, messageKey, messageArguments) { - // If we're in ringing state we skip all toaster notifications. + // If we're in ringing state we skip all notifications. if(!notificationsEnabled || APP.UI.isOverlayVisible()) return; - const options = messageArguments - ? `data-i18n-options='${JSON.stringify(messageArguments)}'` : ""; - let element = toastr.info(` - -
- - ` - ); - APP.translation.translateElement(element); - return element; - }, - - /** - * Removes the toaster. - * @param toasterElement - */ - remove: function(toasterElement) { - toasterElement.remove(); + this.participantNotification( + null, titleKey, null, messageKey, messageArguments); }, /** diff --git a/package.json b/package.json index b8fbe733f04d..76929cc99fb3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@atlaskit/button-group": "1.1.3", "@atlaskit/dropdown-menu": "1.4.0", "@atlaskit/field-text": "2.7.0", + "@atlaskit/flag": "3.0.0", "@atlaskit/icon": "7.0.0", "@atlaskit/inline-dialog": "3.2.0", "@atlaskit/inline-message": "2.1.1", @@ -67,7 +68,6 @@ "strophe": "1.2.4", "strophejs-plugins": "0.0.7", "styled-components": "1.3.0", - "toastr": "2.1.2", "url-polyfill": "github/url-polyfill", "xmldom": "0.1.27" }, diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index bc558d6c7e4c..a4ead621326a 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection'; import { DialogContainer } from '../../base/dialog'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; +import { NotificationsContainer } from '../../notifications'; import { OverlayContainer } from '../../overlay'; import { Toolbox } from '../../toolbox'; import { HideNotificationBarStyle } from '../../unsupported-browser'; @@ -78,6 +79,7 @@ class Conference extends Component { { filmStripOnly ? null : } + { filmStripOnly ? null : } {/* diff --git a/react/features/notifications/actionTypes.js b/react/features/notifications/actionTypes.js new file mode 100644 index 000000000000..4b8593a6d1c1 --- /dev/null +++ b/react/features/notifications/actionTypes.js @@ -0,0 +1,24 @@ +/* + * The type of (redux) action which signals that a specific notification should + * not be displayed anymore. + * + * { + * type: HIDE_NOTIFICATION, + * uid: string + * } + */ +export const HIDE_NOTIFICATION = Symbol('HIDE_NOTIFICATION'); + +/* + * The type of (redux) action which signals that a notification component should + * be displayed. + * + * { + * type: SHOW_NOTIFICATION, + * component: ReactComponent, + * props: Object, + * timeout: number, + * uid: number + * } + */ +export const SHOW_NOTIFICATION = Symbol('SHOW_NOTIFICATION'); diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js new file mode 100644 index 000000000000..ce56b432deb0 --- /dev/null +++ b/react/features/notifications/actions.js @@ -0,0 +1,47 @@ +import { + HIDE_NOTIFICATION, + SHOW_NOTIFICATION +} from './actionTypes'; + +/** + * Removes the notification with the passed in id. + * + * @param {string} uid - The unique identifier for the notification to be + * removed. + * @returns {{ + * type: HIDE_NOTIFICATION, + * uid: string + * }} + */ +export function hideNotification(uid) { + return { + type: HIDE_NOTIFICATION, + uid + }; +} + +/** + * Queues a notification for display. + * + * @param {ReactComponent} component - The notification component to be + * displayed. + * @param {Object} props - The props needed to show the notification component. + * @param {number} timeout - How long the notification should display before + * automatically being hidden. + * @returns {{ + * type: SHOW_NOTIFICATION, + * component: ReactComponent, + * props: Object, + * timeout: number, + * uid: number + * }} + */ +export function showNotification(component, props = {}, timeout) { + return { + type: SHOW_NOTIFICATION, + component, + props, + timeout, + uid: window.Date.now() + }; +} diff --git a/react/features/notifications/components/Notification.native.js b/react/features/notifications/components/Notification.native.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/react/features/notifications/components/Notification.web.js b/react/features/notifications/components/Notification.web.js new file mode 100644 index 000000000000..e49520c69efb --- /dev/null +++ b/react/features/notifications/components/Notification.web.js @@ -0,0 +1,100 @@ +import Flag from '@atlaskit/flag'; +import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info'; +import React, { Component } from 'react'; + +import { translate } from '../../base/i18n'; + +/** + * Implements a React {@link Component} to display a notification. + * + * @extends Component + */ +class Notification extends Component { + /** + * {@code Notification} component's property types. + * + * @static + */ + static propTypes = { + /** + * The translation key to display as the title of the notification if + * no title is provided. + */ + defaultTitleKey: React.PropTypes.string, + + /** + * The translation arguments that may be necessary for the description. + */ + descriptionArguments: React.PropTypes.object, + + /** + * The translation key to use as the body of the notification. + */ + descriptionKey: React.PropTypes.string, + + /** + * Whether or not the dismiss button should be displayed. This is passed + * in by {@code FlagGroup}. + */ + isDismissAllowed: React.PropTypes.bool, + + /** + * Callback invoked when the user clicks to dismiss the notification. + * this is passed in by {@code FlagGroup}. + */ + onDismissed: React.PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: React.PropTypes.func, + + /** + * The text to display at the top of the notification. If not passed in, + * the passed in defaultTitleKey will be used. + */ + title: React.PropTypes.string, + + /** + * The unique identifier for the notification. Passed back by the + * {@code Flag} component in the onDismissed callback. + */ + uid: React.PropTypes.number + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { + defaultTitleKey, + descriptionArguments, + descriptionKey, + isDismissAllowed, + onDismissed, + t, + title, + uid + } = this.props; + + return ( + + ) } + id = { uid } + isDismissAllowed = { isDismissAllowed } + onDismissed = { onDismissed } + title = { title || t(defaultTitleKey) } /> + ); + } +} + +export default translate(Notification); diff --git a/react/features/notifications/components/NotificationsContainer.native.js b/react/features/notifications/components/NotificationsContainer.native.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/react/features/notifications/components/NotificationsContainer.web.js b/react/features/notifications/components/NotificationsContainer.web.js new file mode 100644 index 000000000000..32447f00ee71 --- /dev/null +++ b/react/features/notifications/components/NotificationsContainer.web.js @@ -0,0 +1,157 @@ +import { FlagGroup } from '@atlaskit/flag'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { hideNotification } from '../actions'; + +/** + * The duration for which a notification should be displayed before being + * dismissed automatically. + * + * @type {number} + */ +const DEFAULT_NOTIFICATION_TIMEOUT = 2500; + +/** + * Implements a React {@link Component} which displays notifications and handles + * automatic dismissmal after a notification is shown for a defined timeout + * period. + * + * @extends {Component} + */ +class NotificationsContainer extends Component { + /** + * {@code NotificationsContainer} component's property types. + * + * @static + */ + static propTypes = { + /** + * The notifications to be displayed, with the first index being the + * notification at the top and the rest shown below it in order. + */ + _notifications: React.PropTypes.array, + + /** + * Invoked to update the redux store in order to remove notifications. + */ + dispatch: React.PropTypes.func + }; + + /** + * Initializes a new {@code NotificationsContainer} instance. + * + * @param {Object} props - The read-only React Component props with which + * the new instance is to be initialized. + */ + constructor(props) { + super(props); + + /** + * The timeout set for automatically dismissing a displayed + * notification. This value is set on the instance and not state to + * avoid additional re-renders. + * + * @type {number|null} + */ + this._notificationDismissTimeout = null; + + // Bind event handlers so they are only bound once for every instance. + this._onDismissed = this._onDismissed.bind(this); + } + + /** + * Sets a timeout if the currently displayed notification has changed. + * + * @inheritdoc + * returns {void} + */ + componentDidUpdate() { + const { _notifications } = this.props; + + if (_notifications.length && !this._notificationDismissTimeout) { + const notification = _notifications[0]; + const { timeout, uid } = notification; + + this._notificationDismissTimeout = setTimeout(() => { + this._onDismissed(uid); + }, timeout || DEFAULT_NOTIFICATION_TIMEOUT); + } + } + + /** + * Clear any dismissal timeout that is still active. + * + * @inheritdoc + * returns {void} + */ + componentWillUnmount() { + clearTimeout(this._notificationDismissTimeout); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _notifications } = this.props; + + const flags = _notifications.map(notification => { + const Notification = notification.component; + const { props, uid } = notification; + + // The id attribute is necessary as {@code FlagGroup} looks for + // either id or key to set a key on notifications, but accessing + // props.key will cause React to print an error. + return ( + + + ); + }); + + return ( + + { flags } + + ); + } + + /** + * Emits an action to remove the notification from the redux store so it + * stops displaying. + * + * @param {number} flagUid - The id of the notification to be removed. + * @private + * @returns {void} + */ + _onDismissed(flagUid) { + clearTimeout(this._notificationDismissTimeout); + this._notificationDismissTimeout = null; + + this.props.dispatch(hideNotification(flagUid)); + } +} + +/** + * Maps (parts of) the Redux state to the associated NotificationsContainer's + * props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _notifications: React.PropTypes.array + * }} + */ +function _mapStateToProps(state) { + return { + _notifications: state['features/notifications'] + }; +} + +export default connect(_mapStateToProps)(NotificationsContainer); diff --git a/react/features/notifications/components/index.js b/react/features/notifications/components/index.js new file mode 100644 index 000000000000..642f2f2837cc --- /dev/null +++ b/react/features/notifications/components/index.js @@ -0,0 +1,2 @@ +export { default as Notification } from './Notification'; +export { default as NotificationsContainer } from './NotificationsContainer'; diff --git a/react/features/notifications/index.js b/react/features/notifications/index.js new file mode 100644 index 000000000000..b2e9c09f6caf --- /dev/null +++ b/react/features/notifications/index.js @@ -0,0 +1,5 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; + +import './reducer'; diff --git a/react/features/notifications/reducer.js b/react/features/notifications/reducer.js new file mode 100644 index 000000000000..a510806c6247 --- /dev/null +++ b/react/features/notifications/reducer.js @@ -0,0 +1,43 @@ +import { ReducerRegistry } from '../base/redux'; + +import { + HIDE_NOTIFICATION, + SHOW_NOTIFICATION +} from './actionTypes'; + +/** + * The initial state of the feature notifications. + * + * @type {array} + */ +const DEFAULT_STATE = []; + +/** + * Reduces redux actions which affect the display of notifications. + * + * @param {Object} state - The current redux state. + * @param {Object} action - The redux action to reduce. + * @returns {Object} The next redux state which is the result of reducing the + * specified {@code action}. + */ +ReducerRegistry.register('features/notifications', + (state = DEFAULT_STATE, action) => { + switch (action.type) { + case HIDE_NOTIFICATION: + return state.filter( + notification => notification.uid !== action.uid); + + case SHOW_NOTIFICATION: + return [ + ...state, + { + component: action.component, + props: action.props, + timeout: action.timeout, + uid: action.uid + } + ]; + } + + return state; + });