diff --git a/conference.js b/conference.js
index f4f79b265f..6584ef310c 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 74da7bacb0..93181c6bcc 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 22e5178919..c0f86f5e9f 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 d83574c473..54d7bbabb3 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 38caa9dbe8..0000000000
--- 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 f850c2a5fe..0000000000
--- 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 aec4c5d2be..0000000000
--- 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 063d771ad6..0000000000
--- 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 a2d5070770..7e1a0c25e6 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 d544183e46..5a0f4a2e15 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 eb7c538b83..728b8c34fb 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 c6084c3fb3..016fce7ac3 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 5d0cef83c1..d2b9d12dd1 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 c69ca7b0ca..64d58019ed 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 0000000000..0060140a81
--- /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 0000000000..742f63e06e
--- /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 0000000000..44754ff010
--- /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 0000000000..8f993335a6
--- /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 0000000000..5155c22dc4
--- /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 (
+
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Constructs overlay body with the warning message and count down towards
+ * the conference reload.
+ *
+ * @returns {ReactElement|null}
+ * @override
+ * @protected
+ */
+ _renderOverlayContent() {
+
+ /* eslint-disable react/jsx-handler-names */
+
+ return (
+
+
+
+
+ { this._renderButton() }
+
+ );
+ }
+
+ /**
+ * This method is executed when comonent is mounted.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ super.componentDidMount();
+
+ // 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
+ // FIXME: We should dispatch action for this
+ APP.conference.logEvent('page.reload', undefined /* value */,
+ this.props.reason /* label */);
+ logger.info(`The conference will be reloaded after
+ ${this.state.timeoutSeconds} seconds.`);
+ }
+}
diff --git a/react/features/overlay/components/SuspendedOverlay.js b/react/features/overlay/components/SuspendedOverlay.js
new file mode 100644
index 0000000000..0e9fb6f92a
--- /dev/null
+++ b/react/features/overlay/components/SuspendedOverlay.js
@@ -0,0 +1,37 @@
+import React from 'react';
+
+import AbstractOverlay from './AbstractOverlay';
+
+/**
+ * Implements a React Component for suspended overlay. Shown when suspended
+ * is detected.
+ */
+export default class SuspendedOverlay extends AbstractOverlay {
+ /**
+ * Constructs overlay body with the message and a button to rejoin.
+ *
+ * @returns {ReactElement|null}
+ * @override
+ * @protected
+ */
+ _renderOverlayContent() {
+ const btnClass = 'inlay__button button-control button-control_primary';
+
+ /* eslint-disable react/jsx-handler-names */
+
+ return (
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/react/features/overlay/components/UserMediaPermissionsOverlay.js b/react/features/overlay/components/UserMediaPermissionsOverlay.js
new file mode 100644
index 0000000000..a4a6f52e94
--- /dev/null
+++ b/react/features/overlay/components/UserMediaPermissionsOverlay.js
@@ -0,0 +1,98 @@
+/* global interfaceConfig */
+
+import React from 'react';
+
+import AbstractOverlay from './AbstractOverlay';
+
+/**
+ * Implements a React Component for overlay with guidance how to proceed with
+ * gUM prompt.
+ */
+export default class UserMediaPermissionsOverlay extends AbstractOverlay {
+ /**
+ * UserMediaPermissionsOverlay component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * The browser which is used currently. The text is different for every
+ * browser.
+ * @public
+ * @type {string}
+ */
+ browser: React.PropTypes.string
+ }
+
+ /**
+ * Initializes a new SuspendedOverlay instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ * @public
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ /**
+ * The src value of the image for the policy logo.
+ * @type {string}
+ */
+ policyLogoSrc: interfaceConfig.POLICY_LOGO
+ };
+ }
+
+ /**
+ * Constructs overlay body with the message with guidance how to proceed
+ * with gUM prompt.
+ *
+ * @returns {ReactElement|null}
+ * @override
+ * @protected
+ */
+ _renderOverlayContent() {
+ const textKey = `userMedia.${this.props.browser}GrantPermissions`;
+
+ return (
+