diff --git a/app.js b/app.js index 49b4b1059..2b6e21982 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,7 @@ import RoomnameGenerator from './modules/util/RoomnameGenerator'; import UI from "./modules/UI/UI"; import settings from "./modules/settings/Settings"; import conference from './conference'; +import ConferenceUrl from './modules/URL/ConferenceUrl'; import API from './modules/API/API'; import UIEvents from './service/UI/UIEvents'; @@ -47,6 +48,18 @@ function pushHistoryState(roomName, URL) { return null; } +/** + * Replaces current history state(replaces the URL displayed by the browser). + * @param {string} newUrl the URL string which is to be displayed by the browser + * to the user. + */ +function replaceHistoryState (newUrl) { + if (window.history + && typeof window.history.replaceState === 'function') { + window.history.replaceState({}, document.title, newUrl); + } +} + /** * Builds and returns the room name. */ @@ -82,6 +95,12 @@ const APP = { UI, settings, conference, + /** + * After the APP has been initialized provides utility methods for dealing + * with the conference room URL(address). + * @type ConferenceUrl + */ + ConferenceUrl : null, connection: null, API, init () { @@ -107,6 +126,10 @@ function setTokenData() { function init() { setTokenData(); + // Initialize the conference URL handler + APP.ConferenceUrl = new ConferenceUrl(window.location); + // Clean up the URL displayed by the browser + replaceHistoryState(APP.ConferenceUrl.getInviteUrl()); var isUIReady = APP.UI.start(); if (isUIReady) { APP.conference.init({roomName: buildRoomName()}).then(function () { diff --git a/conference.js b/conference.js index f617d16cb..620287dc5 100644 --- a/conference.js +++ b/conference.js @@ -329,10 +329,6 @@ class ConferenceConnector { } break; - case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE: - APP.UI.notifyBridgeDown(); - break; - // not enough rights to create conference case ConferenceErrors.AUTHENTICATION_REQUIRED: // schedule reconnect to check if someone else created the room @@ -367,6 +363,10 @@ class ConferenceConnector { } break; + // FIXME FOCUS_DISCONNECTED is confusing event name. + // What really happens there is that the library is not ready yet, + // because Jicofo is not available, but it is going to give + // it another try. case ConferenceErrors.FOCUS_DISCONNECTED: { let [focus, retrySec] = params; @@ -375,8 +375,17 @@ class ConferenceConnector { break; case ConferenceErrors.FOCUS_LEFT: + case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE: + // Log the page reload event + // 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'); + // FIXME the conference should be stopped by the library and not by + // the app. Both the errors above are unrecoverable from the library + // perspective. room.leave().then(() => connection.disconnect()); - APP.UI.notifyFocusLeft(); + APP.UI.showPageReloadOverlay(); break; case ConferenceErrors.CONFERENCE_MAX_USERS: diff --git a/css/_variables.scss b/css/_variables.scss index 0c8a54d33..304ae8f85 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -84,8 +84,9 @@ $sidebarWidth: 200px; */ $tooltipsZ: 901; $toolbarZ: 900; -$overlayZ: 800; +$overlayZ: 902; $notificationZ: 1012; +$ringingZ: 800; /** * Font Colors TODO: Change colors when general dialogs are implemented. diff --git a/css/main.scss b/css/main.scss index c65ff91d9..7fb71d2df 100644 --- a/css/main.scss +++ b/css/main.scss @@ -38,6 +38,7 @@ @import 'toastr'; @import 'base'; @import 'overlay/overlay'; +@import 'reload_overlay/reload_overlay'; @import 'modals/dialog'; @import 'modals/feedback/feedback'; @import 'videolayout_default'; diff --git a/css/overlay/_overlay.scss b/css/overlay/_overlay.scss index f440b5ab2..3152c460f 100644 --- a/css/overlay/_overlay.scss +++ b/css/overlay/_overlay.scss @@ -1,48 +1,30 @@ -.overlay { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: $overlayZ; - background: #21B9FC; /* Old browsers */ - opacity: 0.75; - display: block; -} - -.overlay_transparent { - background: rgba(22, 185, 252, .9); -} - .overlay_container { + top: 0; + left: 0; width: 100%; height: 100%; position: fixed; z-index: $overlayZ; + background: rgba(22, 185, 252, .9); } .overlay_content { color: #fff; - font-weight: normal; - font-size: 20px; text-align: center; width: 400px; height: 250px; top: 50%; left: 50%; - position:absolute; + position: absolute; margin-top: -125px; margin-left: -200px; } - .overlay_text_small { + display: block; font-size: 18px; } .overlay_icon { - position: relative; - z-index: 1013; - float: none; font-size: 100px; } diff --git a/css/reload_overlay/_reload_overlay.scss b/css/reload_overlay/_reload_overlay.scss new file mode 100644 index 000000000..15c8fdbd4 --- /dev/null +++ b/css/reload_overlay/_reload_overlay.scss @@ -0,0 +1,17 @@ +.reload_overlay_title { + display: block; + font-size: 16px; + line-height: 20px; +} + +.reload_overlay_msg { + display: block; + font-size: 12px; + line-height: 30px; +} + +#reloadProgressBar { + width: 180px; + margin: 5px auto; +} + diff --git a/css/ringing/_ringing.scss b/css/ringing/_ringing.scss index b973bd41a..b04093a78 100644 --- a/css/ringing/_ringing.scss +++ b/css/ringing/_ringing.scss @@ -5,7 +5,7 @@ width: 100%; height: 100%; position: fixed; - z-index: $overlayZ; + z-index: $ringingZ; background: linear-gradient(transparent, #000); opacity: 0.8; diff --git a/lang/main.json b/lang/main.json index 0c6d1ac73..86de9cc2d 100644 --- a/lang/main.json +++ b/lang/main.json @@ -202,8 +202,9 @@ "detectext": "Error when trying to detect desktopsharing extension.", "failtoinstall": "Failed to install desktop sharing extension", "failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.", - "bridgeUnavailable": "Jitsi Videobridge is currently unavailable. Please try again later!", - "jicofoUnavailable": "Jicofo is currently unavailable. Please try again later!", + "conferenceReloadTitle": "Unfortunately, something went wrong", + "conferenceReloadMsg": "We're trying to fix this", + "conferenceReloadTimeLeft": "__seconds__ sec.", "maxUsersLimitReached": "The limit for maximum number of participants in the conference has been reached. The conference is full. Please try again later!", "lockTitle": "Lock failed", "lockMessage": "Failed to lock the conference.", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 2f005197b..64690b411 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -14,12 +14,12 @@ import Recording from "./recording/Recording"; import GumPermissionsOverlay from './gum_overlay/UserMediaPermissionsGuidanceOverlay'; +import PageReloadOverlay from './reload_overlay/PageReloadOverlay'; 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 { reload } from '../util/helpers'; import RingOverlay from "./ring_overlay/RingOverlay"; import UIErrors from './UIErrors'; @@ -195,13 +195,6 @@ UI.notifyConferenceDestroyed = function (reason) { messageHandler.openDialog(title, reason, true, {}, () => false); }; -/** - * Notify user that Jitsi Videobridge is not accessible. - */ - UI.notifyBridgeDown = function () { - messageHandler.showError("dialog.error", "dialog.bridgeUnavailable"); -}; - /** * Show chat error. * @param err the Error @@ -265,19 +258,6 @@ UI.setLocalRaisedHandStatus = (raisedHandStatus) => { */ UI.initConference = function () { let id = APP.conference.getMyUserId(); - - // Do not include query parameters in the invite URL - // "https:" + "//" + "example.com:8888" + "/SomeConference1245" - var inviteURL = window.location.protocol + "//" + - window.location.host + window.location.pathname; - - this.emitEvent(UIEvents.INVITE_URL_INITIALISED, inviteURL); - - // Clean up the URL displayed by the browser - if (window.history && typeof window.history.replaceState === 'function') { - window.history.replaceState({}, document.title, inviteURL); - } - // Add myself to the contact list. UI.ContactList.addContact(id, true); @@ -1119,25 +1099,11 @@ UI.notifyFocusDisconnected = function (focus, retrySec) { }; /** - * Notify user that focus left the conference so page should be reloaded. + * Notify the user that the video conferencing service is badly broken and + * the page should be reloaded. */ -UI.notifyFocusLeft = function () { - let title = APP.translation.generateTranslationHTML( - 'dialog.serviceUnavailable' - ); - let msg = APP.translation.generateTranslationHTML( - 'dialog.jicofoUnavailable' - ); - messageHandler.openDialog( - title, - msg, - true, // persistent - [{title: 'retry'}], - function () { - reload(); - return false; - } - ); +UI.showPageReloadOverlay = function () { + PageReloadOverlay.show(15 /* will reload in 15 seconds */); }; /** @@ -1475,6 +1441,18 @@ UI.hideRingOverLay = function () { FilmStrip.toggleFilmStrip(true); }; +/** + * Indicates if any the "top" overlays are currently visible. The check includes + * the call overlay, GUM permissions overlay and a page reload overlay. + * + * @returns {*|boolean} {true} if the overlay is visible, {false} otherwise + */ +UI.isOverlayVisible = function () { + return RingOverlay.isVisible() + || PageReloadOverlay.isVisible() + || GumPermissionsOverlay.isVisible(); +}; + /** * Indicates if the ring overlay is currently visible. * diff --git a/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js b/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js index db0d977e4..11c9fd2d3 100644 --- a/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js +++ b/modules/UI/gum_overlay/UserMediaPermissionsGuidanceOverlay.js @@ -1,29 +1,50 @@ -/* global $, APP */ +/* global */ -let $overlay; +import Overlay from '../overlay/Overlay'; /** - * Internal function that constructs overlay with guidance how to proceed with - * gUM prompt. - * @param {string} browser - name of browser for which to construct the - * guidance overlay. + * An overlay with guidance how to proceed with gUM prompt. */ -function buildOverlayHtml(browser) { - $overlay = $(` -
-
-
- - - -
-
`); +class GUMOverlayImpl extends Overlay { - APP.translation.translateElement($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() { + return ` + + + `; + } } +/** + * 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. @@ -31,9 +52,10 @@ export default { * guidance overlay. */ show(browser) { - !$overlay && buildOverlayHtml(browser); - - !$overlay.parents('body').length && $overlay.appendTo('body'); + if (!overlay) { + overlay = new GUMOverlayImpl(browser); + } + overlay.show(); }, /** @@ -41,6 +63,6 @@ export default { * gUM prompt. */ hide() { - $overlay && $overlay.detach(); + overlay && overlay.hide(); } }; diff --git a/modules/UI/invite/Invite.js b/modules/UI/invite/Invite.js index 0d5ecdf11..df5774088 100644 --- a/modules/UI/invite/Invite.js +++ b/modules/UI/invite/Invite.js @@ -14,6 +14,7 @@ const ConferenceEvents = JitsiMeetJS.events.conference; class Invite { constructor(conference) { this.conference = conference; + this.inviteUrl = APP.ConferenceUrl.getInviteUrl(); this.createRoomLocker(conference); this.registerListeners(); } @@ -48,11 +49,6 @@ class Invite { APP.UI.addListener( UIEvents.INVITE_CLICKED, () => { this.openLinkDialog(); }); - APP.UI.addListener( UIEvents.INVITE_URL_INITIALISED, - (inviteUrl) => { - this.updateInviteUrl(inviteUrl); - }); - APP.UI.addListener( UIEvents.PASSWORD_REQUIRED, () => { this.setLockedFromElsewhere(true); @@ -172,14 +168,6 @@ class Invite { } } - /** - * Updates the room invite url. - */ - updateInviteUrl (newInviteUrl) { - this.inviteUrl = newInviteUrl; - this.updateView(); - } - /** * Helper method for encoding * Invite URL diff --git a/modules/UI/overlay/Overlay.js b/modules/UI/overlay/Overlay.js new file mode 100644 index 000000000..babdc587a --- /dev/null +++ b/modules/UI/overlay/Overlay.js @@ -0,0 +1,82 @@ +/* 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; + } + /** + * 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. + * @private + */ + _buildOverlayContent() { + return ''; + } + /** + * Constructs the HTML body of the overlay dialog. + */ + buildOverlayHtml() { + + let overlayContent = this._buildOverlayContent(); + + 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. + * @private + */ + _onShow() { + // To be overridden by subclasses. + } + /** + * Shows the overlay dialog adn 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 new file mode 100644 index 000000000..4529ab478 --- /dev/null +++ b/modules/UI/reload_overlay/PageReloadOverlay.js @@ -0,0 +1,122 @@ +/* global $, APP, AJS */ + +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. + */ + constructor(timeoutSeconds) { + super(); + /** + * Conference reload counter in seconds. + * @type {number} + */ + this.timeLeft = timeoutSeconds; + /** + * Conference reload timeout in seconds. + * @type {number} + */ + this.timeout = timeoutSeconds; + } + /** + * Constructs overlay body with the warning message and count down towards + * the conference reload. + * @override + */ + _buildOverlayContent() { + return ` + + +
+
+ +
+ + +
`; + } + + /** + * Updates the progress indicator position and the label with the time left. + */ + updateDisplay() { + + const timeLeftTxt + = APP.translation.translateString( + "dialog.conferenceReloadTimeLeft", + { seconds: this.timeLeft }); + $("#reloadSecRemaining").text(timeLeftTxt); + + const ratio = (this.timeout - this.timeLeft) / this.timeout; + AJS.progressBars.update("#reloadProgressBar", ratio); + } + + /** + * Starts the reload countdown with the animation. + * @override + */ + _onShow() { + + // 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); + + console.info( + "The conference will be reloaded after " + + this.timeLeft + " seconds."); + } +} + +/** + * Holds the page reload overlay instance. + * + * {@type PageReloadOverlayImpl} + */ +let overlay; + +export default { + /** + * 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 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. + */ + show(timeoutSeconds) { + + if (!overlay) { + overlay = new PageReloadOverlayImpl(timeoutSeconds); + } + overlay.show(); + } +}; diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js index c353d898e..05ff316fb 100644 --- a/modules/UI/toolbars/ToolbarToggler.js +++ b/modules/UI/toolbars/ToolbarToggler.js @@ -34,9 +34,10 @@ function hideToolbar(force) { // eslint-disable-line no-unused-vars clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; - if (Toolbar.isHovered() - || APP.UI.isRingOverlayVisible() - || SideContainerToggler.isVisible()) { + if (force !== true && + (Toolbar.isHovered() + || APP.UI.isRingOverlayVisible() + || SideContainerToggler.isVisible())) { toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); } else { Toolbar.hide(); diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index 319a459e3..0a478a599 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -333,7 +333,7 @@ var messageHandler = { messageArguments, options) { // If we're in ringing state we skip all toaster notifications. - if(!notificationsEnabled || APP.UI.isRingOverlayVisible()) + if(!notificationsEnabled || APP.UI.isOverlayVisible()) return; var displayNameSpan = '