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 3ee6524d8..3d5c94f2b 100644 --- a/conference.js +++ b/conference.js @@ -325,10 +325,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 @@ -363,6 +359,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; @@ -371,8 +371,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/_recording.scss b/css/_recording.scss index 0f58bb789..aa56227a7 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -1,4 +1,4 @@ .recordingSpinner { display: none; - vertical-align: text-bottom; + vertical-align: top; } \ No newline at end of file diff --git a/css/_toolbars.scss b/css/_toolbars.scss index f8d000e2f..22ac3f049 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -1,5 +1,5 @@ .toolbar { - background-color: rgba(0,0,0,0.5); + background-color: $toolbarBackground; position: relative; z-index: $toolbarZ; height: 100%; diff --git a/css/_variables.scss b/css/_variables.scss index 0c8a54d33..7e0ec2c19 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -27,6 +27,7 @@ $defaultBackground: #474747; $tooltipBg: rgba(0,0,0, 0.7); // Toolbar +$toolbarBackground: rgba(0, 0, 0, 0.5); $toolbarSelectBackground: rgba(0, 0, 0, .6); $toolbarBadgeBackground: #165ECC; $toolbarBadgeColor: #FFFFFF; @@ -46,6 +47,8 @@ $dominantSpeakerBg: #165ecc; $raiseHandBg: #D6D61E; $audioLevelBg: #44A5FF; $audioLevelShadow: rgba(9, 36, 77, 0.9); +$videoStateIndicatorColor: $defaultColor; +$videoStateIndicatorBackground: $toolbarBackground; /** * Feedback Modal @@ -84,8 +87,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. @@ -106,6 +110,6 @@ $linkHoverFontColor: #287ade; /** * Forms */ -$inputBg: #505F79; -$inputBgHover: #505F79; -$inputFontColor: #ECEEF1; \ No newline at end of file +$inputBg: $inputSemiBackground; +$inputBgHover: $inputSemiBackground; +$inputFontColor: $defaultDarkFontColor; \ No newline at end of file diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 71879a7a8..9333db78c 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -511,7 +511,7 @@ display: none; position: absolute; width: auto; - z-index: 1011; + z-index: 2; font-weight: 600; font-size: 14px; text-align: center; @@ -534,7 +534,7 @@ position: absolute; width: 100%; top:50%; - z-index: 1011; + z-index: 2; font-weight: 600; font-size: 14px; text-align: center; @@ -546,47 +546,43 @@ 0px 0px 1px rgba(0,0,0,0.3); } -#videoResolutionLabel { - display: none; - position: absolute; - top: 5px; - right: 5px; - background: rgba(0,0,0,.5); - padding: 10px; - color: rgba(255,255,255,.5); - z-index: 1011; +.video-state-indicator { + background: $videoStateIndicatorBackground; + color: $videoStateIndicatorColor; + font-size: 13px; + line-height: 20px; + text-align: center; + min-width: 40px; + height: 40px; + padding: 10px 5px; border-radius: 50%; + position: absolute; + box-sizing: border-box; +} + +#videoResolutionLabel, +.centeredVideoLabel { + display: none; + z-index: 1011; } .centeredVideoLabel { - display: none; - position: absolute; bottom: 45%; - top: auto; - right: auto; - left: auto; - line-height: 28px; - height: 28px; - width: auto; - padding: 5px; - margin-right: auto; - margin-left: auto; - background: rgba(0,0,0,.5); - color: #FFF; - z-index: 1011; border-radius: 2px; -webkit-transition: all 2s 2s linear; transition: all 2s 2s linear; + + &.moveToCorner { + bottom: auto; + } } .moveToCorner { - top: 5px; - right: 50px; /*leave free space for the HD label*/ - margin-right: 0px; - margin-left: auto; - background: rgba(0,0,0,.3); - color: rgba(255,255,255,.5); + position: absolute; + top: 30px; + right: 30px; } -.hidden { -} +.moveToCorner + .moveToCorner { + right: 80px; +} \ No newline at end of file diff --git a/css/aui-components/dropdown.scss b/css/aui-components/dropdown.scss index 930cfebe0..1ef7a531c 100644 --- a/css/aui-components/dropdown.scss +++ b/css/aui-components/dropdown.scss @@ -3,6 +3,10 @@ form.aui { background-color: transparent; > a { + background-color: $inputBg !important; + color: $inputFontColor !important; + border-color: $inputBg !important; + text-shadow: none !important; margin: 0 auto !important; width: 100% !important; } @@ -32,17 +36,7 @@ form.aui { z-index: 900; } -//Dark theme -form.aui{ - //Placeholder - .aui-select2-container.input-container-dark { - a.select2-choice { - text-shadow: none; - } - } -} - .aui-dropdown2.aui-style-default.dropdown-dark { background-color: $defaultBackground; border-color: transparent; -} +} \ No newline at end of file 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/index.html b/index.html index 2e1be2b35..3426833d3 100644 --- a/index.html +++ b/index.html @@ -240,8 +240,8 @@ - HD - + HD + diff --git a/lang/main.json b/lang/main.json index 860483b3d..95f1b6e1b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -204,8 +204,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 b461c1590..74917a5ef 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'; @@ -189,13 +189,6 @@ UI.notifyConferenceDestroyed = function (reason) { "dialog.sessTerminated", 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 @@ -259,19 +252,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); @@ -1104,22 +1084,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 msg = APP.translation.generateTranslationHTML( - 'dialog.jicofoUnavailable' - ); - messageHandler.openDialog( - 'dialog.serviceUnavailable', - msg, - true, // persistent - [{title: 'retry'}], - function () { - reload(); - return false; - } - ); +UI.showPageReloadOverlay = function () { + PageReloadOverlay.show(15 /* will reload in 15 seconds */); }; /** @@ -1447,6 +1416,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 e71b1340d..23ebd79ef 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..bcc225491 --- /dev/null +++ b/modules/UI/reload_overlay/PageReloadOverlay.js @@ -0,0 +1,121 @@ +/* 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() { + + 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() { + + // 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 114418a75..fcc3b008e 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -326,7 +326,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 = '