ref(overlay): The overlays to use React

This commit is contained in:
hristoterezov 2017-01-31 14:58:48 -06:00 committed by Lyubomir Marinov
parent f3269070b2
commit 92d0589a37
24 changed files with 1068 additions and 541 deletions

View File

@ -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) => {

View File

@ -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);
}

View File

@ -4,7 +4,7 @@
line-height: 20px;
}
.reload_overlay_msg {
.reload_overlay_text {
display: block;
font-size: 12px;
line-height: 30px;

View File

@ -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 <tt>true</tt> indicates that it's caused by
* network related failure or <tt>false</tt> 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.
*/

View File

@ -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 += (
`<div class="policy__logo">
<img src="${policyLogoSrc}"/>
</div>`
);
}
return (
`<div class="inlay">
<span class="inlay__icon icon-microphone"></span>
<span class="inlay__icon icon-camera"></span>
<h3 class="inlay__title" data-i18n="${titleKey}"
data-i18n-options='${titleOptions}'></h3>
<span class='inlay__text'data-i18n='[html]${textKey}'></span>
</div>
<div class="policy overlay__policy">
<p class="policy__text" data-i18n="[html]${policyTextKey}"></p>
${policyLogo}
</div>`
);
}
}
/**
* Stores GUM overlay instance.
* @type {GUMOverlayImpl}
*/
let overlay;
export default {
/**
* Checks whether the overlay is currently visible.
* @return {boolean} <tt>true</tt> if the overlay is visible
* or <tt>false</tt> 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();
}
};

View File

@ -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 <tt>Overlay</tt> 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 = $(`
<div class=${containerClass}>
<div class='overlay__content'>
${overlayContent}
</div>
</div>`);
APP.translation.translateElement(this.$overlay);
}
/**
* Checks whether the page reload overlay has been displayed.
* @return {boolean} <tt>true</tt> if the page reload overlay is currently
* visible or <tt>false</tt> 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();
}
}

View File

@ -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 <tt>PageReloadOverlayImpl</tt>
* @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 `<div class="inlay">
<span data-i18n=${this.title}
class='reload_overlay_title'></span>
<span data-i18n=${this.message}
class='reload_overlay_msg'></span>
<div>
<div id='reloadProgressBar'
class="aui-progress-indicator">
<span class="aui-progress-indicator-value"></span>
</div>
<span id='reloadSecRemaining'
data-i18n="dialog.conferenceReloadTimeLeft"
class='reload_overlay_msg'>
</span>
</div>
${this.buttonHtml}
</div>`;
}
/**
* 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} <tt>true</tt> if the page reload overlay is currently
* visible or <tt>false</tt> 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 <tt>true</tt> indicates that it's
* caused by network related failure or <tt>false</tt> 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
= `<button id="reconnectNow" data-i18n="dialog.reconnectNow"
class="button-control button-control_primary
button-control_center"></button>`;
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();
}

View File

@ -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 <tt>SuspendedOverlayImpl</tt>
*/
constructor() {
super();
$(document).on('click', '#rejoin', () => {
APP.ConferenceUrl.reload();
});
}
/**
* Constructs overlay body with the message and a button to rejoin.
* @override
*/
_buildOverlayContent() {
return (
`<div class="inlay">
<span class="inlay__icon icon-microphone"></span>
<span class="inlay__icon icon-camera"></span>
<h3 class="inlay__title" data-i18n="suspendedoverlay.title"></h3>
<button id="rejoin"
data-i18n="suspendedoverlay.rejoinKeyTitle"
class="inlay__button button-control button-control_primary">
</button>
</div>`);
}
}
/**
* Holds the page suspended overlay instance.
*
* {@type SuspendedOverlayImpl}
*/
let overlay;
export default {
/**
* Checks whether the page suspended overlay has been displayed.
* @return {boolean} <tt>true</tt> if the page suspended overlay is
* currently visible or <tt>false</tt> otherwise.
*/
isVisible() {
return overlay && overlay.isVisible();
},
/**
* Shows the page suspended overlay.
*/
show() {
if (!overlay) {
overlay = new SuspendedOverlayImpl();
}
overlay.show();
}
};

View File

@ -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;

View File

@ -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,

View File

@ -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);
// FIXME: workaround for the web version. Currently the creation of the
// conference is handled by /conference.js
if (!APP) {
store.dispatch(createConference());
}
return result;
}

View File

@ -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
};
}

View File

@ -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.
*

View File

@ -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 {
</div>
</div>
</div>
<OverlayContainer />
</div>
);
}

View File

@ -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');

View File

@ -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
};
}

View File

@ -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 (
<div
className = { containerClass }
id = 'overlay'>
<div className = 'overlay__content'>
{ this._renderOverlayContent() }
</div>
</div>
);
}
}

View File

@ -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 (
<PageReloadOverlay
isNetworkFailure = { this.props._isNetworkFailure }
reason = { this.props._reason } />
);
}
if (this.props._suspendDetected) {
return (
<SuspendedOverlay />
);
}
if (this.props._mediaPermissionPromptVisible) {
return (
<UserMediaPermissionsOverlay
browser = { this.props._browser } />
);
}
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);

View File

@ -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 (
<div>
<div
className = 'aui-progress-indicator'
id = 'reloadProgressBar'>
<span className = 'aui-progress-indicator-value' />
</div>
<span
className = 'reload_overlay_text'
id = 'reloadSeconds'>
{ this.state.current }
<span data-i18n = 'dialog.conferenceReloadTimeLeft' />
</span>
</div>
);
}
}
/**
* 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 (
<button
className = { cName }
data-i18n = 'dialog.reconnectNow'
id = 'reconnectNow'
onClick = { this._reconnectNow } />
);
}
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 (
<div className = 'inlay'>
<span
className = 'reload_overlay_title'
data-i18n = { this.state.title } />
<span
className = 'reload_overlay_text'
data-i18n = { this.state.message } />
<ReloadTimer
end = { 0 }
interval = { 1 }
onFinish = { this._reconnectNow }
start = { this.state.timeoutSeconds }
step = { -1 } />
{ this._renderButton() }
</div>
);
}
/**
* 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.`);
}
}

View File

@ -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 (
<div className = 'inlay'>
<span className = 'inlay__icon icon-microphone' />
<span className = 'inlay__icon icon-camera' />
<h3
className = 'inlay__title'
data-i18n = 'suspendedoverlay.title' />
<button
className = { btnClass }
data-i18n = 'suspendedoverlay.rejoinKeyTitle'
id = 'rejoin'
onClick = { this._reconnectNow } />
</div>
);
}
}

View File

@ -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 (
<div>
<div className = 'inlay'>
<span className = 'inlay__icon icon-microphone' />
<span className = 'inlay__icon icon-camera' />
<h3
className = 'inlay__title'
data-i18n = 'startupoverlay.title'
data-i18n-options
= '{"postProcess": "resolveAppName"}' />
<span
className = 'inlay__text'
data-i18n = { `[html]${textKey}` } />
</div>
<div className = 'policy overlay__policy'>
<p
className = 'policy__text'
data-i18n = '[html]startupoverlay.policyText' />
{ this._renderPolicyLogo() }
</div>
</div>
);
}
/**
* Renders the policy logo.
*
* @returns {ReactElement|null}
* @private
*/
_renderPolicyLogo() {
if (this.state.policyLogoSrc) {
return (
<div className = 'policy__logo'>
<img src = { this.state.policyLogoSrc } />
</div>
);
}
return null;
}
}

View File

@ -0,0 +1 @@
export { default as OverlayContainer } from './OverlayContainer';

View File

@ -0,0 +1,2 @@
export * from './components';
export * from './actions';

View File

@ -0,0 +1,145 @@
/* global JitsiMeetJS */
import { CONFERENCE_FAILED } from '../base/conference';
import {
CONNECTION_ESTABLISHED,
CONNECTION_FAILED
} from '../base/connection';
import {
ReducerRegistry,
setStateProperty,
setStateProperties
} from '../base/redux';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SUSPEND_DETECTED
} from './actionTypes';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Reduces the Redux actions of the feature overlay.
*/
ReducerRegistry.register('features/overlay', (state = {}, action) => {
switch (action.type) {
case CONFERENCE_FAILED:
return _conferenceFailed(state, action);
case CONNECTION_ESTABLISHED:
return _connectionEstablished(state, action);
case CONNECTION_FAILED:
return _connectionFailed(state, action);
case MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED:
return _mediaPermissionPromptVisibilityChanged(state, action);
case SUSPEND_DETECTED:
return _suspendDetected(state, action);
}
return state;
});
/**
* Reduces a specific Redux action CONFERENCE_FAILED of the feature
* overlay.
*
* @param {Object} state - The Redux state of the feature overlay.
* @param {Action} action - The Redux action CONFERENCE_FAILED to reduce.
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
* @private
*/
function _conferenceFailed(state, action) {
const ConferenceErrors = JitsiMeetJS.errors.conference;
if (action.error === ConferenceErrors.FOCUS_LEFT
|| action.error === ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE) {
return setStateProperties(state, {
haveToReload: true,
isNetworkFailure: false,
reason: action.errorMessage
});
}
return state;
}
/**
* Reduces a specific Redux action CONNECTION_ESTABLISHED of the feature
* overlay.
*
* @param {Object} state - The Redux state of the feature overlay.
* @returns {Object} The new state of the feature overlay after the
* reduction of the specified action.
* @private
*/
function _connectionEstablished(state) {
return setStateProperty(state, 'connectionEstablished', true);
}
/**
* Reduces a specific Redux action CONNECTION_FAILED of the feature
* overlay.
*
* @param {Object} state - The Redux state of the feature overlay.
* @param {Action} action - The Redux action CONNECTION_FAILED to reduce.
* @returns {Object} The new state of the feature overlay after the
* reduction of the specified action.
* @private
*/
function _connectionFailed(state, action) {
const ConnectionErrors = JitsiMeetJS.errors.connection;
switch (action.error) {
case ConnectionErrors.CONNECTION_DROPPED_ERROR:
case ConnectionErrors.OTHER_ERROR:
case ConnectionErrors.SERVER_ERROR: {
logger.error(`XMPP connection error: ${action.errorMessage}`);
// From all of the cases above only CONNECTION_DROPPED_ERROR
// is considered a network type of failure
return setStateProperties(state, {
haveToReload: true,
isNetworkFailure:
action.error === ConnectionErrors.CONNECTION_DROPPED_ERROR,
reason: `xmpp-conn-dropped: ${action.errorMessage}`
});
}
}
return state;
}
/**
* Reduces a specific Redux action MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
* of the feature overlay.
*
* @param {Object} state - The Redux state of the feature overlay.
* @param {Action} action - The Redux action to reduce.
* @returns {Object} The new state of the feature overlay after the
* reduction of the specified action.
* @private
*/
function _mediaPermissionPromptVisibilityChanged(state, action) {
return setStateProperties(state, {
mediaPermissionPromptVisible: action.isVisible,
browser: action.browser
});
}
/**
* Reduces a specific Redux action SUSPEND_DETECTED of the feature
* overlay.
*
* @param {Object} state - The Redux state of the feature overlay.
* @returns {Object} The new state of the feature overlay after the
* reduction of the specified action.
* @private
*/
function _suspendDetected(state) {
return setStateProperty(state, 'suspendDetected', true);
}