From 92d0589a37afb5a9006864956e96f3fd155aefd9 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Tue, 31 Jan 2017 14:58:48 -0600 Subject: [PATCH] ref(overlay): The overlays to use React --- conference.js | 86 +++-- connection.js | 23 ++ css/reload_overlay/_reload_overlay.scss | 4 +- modules/UI/UI.js | 58 +--- .../UserMediaPermissionsGuidanceOverlay.js | 90 ------ modules/UI/overlay/Overlay.js | 94 ------ .../UI/reload_overlay/PageReloadOverlay.js | 175 ---------- .../UI/suspended_overlay/SuspendedOverlay.js | 63 ---- react/features/app/components/AbstractApp.js | 8 + react/features/base/conference/actions.js | 5 +- react/features/base/conference/middleware.js | 7 +- .../base/connection/actions.native.js | 43 +-- react/features/base/connection/actions.web.js | 5 + .../conference/components/Conference.web.js | 3 + react/features/overlay/actionTypes.js | 25 ++ react/features/overlay/actions.js | 40 +++ .../overlay/components/AbstractOverlay.js | 86 +++++ .../overlay/components/OverlayContainer.js | 213 +++++++++++++ .../overlay/components/PageReloadOverlay.js | 298 ++++++++++++++++++ .../overlay/components/SuspendedOverlay.js | 37 +++ .../components/UserMediaPermissionsOverlay.js | 98 ++++++ react/features/overlay/components/index.js | 1 + react/features/overlay/index.js | 2 + react/features/overlay/reducer.js | 145 +++++++++ 24 files changed, 1068 insertions(+), 541 deletions(-) delete mode 100644 modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js delete mode 100644 modules/UI/overlay/Overlay.js delete mode 100644 modules/UI/reload_overlay/PageReloadOverlay.js delete mode 100644 modules/UI/suspended_overlay/SuspendedOverlay.js create mode 100644 react/features/overlay/actionTypes.js create mode 100644 react/features/overlay/actions.js create mode 100644 react/features/overlay/components/AbstractOverlay.js create mode 100644 react/features/overlay/components/OverlayContainer.js create mode 100644 react/features/overlay/components/PageReloadOverlay.js create mode 100644 react/features/overlay/components/SuspendedOverlay.js create mode 100644 react/features/overlay/components/UserMediaPermissionsOverlay.js create mode 100644 react/features/overlay/components/index.js create mode 100644 react/features/overlay/index.js create mode 100644 react/features/overlay/reducer.js diff --git a/conference.js b/conference.js index f4f79b265..6584ef310 100644 --- a/conference.js +++ b/conference.js @@ -20,6 +20,12 @@ import analytics from './modules/analytics/analytics'; import EventEmitter from "events"; +import { conferenceFailed } from './react/features/base/conference'; +import { + mediaPermissionPromptVisibilityChanged, + suspendDetected +} from './react/features/overlay'; + const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; @@ -91,7 +97,10 @@ function createInitialLocalTracksAndConnect(roomName) { JitsiMeetJS.mediaDevices.addEventListener( JitsiMeetJS.events.mediaDevices.PERMISSION_PROMPT_IS_SHOWN, - browser => APP.UI.showUserMediaPermissionsGuidanceOverlay(browser)); + browser => + APP.store.dispatch( + mediaPermissionPromptVisibilityChanged(true, browser)) + ); // First try to retrieve both audio and video. let tryCreateLocalTracks = createLocalTracks( @@ -109,8 +118,7 @@ function createInitialLocalTracksAndConnect(roomName) { return Promise.all([ tryCreateLocalTracks, connect(roomName) ]) .then(([tracks, con]) => { - APP.UI.hideUserMediaPermissionsGuidanceOverlay(); - + APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false)); if (audioAndVideoError) { if (audioOnlyError) { // If both requests for 'audio' + 'video' and 'audio' only @@ -334,6 +342,7 @@ class ConferenceConnector { this._reject(err); } _onConferenceFailed(err, ...params) { + APP.store.dispatch(conferenceFailed(room, err, ...params)); logger.error('CONFERENCE FAILED:', err, ...params); APP.UI.hideRingOverLay(); switch (err) { @@ -408,8 +417,6 @@ class ConferenceConnector { // the app. Both the errors above are unrecoverable from the library // perspective. room.leave().then(() => connection.disconnect()); - APP.UI.showPageReloadOverlay( - false /* not a network type of failure */, err); break; case ConferenceErrors.CONFERENCE_MAX_USERS: @@ -466,6 +473,26 @@ function disconnect() { return Promise.resolve(); } +/** + * Handles CONNECTION_FAILED events from lib-jitsi-meet. + * @param {JitsiMeetJS.connection.error} error the error reported. + * @returns {void} + * @private + */ +function _connectionFailedHandler (error) { + switch (error) { + case ConnectionErrors.CONNECTION_DROPPED_ERROR: + case ConnectionErrors.OTHER_ERROR: + case ConnectionErrors.SERVER_ERROR: { + APP.connection.removeEventListener( ConnectionEvents.CONNECTION_FAILED, + _connectionFailedHandler); + if (room) + room.leave(); + break; + } + } +} + export default { isModerator: false, audioMuted: false, @@ -518,11 +545,13 @@ export default { return createInitialLocalTracksAndConnect(options.roomName); }).then(([tracks, con]) => { logger.log('initialized with %s local tracks', tracks.length); + con.addEventListener( + ConnectionEvents.CONNECTION_FAILED, + _connectionFailedHandler); APP.connection = connection = con; this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); APP.remoteControl.init(); - this._bindConnectionFailedHandler(con); this._createRoom(tracks); if (UIUtil.isButtonEnabled('contacts') @@ -561,47 +590,6 @@ export default { isLocalId (id) { return this.getMyUserId() === id; }, - /** - * Binds a handler that will handle the case when the connection is dropped - * in the middle of the conference. - * @param {JitsiConnection} connection the connection to which the handler - * will be bound to. - * @private - */ - _bindConnectionFailedHandler (connection) { - const handler = function (error, errMsg) { - /* eslint-disable no-case-declarations */ - switch (error) { - case ConnectionErrors.CONNECTION_DROPPED_ERROR: - case ConnectionErrors.OTHER_ERROR: - case ConnectionErrors.SERVER_ERROR: - - logger.error("XMPP connection error: " + errMsg); - - // From all of the cases above only CONNECTION_DROPPED_ERROR - // is considered a network type of failure - const isNetworkFailure - = error === ConnectionErrors.CONNECTION_DROPPED_ERROR; - - APP.UI.showPageReloadOverlay( - isNetworkFailure, - "xmpp-conn-dropped:" + errMsg); - - connection.removeEventListener( - ConnectionEvents.CONNECTION_FAILED, handler); - - // FIXME it feels like the conference should be stopped - // by lib-jitsi-meet - if (room) - room.leave(); - - break; - } - /* eslint-enable no-case-declarations */ - }; - connection.addEventListener( - ConnectionEvents.CONNECTION_FAILED, handler); - }, /** * Simulates toolbar button click for audio mute. Used by shortcuts and API. * @param mute true for mute and false for unmute. @@ -1365,6 +1353,7 @@ export default { }); room.on(ConferenceEvents.SUSPEND_DETECTED, () => { + APP.store.dispatch(suspendDetected()); // After wake up, we will be in a state where conference is left // there will be dialog shown to user. // We do not want video/audio as we show an overlay and after it @@ -1385,9 +1374,6 @@ export default { if (localAudio) { localAudio.dispose(); } - - // show overlay - APP.UI.showSuspendedOverlay(); }); room.on(ConferenceEvents.DTMF_SUPPORT_CHANGED, (isDTMFSupported) => { diff --git a/connection.js b/connection.js index 74da7bacb..93181c6bc 100644 --- a/connection.js +++ b/connection.js @@ -4,6 +4,11 @@ const logger = require("jitsi-meet-logger").getLogger(__filename); import AuthHandler from './modules/UI/authentication/AuthHandler'; import jitsiLocalStorage from './modules/util/JitsiLocalStorage'; +import { + connectionEstablished, + connectionFailed +} from './react/features/base/connection'; + const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; @@ -67,6 +72,23 @@ function connect(id, password, roomName) { connection.addEventListener( ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed ); + connection.addEventListener( + ConnectionEvents.CONNECTION_FAILED, connectionFailedHandler); + + function connectionFailedHandler (error, errMsg) { + APP.store.dispatch(connectionFailed(connection, error, errMsg)); + + switch (error) { + case ConnectionErrors.CONNECTION_DROPPED_ERROR: + case ConnectionErrors.OTHER_ERROR: + case ConnectionErrors.SERVER_ERROR: { + connection.removeEventListener( + ConnectionEvents.CONNECTION_FAILED, + connectionFailedHandler); + break; + } + } + } function unsubscribe() { connection.removeEventListener( @@ -80,6 +102,7 @@ function connect(id, password, roomName) { } function handleConnectionEstablished() { + APP.store.dispatch(connectionEstablished(connection)); unsubscribe(); resolve(connection); } diff --git a/css/reload_overlay/_reload_overlay.scss b/css/reload_overlay/_reload_overlay.scss index 22e517891..c0f86f5e9 100644 --- a/css/reload_overlay/_reload_overlay.scss +++ b/css/reload_overlay/_reload_overlay.scss @@ -4,7 +4,7 @@ line-height: 20px; } -.reload_overlay_msg { +.reload_overlay_text { display: block; font-size: 12px; line-height: 30px; @@ -13,4 +13,4 @@ #reloadProgressBar { width: 180px; margin: 5px auto; -} \ No newline at end of file +} diff --git a/modules/UI/UI.js b/modules/UI/UI.js index d83574c47..54d7bbabb 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -15,18 +15,13 @@ import UIEvents from "../../service/UI/UIEvents"; import EtherpadManager from './etherpad/Etherpad'; import SharedVideoManager from './shared_video/SharedVideo'; import Recording from "./recording/Recording"; -import GumPermissionsOverlay - from './gum_overlay/UserMediaPermissionsGuidanceOverlay'; -import * as PageReloadOverlay from './reload_overlay/PageReloadOverlay'; -import SuspendedOverlay from './suspended_overlay/SuspendedOverlay'; import VideoLayout from "./videolayout/VideoLayout"; import FilmStrip from "./videolayout/FilmStrip"; import SettingsMenu from "./side_pannels/settings/SettingsMenu"; import Profile from "./side_pannels/profile/Profile"; import Settings from "./../settings/Settings"; import RingOverlay from "./ring_overlay/RingOverlay"; -import { randomInt } from "../../react/features/base/util/randomUtil"; import UIErrors from './UIErrors'; import { debounce } from "../util/helpers"; @@ -40,6 +35,17 @@ import FollowMe from "../FollowMe"; var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; +/** + * Whether an overlay is visible or not. + * + * FIXME: This is temporary solution. Don't use this variable! + * Should be removed when all the code is move to react. + * + * @type {boolean} + * @public + */ +UI.overlayVisible = false; + let etherpadManager; let sharedVideoManager; @@ -1087,20 +1093,6 @@ UI.notifyFocusDisconnected = function (focus, retrySec) { ); }; -/** - * Notify the user that the video conferencing service is badly broken and - * the page should be reloaded. - * - * @param {boolean} isNetworkFailure true indicates that it's caused by - * network related failure or false when it's the infrastructure. - * @param {string} a label string identifying the reason for the page reload - * which will be included in details of the log event. - */ -UI.showPageReloadOverlay = function (isNetworkFailure, reason) { - // Reload the page after 10 - 30 seconds - PageReloadOverlay.show(10 + randomInt(0, 20), isNetworkFailure, reason); -}; - /** * Updates auth info on the UI. * @param {boolean} isAuthEnabled if authentication is enabled @@ -1414,10 +1406,7 @@ UI.hideRingOverLay = function () { * @returns {*|boolean} {true} if the overlay is visible, {false} otherwise */ UI.isOverlayVisible = function () { - return RingOverlay.isVisible() - || SuspendedOverlay.isVisible() - || PageReloadOverlay.isVisible() - || GumPermissionsOverlay.isVisible(); + return RingOverlay.isVisible() || this.overlayVisible; }; /** @@ -1429,29 +1418,6 @@ UI.isRingOverlayVisible = function () { return RingOverlay.isVisible(); }; -/** - * Shows browser-specific overlay with guidance how to proceed with gUM prompt. - * @param {string} browser - name of browser for which to show the guidance - * overlay. - */ -UI.showUserMediaPermissionsGuidanceOverlay = function (browser) { - GumPermissionsOverlay.show(browser); -}; - -/** - * Shows suspended overlay with a button to rejoin conference. - */ -UI.showSuspendedOverlay = function () { - SuspendedOverlay.show(); -}; - -/** - * Hides browser-specific overlay with guidance how to proceed with gUM prompt. - */ -UI.hideUserMediaPermissionsGuidanceOverlay = function () { - GumPermissionsOverlay.hide(); -}; - /** * Handles user's features changes. */ diff --git a/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js b/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js deleted file mode 100644 index 38caa9dbe..000000000 --- a/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js +++ /dev/null @@ -1,90 +0,0 @@ -/* global interfaceConfig */ - -import Overlay from '../overlay/Overlay'; - -/** - * An overlay with guidance how to proceed with gUM prompt. - */ -class GUMOverlayImpl extends Overlay { - - /** - * Constructs overlay with guidance how to proceed with gUM prompt. - * @param {string} browser - name of browser for which to construct the - * guidance overlay. - * @override - */ - constructor(browser) { - super(); - this.browser = browser; - } - - /** - * @inheritDoc - */ - _buildOverlayContent() { - let textKey = `userMedia.${this.browser}GrantPermissions`; - let titleKey = 'startupoverlay.title'; - let titleOptions = '{ "postProcess": "resolveAppName" }'; - let policyTextKey = 'startupoverlay.policyText'; - let policyLogo = ''; - let policyLogoSrc = interfaceConfig.POLICY_LOGO; - if (policyLogoSrc) { - policyLogo += ( - `` - ); - } - - return ( - `
- - -

- -
-
-

- ${policyLogo} -
` - ); - } -} - -/** - * Stores GUM overlay instance. - * @type {GUMOverlayImpl} - */ -let overlay; - -export default { - /** - * Checks whether the overlay is currently visible. - * @return {boolean} true if the overlay is visible - * or false otherwise. - */ - isVisible () { - return overlay && overlay.isVisible(); - }, - /** - * Shows browser-specific overlay with guidance how to proceed with - * gUM prompt. - * @param {string} browser - name of browser for which to show the - * guidance overlay. - */ - show(browser) { - if (!overlay) { - overlay = new GUMOverlayImpl(browser); - } - overlay.show(); - }, - - /** - * Hides browser-specific overlay with guidance how to proceed with - * gUM prompt. - */ - hide() { - overlay && overlay.hide(); - } -}; diff --git a/modules/UI/overlay/Overlay.js b/modules/UI/overlay/Overlay.js deleted file mode 100644 index f850c2a5f..000000000 --- a/modules/UI/overlay/Overlay.js +++ /dev/null @@ -1,94 +0,0 @@ -/* global $, APP */ - -/** - * Base class for overlay components - the components which are displayed on - * top of the application with semi-transparent background covering the whole - * screen. - */ -export default class Overlay{ - /** - * Creates new Overlay instance. - */ - constructor() { - /** - * - * @type {jQuery} - */ - this.$overlay = null; - - /** - * Indicates if this overlay should use the light look & feel or the - * standard one. - * @type {boolean} - */ - this.isLightOverlay = false; - } - /** - * Template method which should be used by subclasses to provide the overlay - * content. The contents provided by this method are later subject to - * the translation using {@link APP.translation.translateElement}. - * @return {string} HTML representation of the overlay dialog contents. - * @protected - */ - _buildOverlayContent() { - return ''; - } - /** - * Constructs the HTML body of the overlay dialog. - * - * @private - */ - _buildOverlayHtml() { - - let overlayContent = this._buildOverlayContent(); - - let containerClass = this.isLightOverlay ? "overlay__container-light" - : "overlay__container"; - - this.$overlay = $(` -
-
- ${overlayContent} -
-
`); - - APP.translation.translateElement(this.$overlay); - } - /** - * Checks whether the page reload overlay has been displayed. - * @return {boolean} true if the page reload overlay is currently - * visible or false otherwise. - */ - isVisible() { - return this.$overlay && this.$overlay.parents('body').length > 0; - } - /** - * Template method called just after the overlay is displayed for the first - * time. - * @protected - */ - _onShow() { - // To be overridden by subclasses. - } - /** - * Shows the overlay dialog and attaches the underlying HTML representation - * to the DOM. - */ - show() { - - !this.$overlay && this._buildOverlayHtml(); - - if (!this.isVisible()) { - this.$overlay.appendTo('body'); - this._onShow(); - } - } - - /** - * Hides the overlay dialog and detaches it's HTML representation from - * the DOM. - */ - hide() { - this.$overlay && this.$overlay.detach(); - } -} diff --git a/modules/UI/reload_overlay/PageReloadOverlay.js b/modules/UI/reload_overlay/PageReloadOverlay.js deleted file mode 100644 index aec4c5d2b..000000000 --- a/modules/UI/reload_overlay/PageReloadOverlay.js +++ /dev/null @@ -1,175 +0,0 @@ -/* global $, APP, AJS */ -const logger = require("jitsi-meet-logger").getLogger(__filename); - -import Overlay from "../overlay/Overlay"; - -/** - * An overlay dialog which is shown before the conference is reloaded. Shows - * a warning message and counts down towards the reload. - */ -class PageReloadOverlayImpl extends Overlay{ - /** - * Creates new PageReloadOverlayImpl - * @param {number} timeoutSeconds how long the overlay dialog will be - * displayed, before the conference will be reloaded. - * @param {string} title the title of the overlay message - * @param {string} message the message of the overlay - * @param {string} buttonHtml the button html or an empty string if there's - * no button - * @param {boolean} isLightOverlay indicates if the overlay should be a - * light overlay or a standard one - */ - constructor(timeoutSeconds, title, message, buttonHtml, isLightOverlay) { - super(); - /** - * Conference reload counter in seconds. - * @type {number} - */ - this.timeLeft = timeoutSeconds; - /** - * Conference reload timeout in seconds. - * @type {number} - */ - this.timeout = timeoutSeconds; - - this.title = title; - this.message = message; - this.buttonHtml = buttonHtml; - this.isLightOverlay = isLightOverlay; - } - /** - * Constructs overlay body with the warning message and count down towards - * the conference reload. - * @override - */ - _buildOverlayContent() { - return `
- - -
-
- -
- - -
- ${this.buttonHtml} -
`; - } - - /** - * Updates the progress indicator position and the label with the time left. - */ - updateDisplay() { - - APP.translation.translateElement( - $("#reloadSecRemaining"), { seconds: this.timeLeft }); - - const ratio = (this.timeout - this.timeLeft) / this.timeout; - AJS.progressBars.update("#reloadProgressBar", ratio); - } - - /** - * Starts the reload countdown with the animation. - * @override - */ - _onShow() { - $("#reconnectNow").click(() => { - APP.ConferenceUrl.reload(); - }); - - // Initialize displays - this.updateDisplay(); - - var intervalId = window.setInterval(function() { - - if (this.timeLeft >= 1) { - this.timeLeft -= 1; - } - - this.updateDisplay(); - - if (this.timeLeft === 0) { - window.clearInterval(intervalId); - APP.ConferenceUrl.reload(); - } - }.bind(this), 1000); - - logger.info( - "The conference will be reloaded after " - + this.timeLeft + " seconds."); - } -} - -/** - * Holds the page reload overlay instance. - * - * {@type PageReloadOverlayImpl} - */ -let overlay; - -/** - * Checks whether the page reload overlay has been displayed. - * @return {boolean} true if the page reload overlay is currently - * visible or false otherwise. - */ -export function isVisible() { - return overlay && overlay.isVisible(); -} - -/** - * Shows the page reload overlay which will do the conference reload after - * the given amount of time. - * - * @param {number} timeoutSeconds how many seconds before the conference - * reload will happen. - * @param {boolean} isNetworkFailure true indicates that it's - * caused by network related failure or false when it's - * the infrastructure. - * @param {string} reason a label string identifying the reason for the page - * reload which will be included in details of the log event - */ -export function show(timeoutSeconds, isNetworkFailure, reason) { - let title; - let message; - let buttonHtml; - let isLightOverlay; - - if (isNetworkFailure) { - title = "dialog.conferenceDisconnectTitle"; - message = "dialog.conferenceDisconnectMsg"; - buttonHtml - = ``; - isLightOverlay = true; - } - else { - title = "dialog.conferenceReloadTitle"; - message = "dialog.conferenceReloadMsg"; - buttonHtml = ""; - isLightOverlay = false; - } - - if (!overlay) { - overlay = new PageReloadOverlayImpl(timeoutSeconds, - title, - message, - buttonHtml, - isLightOverlay); - } - // Log the page reload event - if (!this.isVisible()) { - // FIXME (CallStats - issue) this event will not make it to - // the CallStats, because the log queue is not flushed, before - // "fabric terminated" is sent to the backed - APP.conference.logEvent( - 'page.reload', undefined /* value */, reason /* label */); - } - overlay.show(); -} diff --git a/modules/UI/suspended_overlay/SuspendedOverlay.js b/modules/UI/suspended_overlay/SuspendedOverlay.js deleted file mode 100644 index 063d771ad..000000000 --- a/modules/UI/suspended_overlay/SuspendedOverlay.js +++ /dev/null @@ -1,63 +0,0 @@ -/* global $, APP */ - -import Overlay from '../overlay/Overlay'; - -/** - * An overlay dialog which is shown when a suspended event is detected. - */ -class SuspendedOverlayImpl extends Overlay{ - /** - * Creates new SuspendedOverlayImpl - */ - constructor() { - super(); - - $(document).on('click', '#rejoin', () => { - APP.ConferenceUrl.reload(); - }); - } - /** - * Constructs overlay body with the message and a button to rejoin. - * @override - */ - _buildOverlayContent() { - return ( - `
- - -

- -
`); - } -} - -/** - * Holds the page suspended overlay instance. - * - * {@type SuspendedOverlayImpl} - */ -let overlay; - -export default { - /** - * Checks whether the page suspended overlay has been displayed. - * @return {boolean} true if the page suspended overlay is - * currently visible or false otherwise. - */ - isVisible() { - return overlay && overlay.isVisible(); - }, - /** - * Shows the page suspended overlay. - */ - show() { - - if (!overlay) { - overlay = new SuspendedOverlayImpl(); - } - overlay.show(); - } -}; diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index a2d507077..7e1a0c25e 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -1,3 +1,5 @@ +/* global APP */ + import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { compose, createStore } from 'redux'; @@ -300,6 +302,12 @@ export class AbstractApp extends Component { if (typeof store === 'undefined') { store = this._createStore(); + + // This is temporary workaround to be able to dispatch actions from + // non-reactified parts of the code (conference.js for example). + // Don't use in the react code!!! + // FIXME: remove when the reactification is finished! + APP.store = store; } return store; diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index d544183e4..5a0f4a2e1 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -34,7 +34,7 @@ function _addConferenceListeners(conference, dispatch) { conference.on( JitsiConferenceEvents.CONFERENCE_FAILED, - (...args) => dispatch(_conferenceFailed(conference, ...args))); + (...args) => dispatch(conferenceFailed(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_JOINED, (...args) => dispatch(_conferenceJoined(conference, ...args))); @@ -87,8 +87,9 @@ function _addConferenceListeners(conference, dispatch) { * conference: JitsiConference, * error: string * }} + * @public */ -function _conferenceFailed(conference, error) { +export function conferenceFailed(conference, error) { return { type: CONFERENCE_FAILED, conference, diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index eb7c538b8..728b8c34f 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -1,3 +1,4 @@ +/* global APP */ import { CONNECTION_ESTABLISHED } from '../connection'; import { getLocalParticipant, @@ -53,7 +54,11 @@ MiddlewareRegistry.register(store => next => action => { function _connectionEstablished(store, next, action) { const result = next(action); - store.dispatch(createConference()); + // FIXME: workaround for the web version. Currently the creation of the + // conference is handled by /conference.js + if (!APP) { + store.dispatch(createConference()); + } return result; } diff --git a/react/features/base/connection/actions.native.js b/react/features/base/connection/actions.native.js index c6084c3fb..016fce7ac 100644 --- a/react/features/base/connection/actions.native.js +++ b/react/features/base/connection/actions.native.js @@ -43,13 +43,13 @@ export function connect() { connection.addEventListener( JitsiConnectionEvents.CONNECTION_DISCONNECTED, - connectionDisconnected); + _onConnectionDisconnected); connection.addEventListener( JitsiConnectionEvents.CONNECTION_ESTABLISHED, - connectionEstablished); + _onConnectionEstablished); connection.addEventListener( JitsiConnectionEvents.CONNECTION_FAILED, - connectionFailed); + _onConnectionFailed); connection.connect(); @@ -59,11 +59,12 @@ export function connect() { * * @param {string} message - Disconnect reason. * @returns {void} + * @private */ - function connectionDisconnected(message: string) { + function _onConnectionDisconnected(message: string) { connection.removeEventListener( JitsiConnectionEvents.CONNECTION_DISCONNECTED, - connectionDisconnected); + _onConnectionDisconnected); dispatch(_connectionDisconnected(connection, message)); } @@ -72,10 +73,11 @@ export function connect() { * Resolves external promise when connection is established. * * @returns {void} + * @private */ - function connectionEstablished() { + function _onConnectionEstablished() { unsubscribe(); - dispatch(_connectionEstablished(connection)); + dispatch(connectionEstablished(connection)); } /** @@ -83,11 +85,12 @@ export function connect() { * * @param {JitsiConnectionErrors} err - Connection error. * @returns {void} + * @private */ - function connectionFailed(err) { + function _onConnectionFailed(err) { unsubscribe(); console.error('CONNECTION FAILED:', err); - dispatch(_connectionFailed(connection, err)); + dispatch(connectionFailed(connection, err, '')); } /** @@ -99,10 +102,10 @@ export function connect() { function unsubscribe() { connection.removeEventListener( JitsiConnectionEvents.CONNECTION_ESTABLISHED, - connectionEstablished); + _onConnectionEstablished); connection.removeEventListener( JitsiConnectionEvents.CONNECTION_FAILED, - connectionFailed); + _onConnectionFailed); } }; } @@ -183,13 +186,13 @@ function _connectionDisconnected(connection, message: string) { * * @param {JitsiConnection} connection - The JitsiConnection which was * established. - * @private * @returns {{ * type: CONNECTION_ESTABLISHED, * connection: JitsiConnection * }} + * @public */ -function _connectionEstablished(connection) { +export function connectionEstablished(connection: Object) { return { type: CONNECTION_ESTABLISHED, connection @@ -200,18 +203,22 @@ function _connectionEstablished(connection) { * Create an action for when the signaling connection could not be created. * * @param {JitsiConnection} connection - The JitsiConnection which failed. - * @param {string} error - Error message. - * @private + * @param {string} error - Error. + * @param {string} errorMessage - Error message. * @returns {{ * type: CONNECTION_FAILED, * connection: JitsiConnection, - * error: string + * error: string, + * errorMessage: string * }} + * @public */ -function _connectionFailed(connection, error: string) { +export function connectionFailed( + connection: Object, error: string, errorMessage: string) { return { type: CONNECTION_FAILED, connection, - error + error, + errorMessage }; } diff --git a/react/features/base/connection/actions.web.js b/react/features/base/connection/actions.web.js index 5d0cef83c..d2b9d12dd 100644 --- a/react/features/base/connection/actions.web.js +++ b/react/features/base/connection/actions.web.js @@ -12,6 +12,11 @@ declare var JitsiMeetJS: Object; const JitsiConferenceEvents = JitsiMeetJS.events.conference; const logger = require('jitsi-meet-logger').getLogger(__filename); +export { + connectionEstablished, + connectionFailed +} from './actions.native.js'; + /** * Opens new connection. * diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index c69ca7b0c..64d58019e 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -7,6 +7,8 @@ import { connect, disconnect } from '../../base/connection'; import { Watermarks } from '../../base/react'; import { FeedbackButton } from '../../feedback'; +import { OverlayContainer } from '../../overlay'; + /** * For legacy reasons, inline style for display none. * @@ -162,6 +164,7 @@ class Conference extends Component { + ); } diff --git a/react/features/overlay/actionTypes.js b/react/features/overlay/actionTypes.js new file mode 100644 index 000000000..0060140a8 --- /dev/null +++ b/react/features/overlay/actionTypes.js @@ -0,0 +1,25 @@ +import { Symbol } from '../base/react'; + +/** + * The type of the Redux action which signals that a suspend was detected. + * + * { + * type: SUSPEND_DETECTED + * } + * @public + */ +export const SUSPEND_DETECTED = Symbol('SUSPEND_DETECTED'); + +/** + * The type of the Redux action which signals that the prompt for media + * permission is visible or not. + * + * { + * type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, + * isVisible: {boolean}, + * browser: {string} + * } + * @public + */ +export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED + = Symbol('MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED'); diff --git a/react/features/overlay/actions.js b/react/features/overlay/actions.js new file mode 100644 index 000000000..742f63e06 --- /dev/null +++ b/react/features/overlay/actions.js @@ -0,0 +1,40 @@ +import { + SUSPEND_DETECTED, + MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED +} from './actionTypes'; +import './reducer'; + +/** + * Signals that suspend was detected. + * + * @returns {{ + * type: SUSPEND_DETECTED + * }} + * @public + */ +export function suspendDetected() { + return { + type: SUSPEND_DETECTED + }; +} + +/** + * Signals that the prompt for media permission is visible or not. + * + * @param {boolean} isVisible - If the value is true - the prompt for media + * permission is visible otherwise the value is false/undefined. + * @param {string} browser - The name of the current browser. + * @returns {{ + * type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, + * isVisible: {boolean}, + * browser: {string} + * }} + * @public + */ +export function mediaPermissionPromptVisibilityChanged(isVisible, browser) { + return { + type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, + isVisible, + browser + }; +} diff --git a/react/features/overlay/components/AbstractOverlay.js b/react/features/overlay/components/AbstractOverlay.js new file mode 100644 index 000000000..44754ff01 --- /dev/null +++ b/react/features/overlay/components/AbstractOverlay.js @@ -0,0 +1,86 @@ +/* global $, APP */ + +import React, { Component } from 'react'; + +/** + * Implements an abstract React Component for overlay - the components which + * are displayed on top of the application covering the whole screen. + * + * @abstract + */ +export default class AbstractOverlay extends Component { + /** + * Initializes a new AbstractOverlay instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + * @public + */ + constructor(props) { + super(props); + + this.state = { + /** + * Indicates the css style of the overlay. if true - lighter and + * darker otherwise. + * @type {boolean} + */ + isLightOverlay: false + }; + } + + /** + * Abstract method which should be used by subclasses to provide the overlay + * content. + * + * @returns {ReactElement|null} + * @protected + */ + _renderOverlayContent() { + return null; + } + + /** + * This method is executed when comonent is mounted. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + // XXX Temporary solution until we add React translation. + APP.translation.translateElement($('#overlay')); + } + + /** + * Reloads the page. + * + * @returns {void} + * @protected + */ + _reconnectNow() { + // FIXME: In future we should dispatch an action here that will result + // in reload. + APP.ConferenceUrl.reload(); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement|null} + */ + render() { + const containerClass = this.state.isLightOverlay + ? 'overlay__container-light' : 'overlay__container'; + + return ( +
+
+ { this._renderOverlayContent() } +
+
+ ); + } +} diff --git a/react/features/overlay/components/OverlayContainer.js b/react/features/overlay/components/OverlayContainer.js new file mode 100644 index 000000000..8f993335a --- /dev/null +++ b/react/features/overlay/components/OverlayContainer.js @@ -0,0 +1,213 @@ +/* global APP */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import PageReloadOverlay from './PageReloadOverlay'; +import SuspendedOverlay from './SuspendedOverlay'; +import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay'; + +/** + * Implements a React Component that will display the correct overlay when + * needed. + */ +class OverlayContainer extends Component { + /** + * OverlayContainer component's property types. + * + * @static + */ + static propTypes = { + /** + * The browser which is used currently. + * NOTE: Used by UserMediaPermissionsOverlay only. + * @private + * @type {string} + */ + _browser: React.PropTypes.string, + + /** + * The indicator which determines whether the status of + * JitsiConnection object has been "established" or not. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _connectionEstablished: React.PropTypes.bool, + + /** + * The indicator which determines whether a critical error for reload + * has been received. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _haveToReload: React.PropTypes.bool, + + /** + * The indicator which determines whether the reload was caused by + * network failure. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _isNetworkFailure: React.PropTypes.bool, + + /** + * The indicator which determines whether the GUM permissions prompt + * is displayed or not. + * NOTE: Used by UserMediaPermissionsOverlay only. + * @private + * @type {boolean} + */ + _mediaPermissionPromptVisible: React.PropTypes.bool, + + /** + * The reason for the error that will cause the reload. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {string} + */ + _reason: React.PropTypes.string, + + /** + * The indicator which determines whether the GUM permissions prompt + * is displayed or not. + * NOTE: Used by SuspendedOverlay only. + * @private + * @type {string} + */ + _suspendDetected: React.PropTypes.bool + } + + /** + * React Component method that executes once component is updated. + * + * @inheritdoc + * @returns {void} + * @protected + */ + componentDidUpdate() { + // FIXME: Temporary workaround until everything is moved to react. + APP.UI.overlayVisible + = (this.props._connectionEstablished && this.props._haveToReload) + || this.props._suspendDetected + || this.props._mediaPermissionPromptVisible; + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement|null} + * @public + */ + render() { + if (this.props._connectionEstablished && this.props._haveToReload) { + return ( + + ); + } + + if (this.props._suspendDetected) { + return ( + + ); + } + + if (this.props._mediaPermissionPromptVisible) { + return ( + + ); + } + + return null; + } +} + +/** + * Maps (parts of) the Redux state to the associated OverlayContainer's props. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _browser: string, + * _connectionEstablished: bool, + * _haveToReload: bool, + * _isNetworkFailure: bool, + * _mediaPermissionPromptVisible: bool, + * _reason: string, + * _suspendDetected: bool + * }} + * @private + */ +function _mapStateToProps(state) { + return { + /** + * The browser which is used currently. + * NOTE: Used by UserMediaPermissionsOverlay only. + * @private + * @type {string} + */ + _browser: state['features/overlay'].browser, + + /** + * The indicator which determines whether the status of + * JitsiConnection object has been "established" or not. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _connectionEstablished: + state['features/overlay'].connectionEstablished, + + /** + * The indicator which determines whether a critical error for reload + * has been received. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _haveToReload: state['features/overlay'].haveToReload, + + /** + * The indicator which determines whether the reload was caused by + * network failure. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {boolean} + */ + _isNetworkFailure: state['features/overlay'].isNetworkFailure, + + /** + * The indicator which determines whether the GUM permissions prompt + * is displayed or not. + * NOTE: Used by UserMediaPermissionsOverlay only. + * @private + * @type {boolean} + */ + _mediaPermissionPromptVisible: + state['features/overlay'].mediaPermissionPromptVisible, + + /** + * The reason for the error that will cause the reload. + * NOTE: Used by PageReloadOverlay only. + * @private + * @type {string} + */ + _reason: state['features/overlay'].reason, + + /** + * The indicator which determines whether the GUM permissions prompt + * is displayed or not. + * NOTE: Used by SuspendedOverlay only. + * @private + * @type {string} + */ + _suspendDetected: state['features/overlay'].suspendDetected + }; +} + +export default connect(_mapStateToProps)(OverlayContainer); diff --git a/react/features/overlay/components/PageReloadOverlay.js b/react/features/overlay/components/PageReloadOverlay.js new file mode 100644 index 000000000..5155c22dc --- /dev/null +++ b/react/features/overlay/components/PageReloadOverlay.js @@ -0,0 +1,298 @@ +/* global APP, AJS */ + +import React, { Component } from 'react'; + +import { randomInt } from '../../base/util/randomUtil'; + +import AbstractOverlay from './AbstractOverlay'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); + +/** + * Implements a React Component for the reload timer. Starts counter from + * props.start, adds props.step to the current value on every props.interval + * seconds until the current value reaches props.end. Also displays progress + * bar. + */ +class ReloadTimer extends Component { + /** + * ReloadTimer component's property types. + * + * @static + */ + static propTypes = { + /** + * The end of the timer. When this.state.current reaches this + * value the timer will stop and call onFinish function. + * @public + * @type {number} + */ + end: React.PropTypes.number, + + /** + * The interval in sec for adding this.state.step to this.state.current + * @public + * @type {number} + */ + interval: React.PropTypes.number, + + /** + * The function that will be executed when timer finish (when + * this.state.current === this.props.end) + */ + onFinish: React.PropTypes.func, + + /** + * The start of the timer. The initial value for this.state.current. + * @public + * @type {number} + */ + start: React.PropTypes.number, + + /** + * The value which will be added to this.state.current on every step. + * @public + * @type {number} + */ + step: React.PropTypes.number + } + + /** + * Initializes a new ReloadTimer instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + * @public + */ + constructor(props) { + super(props); + this.state = { + current: this.props.start, + time: Math.abs(this.props.end - this.props.start) + }; + } + + /** + * React Component method that executes once component is mounted. + * + * @inheritdoc + * @returns {void} + * @protected + */ + componentDidMount() { + AJS.progressBars.update('#reloadProgressBar', 0); + const intervalId = setInterval(() => { + if (this.state.current === this.props.end) { + clearInterval(intervalId); + this.props.onFinish(); + + return; + } + this.setState((prevState, props) => { + return { current: prevState.current + props.step }; + }); + }, Math.abs(this.props.interval) * 1000); + } + + /** + * React Component method that executes once component is updated. + * + * @inheritdoc + * @returns {void} + * @protected + */ + componentDidUpdate() { + AJS.progressBars.update('#reloadProgressBar', + Math.abs(this.state.current - this.props.start) / this.state.time); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement|null} + * @public + */ + render() { + return ( +
+
+ +
+ + { this.state.current } + + +
+ ); + } +} + +/** + * Implements a React Component for page reload overlay. Shown before + * the conference is reloaded. Shows a warning message and counts down towards + * the reload. + */ +export default class PageReloadOverlay extends AbstractOverlay { + /** + * PageReloadOverlay component's property types. + * + * @static + */ + static propTypes = { + /** + * The indicator which determines whether the reload was caused by + * network failure. + * @public + * @type {boolean} + */ + isNetworkFailure: React.PropTypes.bool, + + /** + * The reason for the error that will cause the reload. + * NOTE: Used by PageReloadOverlay only. + * @public + * @type {string} + */ + reason: React.PropTypes.string + } + + /** + * Initializes a new PageReloadOverlay instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + * @public + */ + constructor(props) { + super(props); + + /** + * How long the overlay dialog will be + * displayed, before the conference will be reloaded. + * @type {number} + */ + const timeoutSeconds = 10 + randomInt(0, 20); + + let isLightOverlay, message, title; + + if (this.props.isNetworkFailure) { + title = 'dialog.conferenceDisconnectTitle'; + message = 'dialog.conferenceDisconnectMsg'; + isLightOverlay = true; + } else { + title = 'dialog.conferenceReloadTitle'; + message = 'dialog.conferenceReloadMsg'; + isLightOverlay = false; + } + + this.state = { + ...this.state, + + /** + * Indicates the css style of the overlay. if true - lighter and + * darker otherwise. + * @type {boolean} + */ + isLightOverlay, + + /** + * The translation key for the title of the overlay + * @type {string} + */ + message, + + /** + * How long the overlay dialog will be + * displayed, before the conference will be reloaded. + * @type {number} + */ + timeoutSeconds, + + /** + * The translation key for the title of the overlay + * @type {string} + */ + title + }; + } + + /** + * Renders the button for relaod the page if necessary. + * + * @returns {ReactElement|null} + * @private + */ + _renderButton() { + if (this.props.isNetworkFailure) { + const cName = 'button-control button-control_primary ' + + 'button-control_center'; + + /* eslint-disable react/jsx-handler-names */ + + return ( +