Merge branch 'master' into talk-muted

This commit is contained in:
Lyubomir Marinov 2016-09-30 08:57:28 -05:00
commit c95a8e058c
72 changed files with 3538 additions and 1889 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules node_modules
.DS_Store
*.swp *.swp
.idea/ .idea/
*.iml *.iml

13
app.js
View File

@ -8,10 +8,14 @@ import "jquery-ui";
import "strophe"; import "strophe";
import "strophe-disco"; import "strophe-disco";
import "strophe-caps"; import "strophe-caps";
import "tooltip";
import "popover";
import "jQuery-Impromptu"; import "jQuery-Impromptu";
import "autosize"; import "autosize";
import 'aui';
import 'aui-experimental';
import 'aui-css';
import 'aui-experimental-css';
window.toastr = require("toastr"); window.toastr = require("toastr");
import URLProcessor from "./modules/config/URLProcessor"; import URLProcessor from "./modules/config/URLProcessor";
@ -106,6 +110,11 @@ function init() {
var isUIReady = APP.UI.start(); var isUIReady = APP.UI.start();
if (isUIReady) { if (isUIReady) {
APP.conference.init({roomName: buildRoomName()}).then(function () { APP.conference.init({roomName: buildRoomName()}).then(function () {
let server = APP.tokenData.server;
if(server) {
APP.conference.logEvent("server." + server, 1);
}
APP.UI.initConference(); APP.UI.initConference();
APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) { APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {

8
authError.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<head>
<link rel="stylesheet" href="css/all.css"/>
</head>
<body>
<div class="redirectPageMessage">Sorry! You are not allowed to be here :(</div>
</body>
</html>

8
close.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<head>
<link rel="stylesheet" href="css/all.css"/>
</head>
<body>
<div class="redirectPageMessage">Thank you for your feedback!</div>
</body>
</html>

View File

@ -40,7 +40,14 @@ let connectionIsInterrupted = false;
*/ */
let DSExternalInstallationInProgress = false; let DSExternalInstallationInProgress = false;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo"; /**
* Listens whether conference had been left from local user when we are trying
* to navigate away from current page.
* @type {ConferenceLeftListener}
*/
let conferenceLeftListener = null;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
/** /**
* Known custom conference commands. * Known custom conference commands.
@ -203,8 +210,28 @@ function muteLocalVideo (muted) {
/** /**
* Check if the welcome page is enabled and redirects to it. * Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
* @param {boolean} showThankYou whether we should show a thank you dialog
*/ */
function maybeRedirectToWelcomePage() { function maybeRedirectToWelcomePage(showThankYou) {
// if close page is enabled redirect to it, without further action
if (config.enableClosePage) {
window.location.pathname = "close.html";
return;
}
if (showThankYou) {
APP.UI.messageHandler.openMessageDialog(
null, null, null,
APP.translation.translateString(
"dialog.thankYou", {appName:interfaceConfig.APP_NAME}
)
);
}
if (!config.enableWelcomePage) { if (!config.enableWelcomePage) {
return; return;
} }
@ -236,7 +263,7 @@ function disconnectAndShowFeedback(requestFeedback) {
* @param {boolean} [requestFeedback=false] if user feedback should be requested * @param {boolean} [requestFeedback=false] if user feedback should be requested
*/ */
function hangup (requestFeedback = false) { function hangup (requestFeedback = false) {
const errCallback = (f, err) => { const errCallback = (err) => {
// If we want to break out the chain in our error handler, it needs // If we want to break out the chain in our error handler, it needs
// to return a rejected promise. In the case of feedback request // to return a rejected promise. In the case of feedback request
@ -251,14 +278,69 @@ function hangup (requestFeedback = false) {
} }
}; };
const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback); const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback);
APP.conference._room.leave()
.then(disconnect)
.catch(errCallback.bind(null, disconnect))
.then(maybeRedirectToWelcomePage)
.catch(function(err){
console.log(err);
});
if (!conferenceLeftListener)
conferenceLeftListener = new ConferenceLeftListener();
// Make sure that leave is resolved successfully and the set the handlers
// to be invoked once conference had been left
APP.conference._room.leave()
.then(conferenceLeftListener.setHandler(disconnect, errCallback))
.catch(errCallback);
}
/**
* Listens for CONFERENCE_LEFT event so we can check whether it has finished.
* The handler will be called once the conference had been left or if it
* was already left when we are adding the handler.
*/
class ConferenceLeftListener {
/**
* Creates ConferenceLeftListener and start listening for conference
* failed event.
*/
constructor() {
room.on(ConferenceEvents.CONFERENCE_LEFT,
this._handleConferenceLeft.bind(this));
}
/**
* Handles the conference left event, if we have a handler we invoke it.
* @private
*/
_handleConferenceLeft() {
this.conferenceLeft = true;
if (this.handler)
this._handleLeave();
}
/**
* Sets the handlers. If we already left the conference invoke them.
* @param handler
* @param errCallback
*/
setHandler (handler, errCallback) {
this.handler = handler;
this.errCallback = errCallback;
if (this.conferenceLeft)
this._handleLeave();
}
/**
* Invokes the handlers.
* @private
*/
_handleLeave()
{
this.handler()
.catch(this.errCallback)
.then(maybeRedirectToWelcomePage)
.catch(function(err){
console.log(err);
});
}
} }
/** /**
@ -294,8 +376,13 @@ function createLocalTracks (options, checkForPermissionPrompt) {
firefox_fake_device: config.firefox_fake_device, firefox_fake_device: config.firefox_fake_device,
desktopSharingExtensionExternalInstallation: desktopSharingExtensionExternalInstallation:
options.desktopSharingExtensionExternalInstallation options.desktopSharingExtensionExternalInstallation
}, checkForPermissionPrompt) }, checkForPermissionPrompt).then( (tracks) => {
.catch(function (err) { tracks.forEach((track) => {
track.on(TrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(null, track));
});
return tracks;
}).catch(function (err) {
console.error( console.error(
'failed to create local tracks', options.devices, err); 'failed to create local tracks', options.devices, err);
return Promise.reject(err); return Promise.reject(err);
@ -358,6 +445,14 @@ class ConferenceConnector {
case ConferenceErrors.PASSWORD_REQUIRED: case ConferenceErrors.PASSWORD_REQUIRED:
APP.UI.markRoomLocked(true); APP.UI.markRoomLocked(true);
roomLocker.requirePassword().then(function () { roomLocker.requirePassword().then(function () {
let pass = roomLocker.password;
// we received that password is required, but user is trying
// anyway to login without a password, mark room as not locked
// in case he succeeds (maybe someone removed the password
// meanwhile), if it is still locked another password required
// will be received and the room again will be marked as locked
if (!pass)
APP.UI.markRoomLocked(false);
room.join(roomLocker.password); room.join(roomLocker.password);
}); });
break; break;
@ -369,6 +464,13 @@ class ConferenceConnector {
} }
break; break;
case ConferenceErrors.NOT_ALLOWED_ERROR:
{
// let's show some auth not allowed page
window.location.pathname = "authError.html";
}
break;
case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE: case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
APP.UI.notifyBridgeDown(); APP.UI.notifyBridgeDown();
break; break;
@ -649,6 +751,61 @@ export default {
return this._room return this._room
&& this._room.getConnectionState(); && this._room.getConnectionState();
}, },
/**
* Checks whether or not our connection is currently in interrupted and
* reconnect attempts are in progress.
*
* @returns {boolean} true if the connection is in interrupted state or
* false otherwise.
*/
isConnectionInterrupted () {
return connectionIsInterrupted;
},
/**
* Finds JitsiParticipant for given id.
*
* @param {string} id participant's identifier(MUC nickname).
*
* @returns {JitsiParticipant|null} participant instance for given id or
* null if not found.
*/
getParticipantById (id) {
return room ? room.getParticipantById(id) : null;
},
/**
* Checks whether the user identified by given id is currently connected.
*
* @param {string} id participant's identifier(MUC nickname)
*
* @returns {boolean|null} true if participant's connection is ok or false
* if the user is having connectivity issues.
*/
isParticipantConnectionActive (id) {
let participant = this.getParticipantById(id);
return participant ? participant.isConnectionActive() : null;
},
/**
* Gets the display name foe the <tt>JitsiParticipant</tt> identified by
* the given <tt>id</tt>.
*
* @param id {string} the participant's id(MUC nickname/JVB endpoint id)
*
* @return {string} the participant's display name or the default string if
* absent.
*/
getParticipantDisplayName (id) {
let displayName = getDisplayName(id);
if (displayName) {
return displayName;
} else {
if (APP.conference.isLocalId(id)) {
return APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
} else {
return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
}
},
getMyUserId () { getMyUserId () {
return this._room return this._room
&& this._room.myUserId(); && this._room.myUserId();
@ -699,6 +856,30 @@ export default {
return room.getLogs(); return room.getLogs();
}, },
/**
* Download logs, a function that can be called from console while
* debugging.
* @param filename (optional) specify target filename
*/
saveLogs (filename = 'meetlog.json') {
// this can be called from console and will not have reference to this
// that's why we reference the global var
let logs = APP.conference.getLogs();
let data = encodeURIComponent(JSON.stringify(logs, null, ' '));
let elem = document.createElement('a');
elem.download = filename;
elem.href = 'data:application/json;charset=utf-8,\n' + data;
elem.dataset.downloadurl
= ['text/json', elem.download, elem.href].join(':');
elem.dispatchEvent(new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: false
}));
},
/** /**
* Exposes a Command(s) API on this instance. It is necessitated by (1) the * Exposes a Command(s) API on this instance. It is necessitated by (1) the
* desire to keep room private to this instance and (2) the need of other * desire to keep room private to this instance and (2) the need of other
@ -858,8 +1039,6 @@ export default {
return promise.then(function () { return promise.then(function () {
if (stream) { if (stream) {
stream.on(TrackEvents.TRACK_AUDIO_NOT_WORKING,
APP.UI.showAudioNotWorkingDialog);
return room.addTrack(stream); return room.addTrack(stream);
} }
}).then(() => { }).then(() => {
@ -1021,7 +1200,7 @@ export default {
console.log('USER %s connnected', id, user); console.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id); APP.API.notifyUserJoined(id);
APP.UI.addUser(id, user.getDisplayName()); APP.UI.addUser(user);
// check the roles for the new user and reflect them // check the roles for the new user and reflect them
APP.UI.updateUserRole(user); APP.UI.updateUserRole(user);
@ -1037,8 +1216,10 @@ export default {
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => { room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
if (this.isLocalId(id)) { if (this.isLocalId(id)) {
console.info(`My role changed, new role: ${role}`); console.info(`My role changed, new role: ${role}`);
this.isModerator = room.isModerator(); if (this.isModerator !== room.isModerator()) {
APP.UI.updateLocalRole(room.isModerator()); this.isModerator = room.isModerator();
APP.UI.updateLocalRole(room.isModerator());
}
} else { } else {
let user = room.getParticipantById(id); let user = room.getParticipantById(id);
if (user) { if (user) {
@ -1115,6 +1296,11 @@ export default {
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => { ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
APP.UI.handleLastNEndpoints(ids, enteringIds); APP.UI.handleLastNEndpoints(ids, enteringIds);
}); });
room.on(
ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
(id, isActive) => {
APP.UI.participantConnectionStatusChanged(id, isActive);
});
room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => { room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
if (this.isLocalId(id)) { if (this.isLocalId(id)) {
this.isDominantSpeaker = true; this.isDominantSpeaker = true;
@ -1146,10 +1332,12 @@ export default {
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => { room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
connectionIsInterrupted = true; connectionIsInterrupted = true;
ConnectionQuality.updateLocalConnectionQuality(0); ConnectionQuality.updateLocalConnectionQuality(0);
APP.UI.showLocalConnectionInterrupted(true);
}); });
room.on(ConferenceEvents.CONNECTION_RESTORED, () => { room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
connectionIsInterrupted = false; connectionIsInterrupted = false;
APP.UI.showLocalConnectionInterrupted(false);
}); });
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => { room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
@ -1173,6 +1361,7 @@ export default {
console.log("Received channel password lock change: ", state, console.log("Received channel password lock change: ", state,
error); error);
APP.UI.markRoomLocked(state); APP.UI.markRoomLocked(state);
roomLocker.lockedElsewhere = state;
}); });
room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) { room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
@ -1300,15 +1489,6 @@ export default {
&& APP.UI.notifyInitiallyMuted(); && APP.UI.notifyInitiallyMuted();
}); });
APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
APP.UI.inviteParticipants(
roomUrl,
APP.conference.roomName,
roomLocker.password,
APP.settings.getDisplayName()
);
});
room.on( room.on(
ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) { ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
APP.UI.updateDevicesAvailability(id, devices); APP.UI.updateDevicesAvailability(id, devices);
@ -1395,6 +1575,8 @@ export default {
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => { APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
var smallVideoId = smallVideo.getId(); var smallVideoId = smallVideo.getId();
// FIXME why VIDEO_CONTAINER_TYPE instead of checking if
// the participant is on the large video ?
if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
&& !APP.conference.isLocalId(smallVideoId)) { && !APP.conference.isLocalId(smallVideoId)) {
@ -1422,7 +1604,7 @@ export default {
.then(([stream]) => { .then(([stream]) => {
this.useVideoStream(stream); this.useVideoStream(stream);
console.log('switched local video device'); console.log('switched local video device');
APP.settings.setCameraDeviceId(cameraDeviceId); APP.settings.setCameraDeviceId(cameraDeviceId, true);
}) })
.catch((err) => { .catch((err) => {
APP.UI.showDeviceErrorDialog(null, err); APP.UI.showDeviceErrorDialog(null, err);
@ -1444,7 +1626,7 @@ export default {
.then(([stream]) => { .then(([stream]) => {
this.useAudioStream(stream); this.useAudioStream(stream);
console.log('switched local audio device'); console.log('switched local audio device');
APP.settings.setMicDeviceId(micDeviceId); APP.settings.setMicDeviceId(micDeviceId, true);
}) })
.catch((err) => { .catch((err) => {
APP.UI.showDeviceErrorDialog(err, null); APP.UI.showDeviceErrorDialog(err, null);
@ -1539,13 +1721,13 @@ export default {
// storage and settings menu. This is a workaround until // storage and settings menu. This is a workaround until
// getConstraints() method will be implemented in browsers. // getConstraints() method will be implemented in browsers.
if (localAudio) { if (localAudio) {
localAudio._setRealDeviceIdFromDeviceList(devices); APP.settings.setMicDeviceId(
APP.settings.setMicDeviceId(localAudio.getDeviceId()); localAudio.getDeviceId(), false);
} }
if (localVideo) { if (localVideo) {
localVideo._setRealDeviceIdFromDeviceList(devices); APP.settings.setCameraDeviceId(
APP.settings.setCameraDeviceId(localVideo.getDeviceId()); localVideo.getDeviceId(), false);
} }
mediaDeviceHelper.setCurrentMediaDevices(devices); mediaDeviceHelper.setCurrentMediaDevices(devices);
@ -1646,11 +1828,28 @@ export default {
setRaisedHand(raisedHand) { setRaisedHand(raisedHand) {
if (raisedHand !== this.isHandRaised) if (raisedHand !== this.isHandRaised)
{ {
APP.UI.onLocalRaiseHandChanged(raisedHand);
this.isHandRaised = raisedHand; this.isHandRaised = raisedHand;
// Advertise the updated status // Advertise the updated status
room.setLocalParticipantProperty("raisedHand", raisedHand); room.setLocalParticipantProperty("raisedHand", raisedHand);
// Update the view // Update the view
APP.UI.setLocalRaisedHandStatus(raisedHand); APP.UI.setLocalRaisedHandStatus(raisedHand);
} }
},
/**
* Log event to callstats and analytics.
* @param {string} name the event name
* @param {int} value the value (it's int because google analytics supports
* only int).
* NOTE: Should be used after conference.init
*/
logEvent(name, value) {
if(JitsiMeetJS.analytics) {
JitsiMeetJS.analytics.sendEvent(name, value);
}
if(room) {
room.sendApplicationLog(JSON.stringify({name, value}));
}
} }
}; };

View File

@ -56,6 +56,8 @@ var config = {
//disableAdaptiveSimulcast: false, //disableAdaptiveSimulcast: false,
enableRecording: false, enableRecording: false,
enableWelcomePage: true, enableWelcomePage: true,
//enableClosePage: false, // enabling the close page will ignore the welcome
// page redirection when call is hangup
disableSimulcast: false, disableSimulcast: false,
logStats: false, // Enable logging of PeerConnection stats via the focus logStats: false, // Enable logging of PeerConnection stats via the focus
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room. // requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.

BIN
css/.DS_Store vendored

Binary file not shown.

View File

@ -81,24 +81,10 @@ form {
display: block; display: block;
} }
#downloadlog {
display: none;
position: absolute;
bottom: 5;
left: 5;
overflow: visible;
color: rgba(255,255,255,.50);
}
.active { .active {
background-color: #00ccff; background-color: #00ccff;
} }
.glow
{
text-shadow: 0px 0px 30px #06a5df, 0px 0px 10px #06a5df, 0px 0px 5px #06a5df,0px 0px 3px #06a5df;
}
.watermark { .watermark {
display: block; display: block;
position: absolute; position: absolute;
@ -175,4 +161,15 @@ form {
display: -ms-flexbox !important; display: -ms-flexbox !important;
display: -webkit-flex !important; display: -webkit-flex !important;
display: flex !important; display: flex !important;
} }
.tipsy {
z-index: $tooltipsZ;
&-inner {
background-color: $tooltipBg;
}
&-arrow {
border-color: $tooltipBg;
}
}

View File

@ -104,11 +104,6 @@
color: #a7a7a7; color: #a7a7a7;
} }
#unreadMessages {
font-size: 8px;
position: absolute;
}
#chat_container .username { #chat_container .username {
float: left; float: left;
padding-left: 5px; padding-left: 5px;

View File

@ -2,12 +2,11 @@
cursor: default; cursor: default;
> ul#contacts { > ul#contacts {
position: absolute; font-size: 12px;
top: 31px;
bottom: 0px; bottom: 0px;
width: 100%;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
width: 100%;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
} }
@ -20,13 +19,13 @@
#contacts { #contacts {
>li { >li {
color: $defaultSideBarFontColor;
list-style-type: none; list-style-type: none;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
color: #FFF; color: #FFF;
font-size: 10pt; font-size: 10pt;
padding: 7px 10px; padding: 6px 10%;
margin: 2px;
&:hover, &:hover,
&:active { &:active {

View File

@ -255,9 +255,6 @@
.fa-road:before { .fa-road:before {
content: "\f018"; content: "\f018";
} }
.fa-download:before {
content: "\f019";
}
.fa-arrow-circle-o-down:before { .fa-arrow-circle-o-down:before {
content: "\f01a"; content: "\f01a";
} }
@ -842,9 +839,6 @@
.fa-exchange:before { .fa-exchange:before {
content: "\f0ec"; content: "\f0ec";
} }
.fa-cloud-download:before {
content: "\f0ed";
}
.fa-cloud-upload:before { .fa-cloud-upload:before {
content: "\f0ee"; content: "\f0ee";
} }

View File

@ -16,8 +16,9 @@
font-weight: normal; font-weight: normal;
font-variant: normal; font-variant: normal;
text-transform: none; text-transform: none;
line-height: 0.75em; line-height: 1.22em;
font-size: 1.22em; font-size: 1.22em;
cursor: default;
/* Better Font Rendering =========== */ /* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -42,9 +43,6 @@
.icon-chat:before { .icon-chat:before {
content: "\e906"; content: "\e906";
} }
.icon-download:before {
content: "\e902";
}
.icon-edit:before { .icon-edit:before {
content: "\e907"; content: "\e907";
} }
@ -57,12 +55,21 @@
.icon-kick:before { .icon-kick:before {
content: "\e904"; content: "\e904";
} }
.icon-menu-up:before {
content: "\e91f";
}
.icon-menu-down:before {
content: "\e920";
}
.icon-full-screen:before { .icon-full-screen:before {
content: "\e90b"; content: "\e90b";
} }
.icon-exit-full-screen:before { .icon-exit-full-screen:before {
content: "\e90c"; content: "\e90c";
} }
.icon-star:before {
content: "\e916";
}
.icon-star-full:before { .icon-star-full:before {
content: "\e90a"; content: "\e90a";
} }
@ -99,9 +106,6 @@
.icon-settings:before { .icon-settings:before {
content: "\e915"; content: "\e915";
} }
.icon-star:before {
content: "\e916";
}
.icon-share-desktop:before { .icon-share-desktop:before {
content: "\e917"; content: "\e917";
} }
@ -126,6 +130,7 @@
.icon-recEnable:before { .icon-recEnable:before {
content: "\e614"; content: "\e614";
} }
// FIXME not used anymore - consider removing in the next font update
.icon-presentation:before { .icon-presentation:before {
content: "\e603"; content: "\e603";
} }

View File

@ -9,7 +9,8 @@
} }
div.jqi{ div.jqi{
width: 400px; width: 400px;
position: absolute; position: absolute;
color: #3a3a3a;
background-color: #ffffff; background-color: #ffffff;
font-size: 11px; font-size: 11px;
text-align: left; text-align: left;

View File

@ -36,6 +36,19 @@
} }
} }
@mixin circle($diameter) {
width: $diameter;
height: $diameter;
border-radius: 50%;
}
@mixin absoluteAligning($sizeX, $sizeY) {
top: 50%;
left: 50%;
position: absolute;
@include transform(translate(-#{$sizeX / 2}, -#{$sizeY / 2}))
}
@mixin transform($func) { @mixin transform($func) {
-moz-transform: $func; -moz-transform: $func;
-ms-transform: $func; -ms-transform: $func;

12
css/_redirect_page.scss Normal file
View File

@ -0,0 +1,12 @@
html, body {
width: 100%;
height:100%;
color: $defaultColor;
background: $defaultBackground;
}
.redirectPageMessage {
text-align: center;
font-size: 36px;
margin-top: 20%;
}

View File

@ -12,12 +12,13 @@
background-color: rgba(0,0,0,0.8); background-color: rgba(0,0,0,0.8);
z-index: 800; z-index: 800;
overflow: hidden; overflow: hidden;
letter-spacing: 1px;
/** /**
* Labels inside the side panel. * Labels inside the side panel.
*/ */
label { label {
color: $defaultSemiDarkColor; color: $defaultColor;
} }
/** /**
@ -70,16 +71,16 @@
*/ */
> div.title, > div.title,
div.subTitle { div.subTitle {
color: $defaultColor !important;
text-align: left; text-align: left;
margin: 10px 0px 10px 0px; margin: 10px 0px 10px 0px;
padding: 5px 10px 5px 10px;
} }
/** /**
* Main title size. * Main title size.
*/ */
> div.title { > div.title {
color: $defaultColor !important;
text-align: center;
font-size: 16px; font-size: 16px;
} }
@ -87,10 +88,10 @@
* Subtitle specific properties. * Subtitle specific properties.
*/ */
> div.subTitle { > div.subTitle {
font-size: 12px; font-size: 11px;
background: $inputSemiBackground !important; font-weight: 500;
margin-top: 20px !important; color: $defaultSideBarFontColor !important;
margin-bottom: 8px !important; margin-left: 10%;
} }
/** /**

View File

@ -83,14 +83,6 @@
display: none; display: none;
} }
#numberOfParticipants {
position: absolute;
top: 5px;
line-height: 13px;
font-weight: bold;
font-size: 11px;
}
#mainToolbar a.button:last-child::after { #mainToolbar a.button:last-child::after {
content: none; content: none;
} }
@ -118,6 +110,11 @@
cursor: default; cursor: default;
} }
.button.glow
{
text-shadow: 0px 0px 5px $toolbarToggleBackground;
}
a.button.unclickable:hover, a.button.unclickable:hover,
a.button.unclickable:active, a.button.unclickable:active,
a.button.unclickable.selected{ a.button.unclickable.selected{
@ -129,6 +126,7 @@ a.button:hover,
a.button:active, a.button:active,
a.button.selected { a.button.selected {
cursor: pointer; cursor: pointer;
text-decoration: none;
// sum opacity with background layer should give us 0.8 // sum opacity with background layer should give us 0.8
background: $toolbarSelectBackground; background: $toolbarSelectBackground;
} }
@ -144,6 +142,36 @@ a.button>#avatar {
margin-top: auto; margin-top: auto;
} }
/**
* Round badge.
*/
.badge-round {
background-color: $toolbarBadgeBackground;
color: $toolbarBadgeColor;
font-size: 9px;
line-height: 13px;
font-weight: 700;
text-align: center;
border-radius: 50%;
min-width: 13px;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
vertical-align: middle;
// Do not inherit the font-family from the toolbar button, because it's an
// icon style.
font-family: $baseFontFamily;
}
/**
* Toolbar specific round badge.
*/
.toolbar .badge-round {
position: absolute;
right: 9px;
bottom: 9px;
}
/** /**
* START of slide in animation for extended toolbar. * START of slide in animation for extended toolbar.
*/ */

View File

@ -10,20 +10,49 @@ $hangupFontSize: 2em;
*/ */
$defaultToolbarSize: 50px; $defaultToolbarSize: 50px;
// Video layout.
$thumbnailIndicatorSize: 23px;
$thumbnailIndicatorBorder: 0px;
$thumbnailVideoMargin: 2px;
$thumbnailToolbarHeight: 25px;
/** /**
* Color variables. * Color variables.
*/ */
$defaultColor: #F1F1F1; $defaultColor: #F1F1F1;
$defaultSemiDarkColor: #ACACAC; $defaultSideBarFontColor: #44A5FF;
$defaultDarkColor: #4F4F4F; $defaultDarkColor: #4F4F4F;
$defaultBackground: #474747; $defaultBackground: #474747;
$tooltipBg: rgba(0,0,0, 0.7);
// Toolbar
$toolbarSelectBackground: rgba(0, 0, 0, .6); $toolbarSelectBackground: rgba(0, 0, 0, .6);
$toolbarBadgeBackground: #165ECC;
$toolbarBadgeColor: #FFFFFF;
$toolbarToggleBackground: #165ECC;
// Main controls
$inputBackground: rgba(132, 132, 132, .5); $inputBackground: rgba(132, 132, 132, .5);
$inputSemiBackground: rgba(132, 132, 132, .8); $inputSemiBackground: rgba(132, 132, 132, .8);
$inputLightBackground: #EBEBEB; $inputLightBackground: #EBEBEB;
$inputBorderColor: #EBEBEB; $inputBorderColor: #EBEBEB;
$buttonBackground: #44A5FF; $buttonBackground: #44A5FF;
// Video layout.
$videoThumbnailHovered: #BFEBFF;
$videoThumbnailSelected: #165ECC;
$participantNameColor: #fff;
$thumbnailPictogramColor: #fff;
$dominantSpeakerBg: #165ecc;
$raiseHandBg: #D6D61E;
$audioLevelBg: #44A5FF;
$audioLevelShadow: rgba(9, 36, 77, 0.9);
$rateStarDefault: #ccc;
$rateStarActivity: #165ecc;
$rateStarLabelColor: #333;
/** /**
* Misc. * Misc.
*/ */
@ -33,9 +62,6 @@ $defaultWatermarkLink: '../images/watermark.png';
/** /**
* Z-indexes. TODO: Replace this by a function. * Z-indexes. TODO: Replace this by a function.
*/ */
$tooltipsZ: 901;
$toolbarZ: 900; $toolbarZ: 900;
$overlayZ: 800; $overlayZ: 800;
$rateStarDefault: #ccc;
$rateStarActivity: #f6c342;
$rateStarLabelColor: #333;

View File

@ -1,5 +1,10 @@
#videoconference_page {
min-height: 100%;
}
#videospace { #videospace {
display: block; display: block;
min-height: 100%;
position: absolute; position: absolute;
top: 0px; top: 0px;
left: 0px; left: 0px;
@ -13,19 +18,19 @@
display: -ms-flexbox; display: -ms-flexbox;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
flex-direction: row; flex-direction: row-reverse;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-end; justify-content: flex-start;
position:absolute; position:absolute;
text-align:right; text-align:right;
height:196px; height:196px;
padding: 18px; padding: 10px 10px 10px 5px;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 20px; right: 0;
width:auto; width:auto;
border:1px solid transparent; border: 2px solid transparent;
z-index: 5; z-index: 5;
transition: bottom 2s; transition: bottom 2s;
overflow: visible !important; overflow: visible !important;
@ -43,33 +48,58 @@
#remoteVideos .videocontainer { #remoteVideos .videocontainer {
display: none; display: none;
position: relative;
background-color: black; background-color: black;
background-size: contain; background-size: contain;
border-radius:1px; border-radius:1px;
border: 1px solid #212425; margin: 0 $thumbnailVideoMargin;
} }
#remoteVideos .videocontainer.videoContainerFocused { /**
* The toolbar of the video thumbnail.
*/
.videocontainer__toolbar {
position: absolute;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
box-sizing: border-box; // Includes the padding in the 100% width.
height: $thumbnailToolbarHeight;
max-height: 100%;
background-color: rgba(0, 0, 0, 0.5);
padding: 0 5px 0 5px;
}
#remoteVideos .videocontainer.videoContainerFocused,
#remoteVideos .videocontainer:hover {
cursor: hand; cursor: hand;
margin-right: $thumbnailVideoMargin - 2;
margin-left: $thumbnailVideoMargin - 2;
margin-top: -2px;
}
/**
* Focused video thumbnail.
*/
#remoteVideos .videocontainer.videoContainerFocused {
transition-duration: 0.5s; transition-duration: 0.5s;
-webkit-transition-duration: 0.5s; -webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse; -webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s; -webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1; -webkit-animation-iteration-count: 1;
border: 2px solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected !important;
} }
/**
* Hovered video thumbnail.
*/
#remoteVideos .videocontainer:hover { #remoteVideos .videocontainer:hover {
border: 1px solid #c1c1c1; cursor: hand;
} border: 2px solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,
#remoteVideos .videocontainer.videoContainerFocused { 0 0 3px $videoThumbnailHovered;
box-shadow: inset 0 0 28px #006d91;
border: 1px solid #006d91;
}
#remoteVideos .videocontainer.videoContainerFocused:hover {
box-shadow: inset 0 0 5px #c1c1c1, 0 0 10px #c1c1c1, inset 0 0 60px #006d91;
border: 1px solid #c1c1c1;
} }
#localVideoWrapper { #localVideoWrapper {
@ -113,7 +143,6 @@
object-fit: cover; object-fit: cover;
} }
#presentation,
#sharedVideo, #sharedVideo,
#etherpad, #etherpad,
#localVideoWrapper>video, #localVideoWrapper>video,
@ -132,8 +161,7 @@
height: 100%; height: 100%;
} }
#etherpad, #etherpad {
#presentation {
text-align: center; text-align: center;
} }
@ -141,47 +169,36 @@
z-index: 0; z-index: 0;
} }
#remoteVideos .videocontainer>span.focusindicator, /**
#remoteVideos .videocontainer>div.remotevideomenu { * Positions video thumbnail display name and editor.
position: absolute; */
color: #FFFFFF; .videocontainer .displayname,
top: 0; .videocontainer .editdisplayname {
left: 0;
padding: 5px 0px;
width: 25px;
font-size: 11pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 2;
text-align: center;
}
#remoteVideos .videocontainer>span.focusindicator {
display: inline-block; display: inline-block;
}
#remoteVideos .videocontainer>div.remotevideomenu {
display: block;
}
.videocontainer>span.displayname,
.videocontainer>input.displayname {
display: none;
position: absolute; position: absolute;
color: #FFFFFF; left: 30%;
background: rgba(0,0,0,.7); width: 40%;
color: $participantNameColor;
text-align: center; text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 70%; font-size: 12px;
height: 20%; font-weight: 100;
left: 15%; letter-spacing: 1px;
top: 40%;
padding: 5px;
font-size: 11pt;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
line-height: $thumbnailToolbarHeight;
z-index: 2; z-index: 2;
border-radius:3px; }
/**
* Positions video thumbnail display name editor.
*/
.videocontainer .editdisplayname {
outline: none;
border: none;
background: none;
box-shadow: none;
padding: 0;
} }
.videocontainer>span.status { .videocontainer>span.status {
@ -221,6 +238,12 @@
overflow: hidden; overflow: hidden;
} }
.connection.connection_lost
{
color: #8B8B8B;
overflow: visible;
}
.connection.connection_full .connection.connection_full
{ {
color: #FFFFFF;/*#15A1ED*/ color: #FFFFFF;/*#15A1ED*/
@ -257,16 +280,16 @@
} }
#localVideoContainer>span.status:hover, #localVideoContainer>span.status:hover,
#localVideoContainer>span.displayname:hover { #localVideoContainer .displayname:hover {
cursor: text; cursor: text;
} }
.videocontainer>span.status, .videocontainer>span.status,
.videocontainer>span.displayname { .videocontainer .displayname {
pointer-events: none; pointer-events: none;
} }
.videocontainer>input.displayname { .videocontainer .editdisplayname {
height: auto; height: auto;
} }
@ -287,53 +310,103 @@
z-index: 2; z-index: 2;
} }
.videocontainer>span.audioMuted { /**
display: inline-block; * Video thumbnail toolbar icon.
position: absolute; */
color: #FFFFFF; .videocontainer .toolbar-icon {
top: 0;
padding: 8px 5px;
width: 25px;
font-size: 8pt; font-size: 8pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
text-align: center; text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
color: #FFFFFF;
width: 12px;
line-height: $thumbnailToolbarHeight;
height: $thumbnailToolbarHeight;
padding: 0;
border: 0;
margin: 0px 5px 0px 0px;
float: left;
} }
.videocontainer>span.videoMuted { /**
display: inline-block; * Toolbar icon internal i elements (font icons).
position: absolute; */
color: #FFFFFF; .toolbar-icon>i {
top: 0; line-height: $thumbnailToolbarHeight;
right: 0; }
padding: 8px 5px;
width: 25px; /**
font-size: 8pt; * Toolbar icons positioned on the right.
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); */
border: 0px; .toolbar-icon.right {
z-index: 3; float: right;
margin: 0px 0px 0px 5px;
} }
.videocontainer>span.indicator { .videocontainer>span.indicator {
bottom: 0px; position: absolute;
top: 0px;
left: 0px; left: 0px;
width: 25px; @include circle($thumbnailIndicatorSize);
height: 25px; box-sizing: border-box;
line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
z-index: 3; z-index: 3;
text-align: center; text-align: center;
border-radius: 50%; background: $dominantSpeakerBg;
background: #21B9FC; margin: 7px;
margin: 5px;
display: inline-block; display: inline-block;
color: $thumbnailPictogramColor;
font-size: 8pt;
border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
}
.videocontainer>#raisehandindicator {
background: $raiseHandBg;
}
/**
* Audio indicator on video thumbnails.
*/
.videocontainer>span.audioindicator {
position: absolute; position: absolute;
color: #FFFFFF; display: inline-block;
font-size: 11pt; left: 6px;
border: 0px; top: 50%;
margin-top: -17px;
width: 6px;
height: 35px;
z-index: 2;
border: none;
.audiodot-top,
.audiodot-bottom,
.audiodot-middle {
opacity: 0;
display: inline-block;
@include circle(5px);
background: $audioLevelShadow;
margin: 1px 0 1px 0;
transition: opacity .25s ease-in-out;
-moz-transition: opacity .25s ease-in-out;
}
span.audiodot-top::after,
span.audiodot-bottom::after,
span.audiodot-middle::after {
content: "";
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
-webkit-filter: blur(0.5px);
filter: blur(0.5px);
background: $audioLevelBg;
}
} }
#indicatoricon { #indicatoricon {
padding-top: 5px; width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
} }
#reloadPresentation { #reloadPresentation {
@ -366,25 +439,20 @@
width: 300px; width: 300px;
height: 300px; height: 300px;
margin: auto; margin: auto;
overflow: hidden;
position: relative; position: relative;
} }
#dominantSpeakerAudioLevel {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
visibility: inherit;
}
#mixedstream { #mixedstream {
display:none !important; display:none !important;
} }
#dominantSpeakerAvatar { #dominantSpeakerAvatar,
.dynamic-shadow {
width: 200px; width: 200px;
height: 200px; height: 200px;
}
#dominantSpeakerAvatar {
top: 50px; top: 50px;
margin: auto; margin: auto;
position: relative; position: relative;
@ -394,11 +462,18 @@
background-color: #000000; background-color: #000000;
} }
.userAvatar { .dynamic-shadow {
height: 100%; border-radius: 50%;
position: absolute; position: absolute;
left: 0; top: 50%;
border-radius: 2px; left: 50%;
margin: -100px 0 0 -100px;
transition: box-shadow 0.3s ease;
}
.userAvatar {
@include circle(60px);
@include absoluteAligning(60px, 60px);
} }
.sharedVideoAvatar { .sharedVideoAvatar {
@ -436,12 +511,44 @@
filter: grayscale(.5) opacity(0.8); filter: grayscale(.5) opacity(0.8);
} }
.remoteVideoProblemFilter {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.videoProblemFilter { .videoProblemFilter {
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8); -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
filter: blur(10px) grayscale(.5) opacity(0.8); filter: blur(10px) grayscale(.5) opacity(0.8);
} }
#videoConnectionMessage { .videoThumbnailProblemFilter {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
#remoteConnectionMessage {
display: none;
position: absolute;
width: auto;
z-index: 1011;
font-weight: 600;
font-size: 14px;
text-align: center;
color: #FFF;
opacity: .80;
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
0px 1px 1px rgba(0,0,0,0.3),
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
background: rgba(0,0,0,.5);
border-radius: 5px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
}
#localConnectionMessage {
display: none; display: none;
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@ -22,6 +22,8 @@
@import 'toastr'; @import 'toastr';
@import 'base'; @import 'base';
@import 'overlay/overlay'; @import 'overlay/overlay';
@import 'modals/dialog';
@import 'modals/feedback/feedback';
@import 'videolayout_default'; @import 'videolayout_default';
@import 'jquery-impromptu'; @import 'jquery-impromptu';
@import 'modaldialog'; @import 'modaldialog';
@ -38,8 +40,9 @@
@import 'toolbars'; @import 'toolbars';
@import 'side_toolbar_container'; @import 'side_toolbar_container';
@import 'device_settings_dialog'; @import 'device_settings_dialog';
@import 'feedback';
@import 'jquery.contextMenu'; @import 'jquery.contextMenu';
@import 'keyboard-shortcuts'; @import 'keyboard-shortcuts';
@import 'redirect_page';
/* Modules END */ /* Modules END */

53
css/modals/_dialog.scss Normal file
View File

@ -0,0 +1,53 @@
.dialog{
visibility: visible;
height: auto;
p {
color: $defaultDarkColor;
}
textarea {
background: none;
border: 1px solid $inputBorderColor;
}
.aui-dialog2-content:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
.aui-dialog2-content:first-child {
border-top-right-radius: 5px;
border-top-left-radius: 5px;
}
.aui-dialog2-footer{
border-top: 0;
border-radius: 0;
padding-top: 0;
background: none;
border: none;
height: auto;
margin-top: 10px;
}
.aui-button {
height: 28px;
font-size: 12px;
padding: 3px 6px 3px 6px;
border: none;
box-shadow: none;
outline: none;
&_close {
font-weight: 400 !important;
color: $buttonBackground;
background: none !important;
:hover {
text-decoration: underline;
}
}
&_submit {
font-weight: 700 !important;
color: $defaultColor;
background: $buttonBackground;
border-radius: 3px;
}
}
}

View File

@ -33,6 +33,8 @@
} }
.shake-rotate { .shake-rotate {
display: inline-block;
-webkit-animation-duration: .4s; -webkit-animation-duration: .4s;
animation-duration: .4s; animation-duration: .4s;
-webkit-animation-iteration-count: infinite; -webkit-animation-iteration-count: infinite;
@ -43,65 +45,64 @@
animation-timing-function: ease-in-out animation-timing-function: ease-in-out
} }
.text-center { .feedback {
text-align: center;
}
.feedbackDetails textarea {
resize: vertical;
min-height: 100px;
}
.feedback-rating {
line-height: 1.2;
padding: 20px 0;
h2 { h2 {
font-weight: 400; font-weight: 400;
font-size: 24px; font-size: 24px;
line-height: 1.2; line-height: 1.2;
padding: auto;
margin: auto;
border: none;
} }
p { p {
margin-top: 10px; font-weight: 400;
margin-left: 0px; font-size: 14px;
margin-bottom: 0px;
margin-right: 0px;
} }
.star-label { &__content {
font-size: 16px; text-align: center;
color: $rateStarLabelColor;
textarea {
text-align: left;
min-height: 80px;
width: 100%;
}
} }
&__footer {
.star-btn {
color: $rateStarDefault;
font-size: 36px;
position: relative;
cursor: pointer;
outline: none;
text-decoration: none;
@include transition(all .2s ease);
&.starHover,
&.active,
&:hover { &:hover {
color: $rateStarActivity; color: #287ade;
outline: 0;
}
}
&__rating {
line-height: 1.2;
padding: 20px 0;
.fa { p {
top: -6px; margin: 10px 0 0;
}
};
&.rated:hover .fa {
top: 0;
} }
.fa { .star-label {
font-size: 16px;
color: $rateStarLabelColor;
}
.star-btn {
color: $rateStarDefault;
font-size: 36px;
position: relative; position: relative;
cursor: pointer;
outline: none;
text-decoration: none;
@include transition(all .2s ease);
&.starHover,
&.active,
&:hover {
color: $rateStarActivity;
> i:before {
content: "\e90a";
}
};
} }
} }
} }

View File

@ -9,6 +9,11 @@
background: linear-gradient(transparent, #000); background: linear-gradient(transparent, #000);
opacity: 0.8; opacity: 0.8;
&.solidBG {
background: $defaultBackground;
opacity: 1;
}
&__content { &__content {
position: absolute; position: absolute;
width: 400px; width: 400px;
@ -33,4 +38,4 @@
color: #333; color: #333;
} }
} }
} }

Binary file not shown.

View File

@ -42,4 +42,6 @@
<glyph unicode="&#xe91c;" glyph-name="toggle-filmstrip" d="M896 896h-768c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h768c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333zM896 213.333h-768v128h768v-128z" /> <glyph unicode="&#xe91c;" glyph-name="toggle-filmstrip" d="M896 896h-768c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h768c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333zM896 213.333h-768v128h768v-128z" />
<glyph unicode="&#xe91d;" glyph-name="feedback" d="M42.667 128h170.667v512h-170.667v-512zM981.333 597.333c0 46.933-38.4 85.333-85.333 85.333h-269.227l40.533 194.987 1.28 13.653c0 17.493-7.253 33.707-18.773 45.227l-45.227 44.8-280.747-281.173c-15.787-15.36-25.173-36.693-25.173-60.16v-426.667c0-46.933 38.4-85.333 85.333-85.333h384c35.413 0 65.707 21.333 78.507 52.053l128.853 300.8c3.84 9.813 5.973 20.053 5.973 31.147v81.493l-0.427 0.427 0.427 3.413z" /> <glyph unicode="&#xe91d;" glyph-name="feedback" d="M42.667 128h170.667v512h-170.667v-512zM981.333 597.333c0 46.933-38.4 85.333-85.333 85.333h-269.227l40.533 194.987 1.28 13.653c0 17.493-7.253 33.707-18.773 45.227l-45.227 44.8-280.747-281.173c-15.787-15.36-25.173-36.693-25.173-60.16v-426.667c0-46.933 38.4-85.333 85.333-85.333h384c35.413 0 65.707 21.333 78.507 52.053l128.853 300.8c3.84 9.813 5.973 20.053 5.973 31.147v81.493l-0.427 0.427 0.427 3.413z" />
<glyph unicode="&#xe91e;" glyph-name="raised-hand" d="M982 790v-620c0-94-78-170-172-170h-310c-46 0-90 18-122 50l-336 342s54 52 56 52c10 8 22 12 34 12 10 0 18-2 26-6 2 0 184-104 184-104v508c0 36 28 64 64 64s64-28 64-64v-300h42v406c0 36 28 64 64 64s64-28 64-64v-406h42v364c0 36 28 64 64 64s64-28 64-64v-364h44v236c0 36 28 64 64 64s64-28 64-64z" /> <glyph unicode="&#xe91e;" glyph-name="raised-hand" d="M982 790v-620c0-94-78-170-172-170h-310c-46 0-90 18-122 50l-336 342s54 52 56 52c10 8 22 12 34 12 10 0 18-2 26-6 2 0 184-104 184-104v508c0 36 28 64 64 64s64-28 64-64v-300h42v406c0 36 28 64 64 64s64-28 64-64v-406h42v364c0 36 28 64 64 64s64-28 64-64v-364h44v236c0 36 28 64 64 64s64-28 64-64z" />
<glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
<glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

View File

@ -293,6 +293,58 @@
"setId": 2, "setId": 2,
"iconIdx": 243 "iconIdx": 243
}, },
{
"icon": {
"paths": [
"M512 342l256 256-60 60-196-196-196 196-60-60z"
],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"expand_less"
],
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 256,
"order": 106,
"ligatures": "expand_less",
"prevSize": 32,
"code": 59679,
"name": "menu-up"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 257
},
{
"icon": {
"paths": [
"M708 366l60 60-256 256-256-256 60-60 196 196z"
],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"expand_more"
],
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 257,
"order": 107,
"ligatures": "expand_more",
"prevSize": 32,
"code": 59680,
"name": "menu-down"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 258
},
{ {
"icon": { "icon": {
"paths": [ "paths": [

View File

@ -120,28 +120,32 @@
</li> </li>
</ul> </ul>
</span> </span>
<a class="button icon-contactList" id="toolbar_contact_list" data-container="body" data-toggle="popover" data-placement="right" shortcut="contactlistpopover" data-i18n="[content]bottomtoolbar.contactlist" content="Open / close contact list"> <a class="button icon-contactList" id="toolbar_contact_list" shortcut="contactlistpopover">
<span id="numberOfParticipants"></span> <span class="badge-round">
<span id="numberOfParticipants"></span>
</span>
</a> </a>
<!--a class="button icon-link" id="toolbar_button_link" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.invite" content="Invite others"></a--> <!--a class="button icon-link" id="toolbar_button_link"></a-->
<a class="button icon-chat" id="toolbar_button_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="right" data-i18n="[content]toolbar.chat" content="Open / close chat"> <a class="button icon-chat" id="toolbar_button_chat" shortcut="toggleChatPopover">
<span id="unreadMessages"></span> <span class="badge-round">
<span id="unreadMessages"></span>
</span>
</a> </a>
<a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="right" style="display: none"></a> <a class="button" id="toolbar_button_record" style="display: none"></a>
<a class="button icon-security" id="toolbar_button_security" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.lock" content="Lock / unlock room"></a> <a class="button icon-security" id="toolbar_button_security"></a>
<a class="button icon-share-doc" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="right" content="Shared document" data-i18n="[content]toolbar.etherpad"></a> <a class="button icon-share-doc" id="toolbar_button_etherpad"></a>
<a class="button icon-shared-video" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="right" content="Share a YouTube video" data-i18n="[content]toolbar.sharedvideo" style="display: none"> <a class="button icon-shared-video" id="toolbar_button_sharedvideo" style="display: none">
<ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup"> <ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup">
<li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li> <li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
</ul> </ul>
</a> </a>
<a class="button icon-telephone" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="right" content="Call SIP number" data-i18n="[content]toolbar.sip" style="display: none"></a> <a class="button icon-telephone" id="toolbar_button_sip" style="display: none"></a>
<a class="button icon-dialpad" id="toolbar_button_dialpad" data-container="body" data-toggle="popover" data-placement="right" content="Open dialpad" data-i18n="[content]toolbar.dialpad" style="display: none"></a> <a class="button icon-dialpad" id="toolbar_button_dialpad" style="display: none"></a>
<a class="button icon-settings" id="toolbar_button_settings" data-container="body" data-toggle="popover" data-placement="right" content="Settings" data-i18n="[content]toolbar.Settings"></a> <a class="button icon-settings" id="toolbar_button_settings"></a>
<a class="button icon-raised-hand" id="toolbar_button_raisehand" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.raiseHand" content="Raise Hand" shortcut="raiseHandPopover"></a> <a class="button icon-raised-hand" id="toolbar_button_raisehand" shortcut="raiseHandPopover"></a>
<a class="button icon-full-screen" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="right" shortcut="toggleFullscreenPopover" data-i18n="[content]toolbar.fullscreen" content="Enter / Exit Full Screen"></a> <a class="button icon-full-screen" id="toolbar_button_fullScreen" shortcut="toggleFullscreenPopover"></a>
<a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" data-toggle="popover" shortcut="filmstripPopover" data-placement="right" data-i18n="[content]toolbar.filmstrip" content="Show / hide videos"></a> <a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" shortcut="filmstripPopover"></a>
<a class="button icon-feedback" id="feedbackButton" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]feedback"></a> <a class="button icon-feedback" id="feedbackButton"></a>
<div id="sideToolbarContainer"> <div id="sideToolbarContainer">
<div id="profile_container" class="sideToolbarContainer__inner"> <div id="profile_container" class="sideToolbarContainer__inner">
<div class="title" data-i18n="profile.title"></div> <div class="title" data-i18n="profile.title"></div>
@ -208,13 +212,11 @@
<input type="checkbox" id="followMeCheckBox"> <input type="checkbox" id="followMeCheckBox">
<label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label> <label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label>
</div> </div>
<a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="icon-download"></i></a>
</div> </div>
</div> </div>
</div> </div>
<div id="videospace"> <div id="videospace">
<div id="largeVideoContainer" class="videocontainer"> <div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="sharedVideo"><div id="sharedVideoIFrame"></div></div> <div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
<div id="etherpad"></div> <div id="etherpad"></div>
<a target="_new"><div class="watermark leftwatermark"></div></a> <a target="_new"><div class="watermark leftwatermark"></div></a>
@ -223,13 +225,14 @@
<span data-i18n="poweredby"></span> jitsi.org <span data-i18n="poweredby"></span> jitsi.org
</a> </a>
<div id="dominantSpeaker"> <div id="dominantSpeaker">
<div class="dynamic-shadow"></div>
<img id="dominantSpeakerAvatar" src=""/> <img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas>
</div> </div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper"> <div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video> <video id="largeVideo" muted="true" autoplay></video>
</div> </div>
<span id="videoConnectionMessage"></span> <span id="localConnectionMessage"></span>
<span id="videoResolutionLabel">HD</span> <span id="videoResolutionLabel">HD</span>
<span id="recordingLabel" class="centeredVideoLabel"> <span id="recordingLabel" class="centeredVideoLabel">
<span id="recordingLabelText"></span> <span id="recordingLabelText"></span>
@ -238,12 +241,12 @@
</div> </div>
<div id="remoteVideos"> <div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer"> <span id="localVideoContainer" class="videocontainer videocontainer_small">
<span id="localVideoWrapper"> <span id="localVideoWrapper">
<!--<video id="localVideo" autoplay muted></video> - is now per stream generated --> <!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
</span> </span>
<audio id="localAudio" autoplay muted></audio> <audio id="localAudio" autoplay muted></audio>
<span class="focusindicator"></span> <div class="videocontainer__toolbar"></div>
</span> </span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio> <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio> <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
@ -257,5 +260,6 @@
</ul> </ul>
</div> </div>
</div> </div>
<div id="aui-feedback-dialog" class="dialog feedback aui-layer aui-dialog2 aui-dialog2-medium" style="display: none;"></div>
</body> </body>
</html> </html>

View File

@ -34,5 +34,14 @@ var interfaceConfig = {
filmStripOnly: false, filmStripOnly: false,
RANDOM_AVATAR_URL_PREFIX: false, RANDOM_AVATAR_URL_PREFIX: false,
RANDOM_AVATAR_URL_SUFFIX: false, RANDOM_AVATAR_URL_SUFFIX: false,
FILM_STRIP_MAX_HEIGHT: 120 FILM_STRIP_MAX_HEIGHT: 120,
LOCAL_THUMBNAIL_RATIO_WIDTH: 16,
LOCAL_THUMBNAIL_RATIO_HEIGHT: 9,
REMOTE_THUMBNAIL_RATIO_WIDTH: 1,
REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
// Enables feedback star animation.
ENABLE_FEEDBACK_ANIMATION: false,
DISABLE_FOCUS_INDICATOR: false,
AUDIO_LEVEL_PRIMARY_COLOR: "rgba(255,255,255,0.7)",
AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.4)"
}; };

View File

@ -7,7 +7,9 @@
"hy": "Armenisch", "hy": "Armenisch",
"it": "Italienisch", "it": "Italienisch",
"oc": "Okzitanisch", "oc": "Okzitanisch",
"pl": "Polnisch",
"ptBR": "Portugiesisch (Brasilien)", "ptBR": "Portugiesisch (Brasilien)",
"ru": "Russisch",
"sk": "Slowakisch", "sk": "Slowakisch",
"sl": "Slowenisch", "sl": "Slowenisch",
"sv": "Schwedisch", "sv": "Schwedisch",

15
lang/languages-pl.json Normal file
View File

@ -0,0 +1,15 @@
{
"en": "Angielski",
"bg": "Bułgarski",
"de": "Niemiecki",
"es": "Hiszpański",
"fr": "Francuski",
"hy": "Ormiański",
"it": "Włoski",
"oc": "Prowansalski",
"ptBR": "portugalski (brazylijski)",
"sk": "Słowacki",
"sl": "Słoweński",
"sv": "Szwedzki",
"tr": "Turecki"
}

View File

@ -7,7 +7,9 @@
"hy": "Armênio", "hy": "Armênio",
"it": "Italiano", "it": "Italiano",
"oc": "Provençal", "oc": "Provençal",
"pl": "Polonês",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ru": "Russo",
"sk": "Eslovaco", "sk": "Eslovaco",
"sl": "Esloveno", "sl": "Esloveno",
"sv": "Sueco", "sv": "Sueco",

View File

@ -8,7 +8,9 @@
"hy": "Armenian", "hy": "Armenian",
"it": "Italian", "it": "Italian",
"oc": "Occitan", "oc": "Occitan",
"pl": "Polish",
"ptBR": "Portuguese (Brazil)", "ptBR": "Portuguese (Brazil)",
"ru": "Russian",
"sk": "Slovak", "sk": "Slovak",
"sl": "Slovenian", "sl": "Slovenian",
"sv": "Swedish", "sv": "Swedish",

View File

@ -1,17 +1,15 @@
{ {
"contactlist": "Kontaktliste", "contactlist": "Im Gespräch",
"connectionsettings": "Verbindungseinstellungen", "connectionsettings": "Verbindungseinstellungen",
"poweredby": "Betrieben von", "poweredby": "Betrieben von",
"downloadlogs": "Log herunterladen",
"feedback": "Wir freuen uns auf Ihr Feedback!", "feedback": "Wir freuen uns auf Ihr Feedback!",
"roomUrlDefaultMsg": "Die Konferenz wird erstellt...", "roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
"participant": "Teilnehmer",
"me": "ich", "me": "ich",
"speaker": "Sprecher", "speaker": "Sprecher",
"raisedHand": "Möchte sprechen", "raisedHand": "Möchte sprechen",
"defaultNickname": "Bsp: Heidi Blau", "defaultNickname": "Bsp: Heidi Blau",
"defaultLink": "Bsp.: __url__", "defaultLink": "Bsp.: __url__",
"calling": "Rufe __name__ an...", "callingName": "__name__",
"userMedia": { "userMedia": {
"react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>", "react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
"chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>", "chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
@ -27,7 +25,7 @@
"raiseHand": "Heben Sie Ihre Hand.", "raiseHand": "Heben Sie Ihre Hand.",
"pushToTalk": "Drücken um zu sprechen.", "pushToTalk": "Drücken um zu sprechen.",
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.", "toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.",
"toggleFilmstrip": "Videovorschau anzeigen oder verstecken.", "toggleFilmstrip": "Videos anzeigen oder verbergen.",
"toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.", "toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.",
"focusLocal": "Lokales Video fokussieren.", "focusLocal": "Lokales Video fokussieren.",
"focusRemote": "Andere Videos fokussieren.", "focusRemote": "Andere Videos fokussieren.",
@ -37,7 +35,7 @@
}, },
"welcomepage": { "welcomepage": {
"go": "Los", "go": "Los",
"roomname": "Raumnamen eingeben", "roomname": "Konferenzname eingeben",
"disable": "Diesen Hinweis nicht mehr anzeigen", "disable": "Diesen Hinweis nicht mehr anzeigen",
"feature1": { "feature1": {
"title": "Einfach zu benutzen", "title": "Einfach zu benutzen",
@ -49,7 +47,7 @@
}, },
"feature3": { "feature3": {
"title": "Open Source", "title": "Open Source",
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäß dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten." "content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
}, },
"feature4": { "feature4": {
"title": "Unbegrenzte Anzahl Benutzer", "title": "Unbegrenzte Anzahl Benutzer",
@ -76,16 +74,16 @@
"mute": "Stummschaltung aktivieren / deaktivieren", "mute": "Stummschaltung aktivieren / deaktivieren",
"videomute": "Kamera starten / stoppen", "videomute": "Kamera starten / stoppen",
"authenticate": "Anmelden", "authenticate": "Anmelden",
"lock": "Raum schützen / Schutz aufheben", "lock": "Konferenz schützen / Schutz aufheben",
"invite": "Andere einladen", "invite": "Andere einladen",
"chat": "Chat öffnen / schließen", "chat": "Chat öffnen / schliessen",
"etherpad": "Geteiltes Dokument", "etherpad": "Dokument teilen",
"sharedvideo": "Ein YouTube-Video teilen", "sharedvideo": "YouTube-Video teilen",
"sharescreen": "Bildschirm freigeben", "sharescreen": "Bildschirm freigeben",
"fullscreen": "Vollbildmodus aktivieren / deaktivieren", "fullscreen": "Vollbildmodus aktivieren / deaktivieren",
"sip": "SIP Nummer anrufen", "sip": "SIP Nummer anrufen",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"hangup": "Auflegen", "hangup": "Konferenz verlassen",
"login": "Anmelden", "login": "Anmelden",
"logout": "Abmelden", "logout": "Abmelden",
"dialpad": "Tastenblock anzeigen", "dialpad": "Tastenblock anzeigen",
@ -93,17 +91,19 @@
"micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.", "micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.",
"unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.", "unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
"cameraDisabled": "Keine Kamera verfügbar", "cameraDisabled": "Keine Kamera verfügbar",
"micDisabled": "Kein Mikrofon verfügbar" "micDisabled": "Kein Mikrofon verfügbar",
"filmstrip": "Videos anzeigen / verbergen",
"raiseHand": "Hand erheben um zu sprechen"
}, },
"bottomtoolbar": { "bottomtoolbar": {
"chat": "Chat öffnen / schließen", "chat": "Chat öffnen / schliessen",
"filmstrip": "Videovorschau anzeigen / verstecken", "filmstrip": "Videos anzeigen / verbergen",
"contactlist": "Kontaktliste öffnen / schließen" "contactlist": "Kontaktliste öffnen / schliessen"
}, },
"chat": { "chat": {
"nickname": { "nickname": {
"title": "Nickname im Eingabefeld eingeben", "title": "Name eingeben",
"popover": "Einen Namen auswählen" "popover": "Name"
}, },
"messagebox": "Text eingeben..." "messagebox": "Text eingeben..."
}, },
@ -111,20 +111,29 @@
"title": "Einstellungen", "title": "Einstellungen",
"update": "Aktualisieren", "update": "Aktualisieren",
"name": "Name", "name": "Name",
"startAudioMuted": "Stumm beitreten", "startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
"startVideoMuted": "Ohne Video beitreten", "startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
"selectCamera": "Kamera auswählen", "selectCamera": "Kamera",
"selectMic": "Mikrofon auswählen", "selectMic": "Mikrofon",
"selectAudioOutput": "Audio-Ausgabe auswählen", "selectAudioOutput": "Audioausgabe",
"followMe": "Follow-me aktivieren", "followMe": "Follow-me für alle Teilnehmer",
"noDevice": "Kein", "noDevice": "Kein",
"noPermission": "Keine Berechtigung um das Gerät zu verwenden", "noPermission": "Keine Berechtigung um das Gerät zu verwenden",
"avatarUrl": "Avatar URL" "cameraAndMic": "Kamera und Mikrofon",
"moderator": "MODERATOR",
"password": "PASSWORT SETZEN",
"audioVideo": "AUDIO UND VIDEO",
"setPasswordLabel": "Konferenz mit einem Passwort schützen."
},
"profile": {
"title": "PROFIL",
"setDisplayNameLabel": "Anzeigename festlegen",
"setEmailLabel": "E-Mail Adresse für Gravatar"
}, },
"videothumbnail": { "videothumbnail": {
"editnickname": "Klicken, um den Anzeigenamen zu bearbeiten", "editnickname": "Klicken, um den Anzeigenamen zu bearbeiten",
"moderator": "Besitzer dieser Konferenz", "moderator": "Besitzer dieser Konferenz",
"videomute": "Teilnehmer hat die Kamera pausiert.", "videomute": "Teilnehmer hat die Kamera pausiert",
"mute": "Teilnehmer ist stumm geschaltet", "mute": "Teilnehmer ist stumm geschaltet",
"kick": "Hinauswerfen", "kick": "Hinauswerfen",
"muted": "Stummgeschaltet", "muted": "Stummgeschaltet",
@ -172,6 +181,7 @@
"connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.", "connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
"connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__", "connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__",
"connecting": "Verbindung wird hergestellt", "connecting": "Verbindung wird hergestellt",
"copy": "Kopieren",
"error": "Fehler", "error": "Fehler",
"detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.", "detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
"failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.", "failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
@ -183,8 +193,8 @@
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.", "lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
"warning": "Warnung", "warning": "Warnung",
"passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.", "passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
"sorry": "Entschuldigung", "internalErrorTitle": "Interner Fehler",
"internalError": "Interner Anwendungsfehler [setRemoteDescription]", "internalError": "Ups! Es ist etwas schiefgegangen. Der Fehler [setRemoteDescription] ist aufgetreten.",
"unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.", "unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
"SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)", "SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
"SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)", "SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
@ -206,7 +216,7 @@
"logoutTitle": "Abmelden", "logoutTitle": "Abmelden",
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?", "logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
"sessTerminated": "Sitzung beendet", "sessTerminated": "Sitzung beendet",
"hungUp": "Anruf beendet", "hungUp": "Konferenz beendet",
"joinAgain": "Erneut beitreten", "joinAgain": "Erneut beitreten",
"Share": "Teilen", "Share": "Teilen",
"Save": "Speichern", "Save": "Speichern",
@ -215,27 +225,29 @@
"Dial": "Wählen", "Dial": "Wählen",
"sipMsg": "Geben Sie eine SIP Nummer ein", "sipMsg": "Geben Sie eine SIP Nummer ein",
"passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?", "passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
"passwordMsg": "Passwort setzen, um den Raum zu schützen", "passwordMsg": "Passwort setzen um die Konferenz zu schützen",
"Invite": "Einladen", "shareLink": "Diesen Link kopieren und teilen",
"shareLink": "Teilen Sie diesen Link mit jedem den Sie einladen möchten",
"settings1": "Konferenz einrichten", "settings1": "Konferenz einrichten",
"settings2": "Teilnehmer treten stummgeschaltet bei", "settings2": "Teilnehmer treten stummgeschaltet bei",
"settings3": "Nickname erforderlich<br/><br/>Setzen Sie ein Passwort, um den Raum zu schützen:", "settings3": "Name erforderlich<br/><br/>Setzen Sie ein Passwort, um die Konferenz zu schützen:",
"yourPassword": "Ihr Passwort", "yourPassword": "Neues Passwort eingeben",
"Back": "Zurück", "Back": "Zurück",
"serviceUnavailable": "Dienst nicht verfügbar", "serviceUnavailable": "Dienst nicht verfügbar",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.", "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"Yes": "Ja", "Yes": "Ja",
"reservationError": "Fehler im Reservationssystem", "reservationError": "Fehler im Reservationssystem",
"reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__", "reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
"password": "Passwort", "password": "Passwort eingeben",
"userPassword": "Benutzerpasswort", "userPassword": "Benutzerpasswort",
"token": "Token", "token": "Token",
"tokenAuthFailed": "Anmeldung am XMPP-Server fehlgeschlagen: ungültiges Token", "tokenAuthFailedTitle": "Authentifizierungsfehler",
"tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
"displayNameRequired": "Geben Sie Ihren Anzeigenamen ein", "displayNameRequired": "Geben Sie Ihren Anzeigenamen ein",
"extensionRequired": "Erweiterung erforderlich:", "extensionRequired": "Erweiterung erforderlich:",
"firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.", "firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
"feedbackQuestion": "Wie war der Anruf?", "rateExperience": "Bitte bewerten Sie diese Konferenz.",
"feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
"feedbackQuestion": "Anmerkungen zur Konferenz.",
"thankYou": "Danke für die Verwendung von __appName__!", "thankYou": "Danke für die Verwendung von __appName__!",
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?", "sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
"liveStreaming": "Live-Streaming", "liveStreaming": "Live-Streaming",
@ -253,12 +265,17 @@
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.", "cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.", "cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
"cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.", "cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
"cameraNotFoundError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.", "cameraNotFoundError": "Kamera nicht gefunden.",
"cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.", "cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.", "micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.", "micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
"micNotFoundError": "Das angeforderte Mikrofon konnte nicht gefunden werden.", "micNotFoundError": "Mikrofon nicht gefunden.",
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht." "micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
"micNotSendingData": "Das Mikrofon kann nicht verwendet werden. Bitte wählen Sie ein anderes Mikrofon in den Einstellungen oder laden Sie die Konferenz neu.",
"cameraNotSendingData": "Die Kamera kann nicht verwendet werden. Bitte wählen Sie eine andere Kamera in den Einstellungen oder laden Sie die Konferenz neu.",
"goToStore": "Zum Store",
"externalInstallationTitle": "Erweiterung erforderlich",
"externalInstallationMsg": "Die Bildschirmfreigabeerweiterung muss installiert werden."
}, },
"\u0005dialog": {}, "\u0005dialog": {},
"email": { "email": {

344
lang/main-pl.json Normal file
View File

@ -0,0 +1,344 @@
{
"contactlist": "w trakcie rozmowy",
"connectionsettings": "ustawienia połączenia",
"poweredby": "Uruchomiono",
"feedback": "jaka jest twoja opinia ?",
"roomUrlDefaultMsg": "otwarto twoją konferencję",
"me": "to ja",
"speaker": "głośnik",
"raisedHand": "Chcesz się odezwać ?",
"defaultNickname": "np. Ziutek Kowalski",
"defaultLink": "np. _url_",
"callingName": "_nazwa_",
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Share Selected Device</i></b> przycisk",
"operaGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Allow</i></b> przycisk",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>OK</i></b> przycisk",
"nwjsGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu"
},
"keyboardShortcuts": {
"keyboardShortcuts": "Skróty klawiaturowe:",
"raiseHand": "Unieś rękę.",
"pushToTalk": "naciśnij i mów",
"toggleScreensharing": "Przełączanie pomiędzy kamerą i wspóldzieleniem ekranu",
"toggleFilmstrip": "Pokaż lub ukryj klipy wideo",
"toggleShortcuts": "Pokaż lub ukryj pasek pomocy.",
"focusLocal": "Przełącz na lokalne wideo.",
"focusRemote": "Przełącz na któreś ze zdalnych wideo.",
"toggleChat": "Otwórz lub zamknij panel czat.",
"mute": "Wyłącz lub włącz mikrofon.",
"videoMute": "Start lub stop lokalne wideo."
},
"welcomepage": {
"go": "IDŹ",
"roomname": "Podaj nazwę sali konferencyjnej",
"disable": "nie pokazuj ponownie",
"feature1": {
"title": "użyj",
"content": "nie musisz nic pobierać. _app_ jest gotowa do użycia bezpośrednio w przeglądarce. Zaproś innych do udziału w konferencji podając adres URL"
},
"feature2": {
"title": "za mała przepustowość",
"content": "Dla konferencji video potrzeba nie więcej niż 128 kbit/sek. Konferencje dzielenia ekranu lub tylko audio są możliwe przy mniejszej przepustowości. "
},
"feature3": {
"title": "Open source",
"content": "_app_ oparta jest na Apache License. Możesz swobodnie pobierać ją, używać, modyfikować i dzielić się nią."
},
"feature4": {
"title": "Nieograniczona liczba użytkowników",
"content": "Liczba użytkowników czy uczestników konferencji nie jest ograniczona. Determinuje ją moc serwera i dostępna przepustowość lącza."
},
"feature5": {
"title": "Współdzielenie ekranu",
"content": "Z łatwością podzielisz się ekranem z innymi. _app_ jest idealnym narzędziem do prezentacji, nauczania i udzielania zdalnej pomocy technicznej."
},
"feature6": {
"title": "Sale bezpieczne.",
"content": "Potrzebujesz prywatności? _app_ sale konferencyjne mogą być zabezpieczone hasłami niedopuszczającymi niezaproszonych uczestników czy też osoby chcące zakłócić konferencję."
},
"feature7": {
"title": "Współdzielenie uwag.",
"content": "_app_ zawiera Etherpad, współdzielony edytor tekstu doskonały dla redakcji zespołowych artykułów czy komentarzy."
},
"feature8": {
"title": "Statystyki użycia.",
"content": "Analizuj uczestników konferencji z łatwościa integrując dane z Piwik i Google Analitics i innymi systemami monitorującymi."
}
},
"toolbar": {
"mute": "Wycisz / Pogłośnij",
"videomute": "Kamera start / stop ",
"authenticate": "Uwierzytelnianie",
"lock": "Zamknij / Otwórz salę",
"invite": "Zaproś innych",
"chat": "",
"etherpad": "Udostępniaj dokument",
"sharedvideo": "Udostępniaj wideo w Youtube",
"sharescreen": "Udostępnij ekran",
"fullscreen": "Otwórz / Zamknij pełny ekran",
"sip": "Wykręć numer SIP",
"Settings": "",
"hangup": "Rozłącz",
"login": "Zaloguj",
"logout": "",
"dialpad": "Wyświetl panel wybierania",
"sharedVideoMutedPopup": "Współdzielone wideo zostało wyciszone i <br/> możesz zacząć rozmawiać z innymi.",
"micMutedPopup": "Mikrofon został wyłączony i <br/> możesz spokojnie konsumować współdzielone wideo",
"unableToUnmutePopup": "Nie możesz pogłośnić audio podczas współużytkowania wideo",
"cameraDisabled": "Kamera nie jest dostępna",
"micDisabled": "Mikrofon nie jest dostępny",
"filmstrip": "",
"raiseHand": "Podnieś rękę chcąc zabrać głos"
},
"bottomtoolbar": {
"chat": "Otwórz / Zamknij Czat",
"filmstrip": "Pokaż / Ukryj klipy wideo",
"contactlist": "Otwórz / Zamknij spis kontaktów"
},
"chat": {
"nickname": {
"title": "Podaj swój nick poniżej",
"popover": "Wybierz swój nick"
},
"messagebox": "Umieść tekst...."
},
"settings": {
"title": "Ustawienia",
"update": "Aktualizacja",
"name": "Nazwa",
"startAudioMuted": "Wszyscy się wyciszyli",
"startVideoMuted": "Wszyscy się ukryli",
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"selectAudioOutput": "Wyjście audio",
"followMe": "Wszyscy za mną",
"noDevice": "Brak",
"noPermission": "Nie ma zgody na użycie urządzenia",
"cameraAndMic": "Kamera i Mikrofon",
"moderator": "MODERATOR",
"password": "USTAW HASŁO",
"audioVideo": "AUDIO I WIDEO",
"setPasswordLabel": "Zamknij salę konferencyjną z hasłem"
},
"profile": {
"title": "PROFIL",
"setDisplayNameLabel": "Podaj swoją wyświetlaną nazwę",
"setEmailLabel": "Ustaw email swojego gravatara"
},
"videothumbnail": {
"editnickname": "Kliknij <br/>celem edycji swojej nazwy",
"moderator": "Gospodarz <br/>tej konferencji",
"videomute": "Uczestnik <br/>wyłączyl kamerę",
"mute": "Uczestnik ma wyciszone audio",
"kick": "Kick out",
"muted": "Wyciszony",
"domute": "Wyciszenie",
"flip": "Odwrócenie"
},
"connectionindicator": {
"bitrate": "Szybkość transmisji:",
"packetloss": "Strata pakietów:",
"resolution": "Rozdzielczość:",
"less": "Pokaż mniej",
"more": "Pokaż więcej",
"address": "Adres:",
"remoteport": "Zdalny port:Zdalne porty:",
"remoteport_plural_2": "",
"remoteport_plural_5": "",
"localport": "Lokalny port:Lokalne porty:",
"localport_plural_2": "",
"localport_plural_5": "",
"localaddress": "Lokalny adres:Lokalne Adresy:",
"localaddress_plural_2": "",
"localaddress_plural_5": "",
"remoteaddress": "Zdalny adres:Zdalne adresy:",
"remoteaddress_plural_2": "",
"remoteaddress_plural_5": "",
"transport": "Przekazywanie:",
"bandwidth": "Zakładana przepustowość:",
"na": "Po informację o połączeniu wróć gdy wystartuje konferencja"
},
"notify": {
"disconnected": "rozłączone",
"moderator": "Prawa moderatora przydzielone!",
"connected": "połączono",
"somebody": "Ktoś",
"me": "To ja",
"focus": "Fokus konferencji",
"focusFail": "_składnik_nie dostępny - zastosuj w _ms_sek",
"grantedTo": "Prawa moderatora przyznane _to_!",
"grantedToUnknown": "Prawa Moderatora przyznane $t(somebody)!",
"muted": "Masz wyciszony mikrofon",
"mutedTitle": "Jesteś wyciszony!",
"raisedHand": "Możesz mówić."
},
"dialog": {
"kickMessage": "Ocho! Zostałeś wyproszony z konferencji!",
"popupError": "Twoja przeglądarka blokuje wyskakujące okienka z tej witryny. Proszę, zmień w ustawieniach przeglądarki.",
"passwordError": "Ta konwersacja aktualnie jest zabezpieczona hasłem. Tylko gospodarz konferencji może zakładać hasło.",
"passwordError2": "Ta rozmowa nie jest zabezpieczona hasłem. Tylko gospodarz konferencji może ustanowić hasło zabezpieczające.",
"connectError": "Ocho! Cos poszło nie tak, nie można podłaczyć się do tej konferencji.",
"connectErrorWithMsg": "Ocho! Coś poszło nie tak i nie można podłączyć się do tej konferencji:_msg_",
"connecting": "",
"copy": "Kopiuj",
"error": "",
"detectext": "Błąd podczas rozpoznania rozszerzenia wspóldzielenia ekranu.",
"failtoinstall": "Instalacja współdzielenia ekranu nie powiodła się.",
"failedpermissions": "Brak akceptacji dla użycia kamery i mikrofonu",
"bridgeUnavailable": "Jitsi Videobridge aktualnie jest niedostępne. Proszę, spróbuj później!",
"jicofoUnavailable": "Jicofo jest aktualnie niedostępne. Proszę, spróbuj później!",
"maxUsersLimitReached": "Osiągnięto max liczbę uczestników konferencji. Proszę spróbuj później! ",
"lockTitle": "Nie powiodło się zabezpieczenie konferencji",
"lockMessage": "Zabezpieczenie konferencji nie powiodło się.",
"warning": "Uwaga",
"passwordNotSupported": "Hasła sali konferencyjnych są aktualnie niedostępne.",
"internalErrorTitle": "Błąd wewnętrzny",
"internalError": "Ocho! coś poszło nie tak. Wystąpił błąd: [setRemoteDescription]",
"unableToSwitch": "Nie można przełaczyć na strumień wideo",
"SLDFailure": "Ocho! Coś poszło nie tak i nie można wyciszyć! (SLD Failure)",
"SRDFailure": "Ocho! Coś poszło nie tak i nie można zatrzymać wideo! (SRD Failure)",
"oops": "Ups",
"defaultError": "Wystąpił jakiś błąd",
"passwordRequired": "Wymagane hasło",
"Ok": "Ok",
"Remove": "Usuń",
"shareVideoTitle": "Współdziel wideo",
"shareVideoLinkError": "Podaj proszę prawidłowy link youtube.",
"removeSharedVideoTitle": "Usuń wideo współdzielone",
"removeSharedVideoMsg": "Na pewno chcesz usunąć współdzielone wideo?",
"alreadySharedVideoMsg": "Inny uczestnik aktualnie współdzieli wideo. W tej konferencji tylko jedno wideo może być współdzielone.",
"WaitingForHost": "Oczekiwanie na komputer",
"WaitForHostMsg": "Konferencja <b>_room_</b> jeszcze nie wystartowała. Jeśli jesteś gospodarzem podaj dane autentykacji. Jeśli nie czekaj na gospodarza.",
"IamHost": "Jestem gospodarzem",
"Cancel": "Anuluj",
"retry": "Ponów",
"logoutTitle": "Wyloguj",
"logoutQuestion": "Na pewno chcesz się wylogować i zakończyć konferencję?",
"sessTerminated": "Sesja zakończona",
"hungUp": "Przerwałeś połączenie",
"joinAgain": "Ponownie przystąp",
"Share": "Współdziel",
"Save": "Zapisz",
"recording": "",
"recordingToken": "Proszę podać token nagrywania",
"Dial": "Dzwoń",
"sipMsg": "Podaj numer SIP",
"passwordCheck": "Czy na pewno chcesz usunąć swoje hasło ?",
"passwordMsg": "Podaj hasło aby zabezpieczyć salę konferencyjną",
"shareLink": "Skopiuj i udostępnij ten link",
"settings1": "Skonfiguruj swoją konferencję",
"settings2": "Wyciszenie współuczestników",
"settings3": "Wymagane nicki <br/><br/>Wprowadź hasło dla zabezpieczenia sali konferencyjnej:",
"yourPassword": "Proszę wprowadzić nowe hasło",
"Back": "Wstecz",
"serviceUnavailable": "Usługa jest niedostępna",
"gracefulShutdown": "Aktualnie serwis jest konserwowany. Prosze spróbować później.",
"Yes": "Tak",
"reservationError": "Błąd systemu rezerwacji",
"reservationErrorMsg": "Kod błędu: _code_, treść: _msg_",
"password": "Podaj hasło",
"userPassword": "hasło użytkownika",
"token": "token",
"tokenAuthFailedTitle": "Problem uwierzytelnienia",
"tokenAuthFailed": "Przepraszam, ale nie jesteś upoważniony do uczestnictwa w tym połączeniu",
"displayNameRequired": "Wprowadź swoją nazwę użytkownika",
"extensionRequired": "Wymagane jest rozszerzenie:",
"firefoxExtensionPrompt": "Potrzebujesz zainstalować rozszerzenie firefox aby móc współdzielić ekran. Spróbuj ponownie później <a href='__url__'>weź z</a>!",
"rateExperience": "Oceń proszę swoje doświadczenia z konferencji.",
"feedbackHelp": "Twoja opinia będzie pomocna w usprawnieniu naszego serwisu.",
"feedbackQuestion": "Powiedz nam o twoim połączeniu!",
"thankYou": "Dziękujemy Ci za używanie _appName_!",
"sorryFeedback": "Przykro nam to słyszeć. Czy możesz powiedzieć więcej na ten temat?",
"liveStreaming": "",
"streamKey": "Nazwa strumienia/klucz",
"startLiveStreaming": "Uruchom strumień live",
"stopStreamingWarning": "Czy jesteś pewny, że chcesz zatrzymać ten strumień live?",
"stopRecordingWarning": "Naprawdę chcesz zatrzymać nagrywanie?",
"stopLiveStreaming": "Zatrzymaj transmisję live",
"stopRecording": "Zatrzymaj nagrywanie",
"doNotShowWarningAgain": "Nie pokazuj tego ostrzeżenia ponownie",
"permissionDenied": "Brak uprawnień",
"screenSharingPermissionDeniedError": "Nie posiadasz uprawnień do współdzielenia ekranu.",
"micErrorPresent": "Wystąpił błąd w dostępie do mikrofonu.",
"cameraErrorPresent": "Wystąpił błąd w dostępie do twojej kamery.",
"cameraUnsupportedResolutionError": "Twoja kamera nie obsługuje wymaganej rozdzielczości.",
"cameraUnknownError": "Z nieznanej przyczyny nie można użyć kamery ",
"cameraPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojej kamery. Nadal możesz włączyć się do konferencji ale inni nie będą cię widzieli. Naciśnij przycisk kamera w pasku menu aby użyć właściwą kamerę. ",
"cameraNotFoundError": "Kamera nie znaleziona.",
"cameraConstraintFailedError": "Twoja kamera nie spełnia wymagań.",
"micUnknownError": "Z przyczyn nieznanych nie można użyć mikrofonu. ",
"micPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojego mikrofonu. Nadal możesz uczestniczyc w konferencji ale inni nie będą cię słyszeli. Użyj przycisku kamera aby to naprawić.",
"micNotFoundError": "Mikrofon nie jest odnaleziony.",
"micConstraintFailedError": "Twój mikrofon nie obsługuje wymaganych parametrów.",
"micNotSendingData": "Nie możemy mieć dostępu do twojego mikrofonu. Proszę, wskaż inne urządzenie lub przeładuj aplikację.",
"cameraNotSendingData": "Nie możemy mieć dostępu do twojej kamery. Sprawdź czy inna aplikacja nie używa twojej kamery, wybierz inne urządzenie lub ponownie uruchom aplikację.",
"goToStore": "Idź do sklepu",
"externalInstallationTitle": "Wymagane rozszerzenie",
"externalInstallationMsg": "Zainstaluj rozszerzenie naszego współdzielenia ekranu."
},
"email": {
"sharedKey": [
"Ta konferencja jest zabezpieczona hasłem. Aby się podłączyć proszę zastosuj następujący pin:",
"",
"",
"_sharedKey_",
"",
" "
],
"subject": "Zaproszenie do a_appName_(_conferenceName_)",
"body": [
"Witaj, I%27 zaprasza cię do udziału w konferencji_appName_.",
"",
"",
"Kliknij na poniższy link aby uczestniczyć w konferencji.",
"",
"",
"_roomUrl_",
"",
"",
"_sharedKeyTex_",
"Zauważ, że -appName_ możesz używać tylko przy pomocy _supportedBrowsers_.",
"",
"",
"Polączymy się błyskawicznie! "
],
"and": "i"
},
"connection": {
"ERROR": "Błąd",
"CONNECTING": "Nawiązywanie połączenia",
"RECONNECTING": "Wystąpił problem w sieci. Ponowienie połaczenia....",
"CONNFAIL": "Połączenie się nie powiodło",
"AUTHENTICATING": "Uwierzytelnianie",
"AUTHFAIL": "Uwierzytelnianie nie powiodło się",
"CONNECTED": "Połączono",
"DISCONNECTED": "Rozłączony",
"DISCONNECTING": "Rozłączanie",
"ATTACHED": "Załącznik"
},
"recording": {
"pending": "Nagrywanie oczekiwanie na uczestników konferencji.....",
"on": "Nagrywanie",
"off": "Nagrywanie zatrzymane",
"failedToStart": "Nagrywanie nie jest możliwe",
"buttonTooltip": "Nagrywanie start / stop",
"error": "Nagranie się nie powiodło. Proszę, spróbuj ponownie.",
"unavailable": "Serwis nagrywania jest aktualnie niedostępny. Proszę, spróbować później."
},
"liveStreaming": {
"pending": "Start strumieniowania live...",
"on": "Strumień live",
"off": "Strumieniowanie live zastopowane",
"unavailable": "Strumieniowanie live aktualnie jest niedostepne. Proszę spróbować później.",
"failedToStart": "Strumieniowanie live nie powiodło się",
"buttonTooltip": "Strumieniowanie live start / stop",
"streamIdRequired": "Proszę podaj id strumieniowania aby uruchomić live.",
"error": "Strumieniowanie live nie powiodło się. Spróbuj później.",
"busy": "Wszystkie nagrywarki są zajęte. Proszę, sprawdź ponownie później."
}
}

View File

@ -1,17 +1,15 @@
{ {
"contactlist": "LISTA DE CONTATO", "contactlist": "Na chamada",
"connectionsettings": "Configurações de conexão", "connectionsettings": "Configurações de conexão",
"poweredby": "distribuído por", "poweredby": "distribuído por",
"downloadlogs": "Baixar registros",
"feedback": "Dê seus comentários", "feedback": "Dê seus comentários",
"roomUrlDefaultMsg": "Sua conferência está sendo criado...", "roomUrlDefaultMsg": "Sua conferência está sendo criado...",
"participant": "Participante",
"me": "eu", "me": "eu",
"speaker": "Orador", "speaker": "Orador",
"raisedHand": "Gostaria de falar", "raisedHand": "Gostaria de falar",
"defaultNickname": "ex. João Pedro", "defaultNickname": "ex. João Pedro",
"defaultLink": "i.e. __url__", "defaultLink": "i.e. __url__",
"calling": "Chamando __name__ ...", "callingName": "__name__",
"userMedia": { "userMedia": {
"react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>", "react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
"chromeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>", "chromeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
@ -27,7 +25,7 @@
"raiseHand": "Erguer sua mão.", "raiseHand": "Erguer sua mão.",
"pushToTalk": "Pressione para falar.", "pushToTalk": "Pressione para falar.",
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.", "toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.",
"toggleFilmstrip": "Mostrar ou ocultar a tira de filme.", "toggleFilmstrip": "Mostrar ou ocultar os vídeos.",
"toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.", "toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.",
"focusLocal": "Foco no vídeo local.", "focusLocal": "Foco no vídeo local.",
"focusRemote": "Foco em um dos vídeos remotos.", "focusRemote": "Foco em um dos vídeos remotos.",
@ -93,11 +91,13 @@
"micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.", "micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.",
"unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.", "unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
"cameraDisabled": "A câmera não está disponível", "cameraDisabled": "A câmera não está disponível",
"micDisabled": "O microfone não está disponível" "micDisabled": "O microfone não está disponível",
"filmstrip": "",
"raiseHand": "Levantar a mão para falar"
}, },
"bottomtoolbar": { "bottomtoolbar": {
"chat": "Abrir / fechar bate-papo", "chat": "Abrir / fechar bate-papo",
"filmstrip": "Mostrar / ocultar a tira de usuários", "filmstrip": "Mostrar/ocultar vídeos",
"contactlist": "Abrir / fechar a lista de contatos" "contactlist": "Abrir / fechar a lista de contatos"
}, },
"chat": { "chat": {
@ -108,18 +108,27 @@
"messagebox": "Digite um texto..." "messagebox": "Digite um texto..."
}, },
"settings": { "settings": {
"title": "CONFIGURAÇÕES", "title": "Configurações",
"update": "Atualizar", "update": "Atualizar",
"name": "Nome", "name": "Nome",
"startAudioMuted": "Iniciar sem áudio", "startAudioMuted": "Todos iniciam mudos",
"startVideoMuted": "Iniciar sem vídeo", "startVideoMuted": "Todos iniciam ocultos",
"selectCamera": "Selecione a câmera", "selectCamera": "Câmera",
"selectMic": "Selecionar o microfone", "selectMic": "Microfone",
"selectAudioOutput": "Selecionar a saída de áudio", "selectAudioOutput": "Saída de áudio",
"followMe": "Habilitar o siga-me", "followMe": "Todos me seguem",
"noDevice": "Nenhum", "noDevice": "Nenhum",
"noPermission": "Permissão para usar o dispositivo não concedida", "noPermission": "Permissão para usar o dispositivo não concedida",
"avatarUrl": "URL do Avatar" "cameraAndMic": "Câmera e microfone",
"moderator": "MODERADOR",
"password": "DEFINIR SENHA",
"audioVideo": "ÁUDIO E VÍDEO",
"setPasswordLabel": "Trancar sua sala com uma senha."
},
"profile": {
"title": "PERFIL",
"setDisplayNameLabel": "Definir seu nome de exibição",
"setEmailLabel": "Definir seu email de gravatar"
}, },
"videothumbnail": { "videothumbnail": {
"editnickname": "Clique para editar o seu <br/>nome de exibição", "editnickname": "Clique para editar o seu <br/>nome de exibição",
@ -172,6 +181,7 @@
"connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.", "connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
"connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__", "connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
"connecting": "Conectando", "connecting": "Conectando",
"copy": "Copiar",
"error": "Erro", "error": "Erro",
"detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.", "detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
"failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela", "failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela",
@ -183,8 +193,8 @@
"lockMessage": "Falha ao travar a conferência.", "lockMessage": "Falha ao travar a conferência.",
"warning": "Atenção", "warning": "Atenção",
"passwordNotSupported": "Senhas de salas não são suportadas atualmente.", "passwordNotSupported": "Senhas de salas não são suportadas atualmente.",
"sorry": "Desculpe", "internalErrorTitle": "Erro interno",
"internalError": "Erro interno de aplicação [setRemoteDescription]", "internalError": "Ops! Alguma coisa está errada. Ocorreu o seguinte erro: [setRemoteDescriptio]",
"unableToSwitch": "Impossível trocar o fluxo de vídeo.", "unableToSwitch": "Impossível trocar o fluxo de vídeo.",
"SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)", "SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
"SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)", "SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
@ -216,26 +226,28 @@
"sipMsg": "Digite o número SIP", "sipMsg": "Digite o número SIP",
"passwordCheck": "Você tem certeza que deseja remover sua senha?", "passwordCheck": "Você tem certeza que deseja remover sua senha?",
"passwordMsg": "Definir uma senha para trancar sua sala", "passwordMsg": "Definir uma senha para trancar sua sala",
"Invite": "Convidar", "shareLink": "Copiar e compartilhar este link",
"shareLink": "Compartilhar este link com quem você espera convidar",
"settings1": "Configure sua conferência", "settings1": "Configure sua conferência",
"settings2": "Participantes entram mudos", "settings2": "Participantes entram mudos",
"settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:", "settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:",
"yourPassword": "sua Senha", "yourPassword": "Digite a nova senha",
"Back": "Voltar", "Back": "Voltar",
"serviceUnavailable": "Serviço indisponível", "serviceUnavailable": "Serviço indisponível",
"gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.", "gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
"Yes": "Sim", "Yes": "Sim",
"reservationError": "Erro de sistema de reserva", "reservationError": "Erro de sistema de reserva",
"reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__", "reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
"password": "senha", "password": "Insira a senha",
"userPassword": "senha do usuário", "userPassword": "senha do usuário",
"token": "token", "token": "token",
"tokenAuthFailed": "Falha em autenticar com o servidor XMPP: token inválido", "tokenAuthFailedTitle": "Problema na autenticação",
"tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
"displayNameRequired": "Digite seu nome de exibição", "displayNameRequired": "Digite seu nome de exibição",
"extensionRequired": "Extensão requerida:", "extensionRequired": "Extensão requerida:",
"firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!", "firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
"feedbackQuestion": "Como foi a chamada?", "rateExperience": "Por favor, avalie sua experiência na reunião.",
"feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
"feedbackQuestion": "Nos conte sobre sua chamada!",
"thankYou": "Obrigado por usar o __appName__!", "thankYou": "Obrigado por usar o __appName__!",
"sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?", "sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
"liveStreaming": "Live Streaming", "liveStreaming": "Live Streaming",
@ -253,12 +265,17 @@
"cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.", "cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
"cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.", "cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
"cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.", "cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
"cameraNotFoundError": "Câmera solicitada não foi encontrada.", "cameraNotFoundError": "A câmera não foi encontrada.",
"cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.", "cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.",
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.", "micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
"micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.", "micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
"micNotFoundError": "O microfone solicitado não foi encontrado.", "micNotFoundError": "O microfone não foi encontrado.",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas." "micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas.",
"micNotSendingData": "Seu microfone está inacessível. Selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
"cameraNotSendingData": "Sua câmera está inacessível. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
"goToStore": "Vá para a loja virtual",
"externalInstallationTitle": "Extensão requerida",
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela."
}, },
"email": { "email": {
"sharedKey": [ "sharedKey": [

View File

@ -1,11 +1,9 @@
{ {
"contactlist": "ON CALL (__participants__)", "contactlist": "On Call",
"connectionsettings": "Connection Settings", "connectionsettings": "Connection Settings",
"poweredby": "powered by", "poweredby": "powered by",
"downloadlogs": "Download logs",
"feedback": "Give us your feedback", "feedback": "Give us your feedback",
"roomUrlDefaultMsg": "Your conference is currently being created...", "roomUrlDefaultMsg": "Your conference is currently being created...",
"participant": "Participant",
"me": "me", "me": "me",
"speaker": "Speaker", "speaker": "Speaker",
"raisedHand": "Would like to speak", "raisedHand": "Would like to speak",
@ -33,6 +31,7 @@
"focusRemote": "Focus on one of the remote videos.", "focusRemote": "Focus on one of the remote videos.",
"toggleChat": "Open or close the chat panel.", "toggleChat": "Open or close the chat panel.",
"mute": "Mute or unmute the microphone.", "mute": "Mute or unmute the microphone.",
"fullScreen": "Enter or exit full screen mode.",
"videoMute": "Stop or start the local video." "videoMute": "Stop or start the local video."
}, },
"welcomepage":{ "welcomepage":{
@ -112,21 +111,21 @@
}, },
"settings": "settings":
{ {
"title": "SETTINGS", "title": "Settings",
"update": "Update", "update": "Update",
"name": "Name", "name": "Name",
"startAudioMuted": "Everyone starts muted", "startAudioMuted": "Everyone starts muted",
"startVideoMuted": "Everyone starts hidden", "startVideoMuted": "Everyone starts hidden",
"selectCamera": "Select camera", "selectCamera": "Camera",
"selectMic": "Select microphone", "selectMic": "Microphone",
"selectAudioOutput": "Select audio output", "selectAudioOutput": "Audio output",
"followMe": "Everyone follows me", "followMe": "Everyone follows me",
"noDevice": "None", "noDevice": "None",
"noPermission": "Permission to use device is not granted", "noPermission": "Permission to use device is not granted",
"cameraAndMic": "Camera and microphone", "cameraAndMic": "Camera and microphone",
"moderator": "MODERATOR", "moderator": "MODERATOR",
"password": "SET PASSWORD", "password": "SET PASSWORD",
"audioVideo": "AUDIO / VIDEO", "audioVideo": "AUDIO AND VIDEO",
"setPasswordLabel": "Lock your room with a password." "setPasswordLabel": "Lock your room with a password."
}, },
"profile": { "profile": {
@ -138,7 +137,7 @@
{ {
"editnickname": "Click to edit your<br/>display name", "editnickname": "Click to edit your<br/>display name",
"moderator": "The owner of<br/>this conference", "moderator": "The owner of<br/>this conference",
"videomute": "Participant has<br/>stopped the camera.", "videomute": "Participant has<br/>stopped the camera",
"mute": "Participant is muted", "mute": "Participant is muted",
"kick": "Kick out", "kick": "Kick out",
"muted": "Muted", "muted": "Muted",
@ -188,6 +187,7 @@
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.", "connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
"connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__", "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
"connecting": "Connecting", "connecting": "Connecting",
"copy": "Copy",
"error": "Error", "error": "Error",
"detectext": "Error when trying to detect desktopsharing extension.", "detectext": "Error when trying to detect desktopsharing extension.",
"failtoinstall": "Failed to install desktop sharing extension", "failtoinstall": "Failed to install desktop sharing extension",
@ -199,8 +199,8 @@
"lockMessage": "Failed to lock the conference.", "lockMessage": "Failed to lock the conference.",
"warning": "Warning", "warning": "Warning",
"passwordNotSupported": "Room passwords are currently not supported.", "passwordNotSupported": "Room passwords are currently not supported.",
"sorry": "Sorry", "internalErrorTitle": "Internal error",
"internalError": "Internal application error [setRemoteDescription]", "internalError": "Oups! Something went wrong. The following error occurred: [setRemoteDescription]",
"unableToSwitch": "Unable to switch video stream.", "unableToSwitch": "Unable to switch video stream.",
"SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)", "SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
"SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)", "SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
@ -233,7 +233,6 @@
"passwordCheck": "Are you sure you would like to remove your password?", "passwordCheck": "Are you sure you would like to remove your password?",
"Remove": "Remove", "Remove": "Remove",
"passwordMsg": "Set a password to lock your room", "passwordMsg": "Set a password to lock your room",
"Invite": "Invite",
"shareLink": "Copy and share this link", "shareLink": "Copy and share this link",
"settings1": "Configure your conference", "settings1": "Configure your conference",
"settings2": "Participants join muted", "settings2": "Participants join muted",
@ -248,13 +247,14 @@
"password": "Enter password", "password": "Enter password",
"userPassword": "user password", "userPassword": "user password",
"token": "token", "token": "token",
"tokenAuthFailed": "Failed to authenticate with XMPP server: invalid token", "tokenAuthFailedTitle": "Authentication problem",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"displayNameRequired": "Please enter your display name", "displayNameRequired": "Please enter your display name",
"extensionRequired": "Extension required:", "extensionRequired": "Extension required:",
"firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!", "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
"rateExperience": "Please rate your meeting experience.", "rateExperience": "Please rate your meeting experience.",
"feedbackHelp": "Your feedback will help us to improve our video experience.", "feedbackHelp": "Your feedback will help us to improve our video experience.",
"feedbackQuestion": "How was your call?", "feedbackQuestion": "Tell us about your call!",
"thankYou": "Thank you for using __appName__!", "thankYou": "Thank you for using __appName__!",
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?", "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
"liveStreaming": "Live Streaming", "liveStreaming": "Live Streaming",
@ -279,6 +279,7 @@
"micNotFoundError": "Microphone was not found.", "micNotFoundError": "Microphone was not found.",
"micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.", "micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.", "micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to restart the application.",
"goToStore": "Go to the webstore", "goToStore": "Go to the webstore",
"externalInstallationTitle": "Extension required", "externalInstallationTitle": "Extension required",
"externalInstallationMsg": "You need to install our desktop sharing extension." "externalInstallationMsg": "You need to install our desktop sharing extension."
@ -325,7 +326,8 @@
"ATTACHED": "Attached", "ATTACHED": "Attached",
"FETCH_SESSION_ID": "Obtaining session-id...", "FETCH_SESSION_ID": "Obtaining session-id...",
"GOT_SESSION_ID": "Obtaining session-id... Done", "GOT_SESSION_ID": "Obtaining session-id... Done",
"GET_SESSION_ID_ERROR": "Get session-id error: " "GET_SESSION_ID_ERROR": "Get session-id error: ",
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
}, },
"recording": "recording":
{ {

View File

@ -97,6 +97,7 @@ class TokenData{
this.payload = this.decodedJWT.payload; this.payload = this.decodedJWT.payload;
if(!this.payload.context) if(!this.payload.context)
return; return;
this.server = this.payload.context.server;
let callerData = this.payload.context.user; let callerData = this.payload.context.user;
let calleeData = this.payload.context.callee; let calleeData = this.payload.context.callee;
if(callerData) if(callerData)

View File

@ -1,321 +0,0 @@
/* global $, APP, config, interfaceConfig, JitsiMeetJS */
import UIEvents from "../../service/UI/UIEvents";
/**
* Constructs the html for the overall feedback window.
*
* @returns {string} the constructed html string
*/
var constructOverallFeedbackHtml = function() {
var feedbackQuestion = (Feedback.feedbackScore < 0)
? '<br/><br/>' + APP.translation
.translateString("dialog.feedbackQuestion")
: '';
var message = '<div class="feedback"><div>' +
'<div class="feedbackTitle">' +
APP.translation.translateString("dialog.thankYou",
{appName:interfaceConfig.APP_NAME}) +
'</div>' +
feedbackQuestion +
'</div><br/><br/>' +
'<div id="stars">' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'</div></div>';
return message;
};
/**
* Constructs the html for the detailed feedback window.
*
* @returns {string} the contructed html string
*/
var constructDetailedFeedbackHtml = function() {
// Construct the html, which will be served as a dialog message.
var message = '<div class="feedback">' +
'<div class="feedbackTitle">' +
APP.translation.translateString("dialog.sorryFeedback") +
'</div><br/><br/>' +
'<div class="feedbackDetails">' +
'<textarea id="feedbackTextArea" rows="10" cols="50" autofocus>' +
'</textarea>' +
'</div></div>';
return message;
};
var createRateFeedbackHTML = function () {
var rate = APP.translation.translateString('dialog.rateExperience'),
help = APP.translation.translateString('dialog.feedbackHelp');
return `
<div class="feedback-rating text-center">
<h2>${ rate }</h2>
<p class="star-label">&nbsp;</p>
<div id="stars" class="feedback-stars">
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
</div>
<p>&nbsp;</p>
<p>${ help }</p>
</div>
`;
};
/**
* The callback function corresponding to the openFeedbackWindow parameter.
*
* @type {function}
*/
var feedbackWindowCallback = null;
/**
* Shows / hides the feedback button.
* @private
*/
function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
/**
* Shows / hides the feedback button.
* @param {show} set to {true} to show the feedback button or to {false}
* to hide it
* @private
*/
function _showFeedbackButton (show) {
var feedbackButton = $("#feedbackButtonDiv");
if (show)
feedbackButton.css("display", "block");
else
feedbackButton.css("display", "none");
}
/**
* Defines all methods in connection to the Feedback window.
*
* @type {{feedbackScore: number, openFeedbackWindow: Function,
* toggleStars: Function, hoverStars: Function, unhoverStars: Function}}
*/
var Feedback = {
/**
* The feedback score. -1 indicates no score has been given for now.
*/
feedbackScore: -1,
/**
* Initialise the Feedback functionality.
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function (emitter) {
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
// If enabled property is still undefined, i.e. it hasn't been set from
// some other module already, we set it to true by default.
if (typeof this.enabled == "undefined")
this.enabled = true;
_showFeedbackButton(this.enabled);
$("#feedbackButton").click(function (event) {
Feedback.openFeedbackWindow();
});
// Show / hide the feedback button whenever the film strip is
// shown / hidden.
emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
_toggleFeedbackIcon();
});
},
/**
* Enables/ disabled the feedback feature.
*/
enableFeedback: function (enable) {
if (this.enabled !== enable)
_showFeedbackButton(enable);
this.enabled = enable;
},
/**
* Indicates if the feedback functionality is enabled.
*
* @return true if the feedback functionality is enabled, false otherwise.
*/
isEnabled: function() {
return this.enabled && APP.conference.isCallstatsEnabled();
},
/**
* Returns true if the feedback window is currently visible and false
* otherwise.
* @return {boolean} true if the feedback window is visible, false
* otherwise
*/
isVisible: function() {
return $(".feedback").is(":visible");
},
/**
* Opens the feedback window.
*/
openFeedbackWindow: function (callback) {
feedbackWindowCallback = callback;
// Add all mouse and click listeners.
var onLoadFunction = function (event) {
$('#stars >a').each(function(index) {
// On star mouse over.
$(this).get(0).onmouseover = function(){
Feedback.hoverStars(index);
};
// On star mouse leave.
$(this).get(0).onmouseleave = function(){
Feedback.unhoverStars(index);
};
// On star click.
$(this).get(0).onclick = function(){
Feedback.toggleStars(index);
Feedback.feedbackScore = index+1;
// If the feedback is less than 3 stars we're going to
// ask the user for more information.
if (Feedback.feedbackScore > 3) {
APP.conference.sendFeedback(Feedback.feedbackScore, "");
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
}
else {
feedbackDialog.goToState('detailed_feedback');
}
};
// Init stars to correspond to previously entered feedback.
if (Feedback.feedbackScore > 0
&& index < Feedback.feedbackScore) {
Feedback.hoverStars(index);
Feedback.toggleStars(index);
}
});
};
// Defines the different states of the feedback window.
var states = {
overall_feedback: {
html: createRateFeedbackHTML(),
persistent: false,
buttons: {},
closeText: '',
focus: "div[id='stars']",
position: {width: 500}
},
detailed_feedback: {
html: constructDetailedFeedbackHtml(),
buttons: {"Submit": true, "Cancel": false},
closeText: '',
focus: "textarea[id='feedbackTextArea']",
position: {width: 500},
submit: function(e,v,m,f) {
e.preventDefault();
if (v) {
var feedbackDetails
= document.getElementById("feedbackTextArea").value;
if (feedbackDetails && feedbackDetails.length > 0) {
APP.conference.sendFeedback( Feedback.feedbackScore,
feedbackDetails);
}
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
} else {
// User cancelled
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
}
}
}
};
// Create the feedback dialog.
var feedbackDialog
= APP.UI.messageHandler.openDialogWithStates(
states,
{ persistent: false,
buttons: {},
closeText: '',
loaded: onLoadFunction,
position: {width: 500}}, null);
JitsiMeetJS.analytics.sendEvent('feedback.open');
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been clicked/selected.
*
* @param starCount the number of stars, for which to toggle the css class
*/
toggleStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount) {
$(this).removeClass("icon-star");
}
else
$(this).addClass("icon-star");
});
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been hovered.
*
* @param starCount the number of stars, for which to toggle the css class
*/
hoverStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount)
$(this).addClass("starHover");
});
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been un-hovered.
*
* @param starCount the number of stars, for which to toggle the css class
*/
unhoverStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount && $(this).hasClass("icon-star"))
$(this).removeClass("starHover");
});
}
};
// Exports the Feedback class.
module.exports = Feedback;

View File

@ -29,7 +29,7 @@ var EventEmitter = require("events");
UI.messageHandler = require("./util/MessageHandler"); UI.messageHandler = require("./util/MessageHandler");
var messageHandler = UI.messageHandler; var messageHandler = UI.messageHandler;
var JitsiPopover = require("./util/JitsiPopover"); var JitsiPopover = require("./util/JitsiPopover");
var Feedback = require("./Feedback"); var Feedback = require("./feedback/Feedback");
import FollowMe from "../FollowMe"; import FollowMe from "../FollowMe";
@ -60,6 +60,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NOT_FOUND]
= "dialog.cameraNotFoundError"; = "dialog.cameraNotFoundError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED] JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED]
= "dialog.cameraConstraintFailedError"; = "dialog.cameraConstraintFailedError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NO_DATA_FROM_SOURCE]
= "dialog.cameraNotSendingData";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL] JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
= "dialog.micUnknownError"; = "dialog.micUnknownError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED] JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED]
@ -68,6 +70,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NOT_FOUND]
= "dialog.micNotFoundError"; = "dialog.micNotFoundError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED] JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
= "dialog.micConstraintFailedError"; = "dialog.micConstraintFailedError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
= "dialog.micNotSendingData";
/** /**
* Prompt user for nickname. * Prompt user for nickname.
@ -257,6 +261,17 @@ UI.changeDisplayName = function (id, displayName) {
} }
}; };
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
UI.showLocalConnectionInterrupted = function (isInterrupted) {
VideoLayout.showLocalConnectionInterrupted(isInterrupted);
};
/** /**
* Sets the "raised hand" status for a participant. * Sets the "raised hand" status for a participant.
*/ */
@ -292,11 +307,11 @@ UI.initConference = function () {
} }
// Add myself to the contact list. // Add myself to the contact list.
ContactList.addContact(id); ContactList.addContact(id, true);
//update default button states before showing the toolbar // Update default button states before showing the toolbar
//if local role changes buttons state will be again updated // if local role changes buttons state will be again updated.
UI.updateLocalRole(false); UI.updateLocalRole(APP.conference.isModerator);
UI.showToolbar(); UI.showToolbar();
@ -325,6 +340,8 @@ UI.initConference = function () {
// to the UI (depending on the moderator role of the local participant) and // to the UI (depending on the moderator role of the local participant) and
// (2) APP.conference as means of communication between the participants. // (2) APP.conference as means of communication between the participants.
followMeHandler = new FollowMe(APP.conference, UI); followMeHandler = new FollowMe(APP.conference, UI);
UIUtil.activateTooltips();
}; };
UI.mucJoined = function () { UI.mucJoined = function () {
@ -339,6 +356,22 @@ UI.handleToggleFilmStrip = () => {
VideoLayout.resizeVideoArea(true, false); VideoLayout.resizeVideoArea(true, false);
}; };
/**
* Sets tooltip defaults.
*
* @private
*/
function _setTooltipDefaults() {
$.fn.tooltip.defaults = {
opacity: 1, //defaults to 1
offset: 1,
delayIn: 0, //defaults to 500
hoverable: true,
hideOnClick: true,
aria: true
};
}
/** /**
* Setup some UI event listeners. * Setup some UI event listeners.
*/ */
@ -431,6 +464,9 @@ UI.start = function () {
// Set the defaults for prompt dialogs. // Set the defaults for prompt dialogs.
$.prompt.setDefaults({persistent: false}); $.prompt.setDefaults({persistent: false});
// Set the defaults for tooltips.
_setTooltipDefaults();
registerListeners(); registerListeners();
ToolbarToggler.init(); ToolbarToggler.init();
@ -463,20 +499,10 @@ UI.start = function () {
$('#noticeText').text(config.noticeMessage); $('#noticeText').text(config.noticeMessage);
$('#notice').css({display: 'block'}); $('#notice').css({display: 'block'});
} }
$("#downloadlog").click(function (event) {
let logs = APP.conference.getLogs();
let data = encodeURIComponent(JSON.stringify(logs, null, ' '));
let elem = event.target.parentNode;
elem.download = 'meetlog.json';
elem.href = 'data:application/json;charset=utf-8,\n' + data;
});
} else { } else {
$("#mainToolbarContainer").css("display", "none"); $("#mainToolbarContainer").css("display", "none");
$("#downloadlog").css("display", "none");
FilmStrip.setupFilmStripOnly(); FilmStrip.setupFilmStripOnly();
messageHandler.enableNotifications(false); messageHandler.enableNotifications(false);
$('body').popover("disable");
JitsiPopover.enabled = false; JitsiPopover.enabled = false;
} }
@ -589,10 +615,11 @@ UI.getSharedDocumentManager = function () {
/** /**
* Show user on UI. * Show user on UI.
* @param {string} id user id * @param {JitsiParticipant} user
* @param {string} displayName user nickname
*/ */
UI.addUser = function (id, displayName) { UI.addUser = function (user) {
var id = user.getId();
var displayName = user.getDisplayName();
UI.hideRingOverLay(); UI.hideRingOverLay();
ContactList.addContact(id); ContactList.addContact(id);
@ -605,7 +632,7 @@ UI.addUser = function (id, displayName) {
UIUtil.playSoundNotification('userJoined'); UIUtil.playSoundNotification('userJoined');
// Add Peer's container // Add Peer's container
VideoLayout.addParticipantContainer(id); VideoLayout.addParticipantContainer(user);
// Configure avatar // Configure avatar
UI.setUserEmail(id); UI.setUserEmail(id);
@ -662,7 +689,9 @@ UI.updateLocalRole = function (isModerator) {
SettingsMenu.showFollowMeOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) { if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator"); if (!interfaceConfig.DISABLE_FOCUS_INDICATOR)
messageHandler
.notify(null, "notify.me", 'connected', "notify.moderator");
Recording.checkAutoRecord(); Recording.checkAutoRecord();
} }
@ -676,7 +705,9 @@ UI.updateLocalRole = function (isModerator) {
UI.updateUserRole = function (user) { UI.updateUserRole = function (user) {
VideoLayout.showModeratorIndicator(); VideoLayout.showModeratorIndicator();
if (!user.isModerator()) { // We don't need to show moderator notifications when the focus (moderator)
// indicator is disabled.
if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
return; return;
} }
@ -970,6 +1001,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
VideoLayout.onLastNEndpointsChanged(ids, enteringIds); VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
}; };
/**
* Will handle notification about participant's connectivity status change.
*
* @param {string} id the id of remote participant(MUC jid)
* @param {boolean} isActive true if the connection is ok or false if the user
* is having connectivity issues.
*/
UI.participantConnectionStatusChanged = function (id, isActive) {
VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
};
/** /**
* Update audio level visualization for specified user. * Update audio level visualization for specified user.
* @param {string} id user id * @param {string} id user id
@ -1052,50 +1094,6 @@ UI.updateDTMFSupport = function (isDTMFSupported) {
//Toolbar.showDialPadButton(dtmfSupport); //Toolbar.showDialPadButton(dtmfSupport);
}; };
/**
* Invite participants to conference.
* @param {string} roomUrl
* @param {string} conferenceName
* @param {string} key
* @param {string} nick
*/
UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
let keyText = "";
if (key) {
keyText = APP.translation.translateString(
"email.sharedKey", {sharedKey: key}
);
}
let and = APP.translation.translateString("email.and");
let supportedBrowsers = `Chromium, Google Chrome, Firefox ${and} Opera`;
let subject = APP.translation.translateString(
"email.subject", {appName:interfaceConfig.APP_NAME, conferenceName}
);
let body = APP.translation.translateString(
"email.body", {
appName:interfaceConfig.APP_NAME,
sharedKeyText: keyText,
roomUrl,
supportedBrowsers
}
);
body = body.replace(/\n/g, "%0D%0A");
if (nick) {
body += "%0D%0A%0D%0A" + UIUtil.escapeHtml(nick);
}
if (interfaceConfig.INVITATION_POWERED_BY) {
body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
}
window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
};
/** /**
* Show user feedback dialog if its required or just show "thank you" dialog. * Show user feedback dialog if its required or just show "thank you" dialog.
* @returns {Promise} when dialog is closed. * @returns {Promise} when dialog is closed.
@ -1103,12 +1101,15 @@ UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
UI.requestFeedback = function () { UI.requestFeedback = function () {
if (Feedback.isVisible()) if (Feedback.isVisible())
return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS); return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS);
// Feedback has been submitted already.
else if (Feedback.isEnabled() && Feedback.isSubmitted())
return Promise.resolve();
else else
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (Feedback.isEnabled()) { if (Feedback.isEnabled()) {
// If the user has already entered feedback, we'll show the // If the user has already entered feedback, we'll show the
// window and immidiately start the conference dispose timeout. // window and immidiately start the conference dispose timeout.
if (Feedback.feedbackScore > 0) { if (Feedback.getFeedbackScore() > 0) {
Feedback.openFeedbackWindow(); Feedback.openFeedbackWindow();
resolve(); resolve();
@ -1117,14 +1118,9 @@ UI.requestFeedback = function () {
} }
} else { } else {
// If the feedback functionality isn't enabled we show a thank // If the feedback functionality isn't enabled we show a thank
// you dialog. // you dialog. Signaling it (true), so the caller
messageHandler.openMessageDialog( // of requestFeedback can act on it
null, null, null, resolve(true);
APP.translation.translateString(
"dialog.thankYou", {appName:interfaceConfig.APP_NAME}
)
);
resolve();
} }
}); });
}; };
@ -1134,11 +1130,13 @@ UI.updateRecordingState = function (state) {
}; };
UI.notifyTokenAuthFailed = function () { UI.notifyTokenAuthFailed = function () {
messageHandler.showError("dialog.error", "dialog.tokenAuthFailed"); messageHandler.showError( "dialog.tokenAuthFailedTitle",
"dialog.tokenAuthFailed");
}; };
UI.notifyInternalError = function () { UI.notifyInternalError = function () {
messageHandler.showError("dialog.sorry", "dialog.internalError"); messageHandler.showError( "dialog.internalErrorTitle",
"dialog.internalError");
}; };
UI.notifyFocusDisconnected = function (focus, retrySec) { UI.notifyFocusDisconnected = function (focus, retrySec) {
@ -1193,6 +1191,16 @@ UI.onStartMutedChanged = function (startAudioMuted, startVideoMuted) {
SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted); SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
}; };
/**
* Notifies interested listeners that the raise hand property has changed.
*
* @param {boolean} isRaisedHand indicates the current state of the
* "raised hand"
*/
UI.onLocalRaiseHandChanged = function (isRaisedHand) {
eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
};
/** /**
* Update list of available physical devices. * Update list of available physical devices.
* @param {object[]} devices new list of available devices * @param {object[]} devices new list of available devices
@ -1415,12 +1423,13 @@ UI.showDeviceErrorDialog = function (micError, cameraError) {
/** /**
* Shows error dialog that informs the user that no data is received from the * Shows error dialog that informs the user that no data is received from the
* microphone. * device.
*/ */
UI.showAudioNotWorkingDialog = function () { UI.showTrackNotWorkingDialog = function (stream) {
messageHandler.openMessageDialog( messageHandler.openMessageDialog(
"dialog.error", "dialog.error",
"dialog.micNotSendingData", stream.isAudioTrack()? "dialog.micNotSendingData" :
"dialog.cameraNotSendingData",
null, null,
null); null);
}; };

View File

@ -1,260 +1,165 @@
/* global APP, interfaceConfig, $ */ /* global interfaceConfig */
/* jshint -W101 */
import CanvasUtil from './CanvasUtils'; import UIUtil from "../util/UIUtil";
import FilmStrip from '../videolayout/FilmStrip';
const LOCAL_LEVEL = 'local';
let ASDrawContext = null;
let audioLevelCanvasCache = {};
let dominantSpeakerAudioElement = null;
function initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
let ASRadius = dominantSpeakerAvatarSize / 2;
let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
// Draw a circle.
ASDrawContext.beginPath();
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
ASDrawContext.closePath();
// Add a shadow around the circle
ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
ASDrawContext.shadowOffsetX = 0;
ASDrawContext.shadowOffsetY = 0;
}
/** /**
* Resizes the given audio level canvas to match the given thumbnail size. * Responsible for drawing audio levels.
*/
function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(id, audioLevel) {
if (!audioLevelCanvasCache[id]) {
let videoSpanId = getVideoSpanId(id);
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig) {
audioLevelCanvasCache[id]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
let canvas = audioLevelCanvasCache[id];
if (!canvas) {
return;
}
let drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
let shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0) {
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow(
drawContext,
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
}
}
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
let shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
} else if (audioLevel <= 0.6) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
} else {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
}
/**
* Returns the video span id corresponding to the given user id
*/
function getVideoSpanId(id) {
let videoSpanId = null;
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
videoSpanId = 'localVideoContainer';
} else {
videoSpanId = `participant_${id}`;
}
return videoSpanId;
}
/**
* The audio Levels plugin.
*/ */
const AudioLevels = { const AudioLevels = {
init () { /**
dominantSpeakerAudioElement = $('#dominantSpeakerAudioLevel')[0]; * The number of dots.
ASDrawContext = dominantSpeakerAudioElement.getContext('2d'); *
* IMPORTANT: functions below assume that this is an odd number.
let parentContainer = $("#dominantSpeaker"); */
let dominantSpeakerWidth = parentContainer.width(); _AUDIO_LEVEL_DOTS: 5,
let dominantSpeakerHeight = parentContainer.height();
dominantSpeakerAudioElement.width = dominantSpeakerWidth;
dominantSpeakerAudioElement.height = dominantSpeakerHeight;
let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
},
/** /**
* Updates the audio level canvas for the given id. If the canvas * Creates the audio level indicator span element.
* didn't exist we create it. *
* IMPORTANT: This function assumes that the number of dots is an
* odd number.
*
* @return {Element} the document element representing audio levels
*/ */
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) { createThumbnailAudioLevelIndicator() {
let videoSpanId = 'localVideoContainer';
if (id) { let audioSpan = document.createElement('span');
videoSpanId = `participant_${id}`; audioSpan.className = 'audioindicator';
}
this.sideDotsCount = Math.floor(this._AUDIO_LEVEL_DOTS/2);
let videoSpan = document.getElementById(videoSpanId);
for (let i = 0; i < this._AUDIO_LEVEL_DOTS; i++) {
if (!videoSpan) { let audioDot = document.createElement('span');
if (id) {
console.error("No video element for id", id); // The median index will be equal to the number of dots on each
} else { // side.
console.error("No video element for local video."); if (i === this.sideDotsCount)
} audioDot.className = "audiodot-middle";
return; else
} audioDot.className = (i < this.sideDotsCount)
? "audiodot-top"
let audioLevelCanvas = $(`#${videoSpanId}>canvas`); : "audiodot-bottom";
if (!audioLevelCanvas || audioLevelCanvas.length === 0) { audioSpan.appendChild(audioDot);
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
audioLevelCanvas.style.left
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
} }
return audioSpan;
}, },
/** /**
* Updates the audio level UI for the given id. * Updates the audio level UI for the given id.
* *
* @param id id of the user for whom we draw the audio level * @param {string} id id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render * @param {number} audioLevel the newAudio level to render
*/ */
updateAudioLevel (id, audioLevel, largeVideoId) { updateThumbnailAudioLevel (id, audioLevel) {
drawAudioLevelCanvas(id, audioLevel);
let videoSpanId = getVideoSpanId(id); // First make sure we are sensitive enough.
audioLevel *= 1.2;
audioLevel = Math.min(audioLevel, 1);
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0); // Let's now stretch the audio level over the number of dots we have.
let stretchedAudioLevel = (this.sideDotsCount + 1) * audioLevel;
let dotLevel = 0.0;
if (!audioLevelCanvas) { for (let i = 0; i < (this.sideDotsCount + 1); i++) {
return;
}
let drawContext = audioLevelCanvas.getContext('2d'); dotLevel = Math.min(1, Math.max(0, (stretchedAudioLevel - i)));
this._setDotLevel(id, i, dotLevel);
let canvasCache = audioLevelCanvasCache[id];
drawContext.clearRect(
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
);
drawContext.drawImage(canvasCache, 0, 0);
if (id === LOCAL_LEVEL) {
id = APP.conference.getMyUserId();
if (!id) {
return;
}
}
if(id === largeVideoId) {
window.requestAnimationFrame(function () {
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
});
} }
}, },
updateDominantSpeakerAudioLevel (audioLevel) { /**
if($("#dominantSpeaker").css("visibility") == "hidden" * Fills the dot(s) with the specified "index", with as much opacity as
|| ASDrawContext === null) { * indicated by "opacity".
*
* @param {string} elementID the parent audio indicator span element
* @param {number} index the index of the dots to fill, where 0 indicates
* the middle dot and the following increments point toward the
* corresponding pair of dots.
* @param {number} opacity the opacity to set for the specified dot.
*/
_setDotLevel(elementID, index, opacity) {
let audioSpan = document.getElementById(elementID)
.getElementsByClassName("audioindicator");
// Make sure the audio span is still around.
if (audioSpan && audioSpan.length > 0)
audioSpan = audioSpan[0];
else
return;
let audioTopDots
= audioSpan.getElementsByClassName("audiodot-top");
let audioDotMiddle
= audioSpan.getElementsByClassName("audiodot-middle");
let audioBottomDots
= audioSpan.getElementsByClassName("audiodot-bottom");
// First take care of the middle dot case.
if (index === 0){
audioDotMiddle[0].style.opacity = opacity;
return; return;
} }
ASDrawContext.clearRect(0, 0, // Index > 0 : we are setting non-middle dots.
dominantSpeakerAudioElement.width, index--;
dominantSpeakerAudioElement.height); audioBottomDots[index].style.opacity = opacity;
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
if (!audioLevel) {
return;
}
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
// Fill the shape.
ASDrawContext.fill();
}, },
updateCanvasSize (thumbWidth, thumbHeight) { /**
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA; * Updates the audio level of the large video.
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA; *
* @param audioLevel the new audio level to set.
*/
updateLargeVideoAudioLevel(elementId, audioLevel) {
let element = document.getElementById(elementId);
FilmStrip.getThumbs().children('canvas').each(function () { if(!UIUtil.isVisible(element))
$(this).attr('width', canvasWidth); return;
$(this).attr('height', canvasHeight);
});
Object.keys(audioLevelCanvasCache).forEach(function (id) { let level = parseFloat(audioLevel);
audioLevelCanvasCache[id].width = canvasWidth;
audioLevelCanvasCache[id].height = canvasHeight; level = isNaN(level) ? 0 : level;
});
let shadowElement = element.getElementsByClassName("dynamic-shadow");
if (shadowElement && shadowElement.length > 0)
shadowElement = shadowElement[0];
shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
},
/**
* Updates the large video shadow effect.
*/
_updateLargeVideoShadow (level) {
var scale = 2,
// Internal circle audio level.
int = {
level: level > 0.15 ? 20 : 0,
color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
},
// External circle audio level.
ext = {
level: (int.level * scale * level + int.level).toFixed(0),
color: interfaceConfig.AUDIO_LEVEL_SECONDARY_COLOR
};
// Internal blur.
int.blur = int.level ? 2 : 0;
// External blur.
ext.blur = ext.level ? 6 : 0;
return [
`0 0 ${ int.blur }px ${ int.level }px ${ int.color }`,
`0 0 ${ ext.blur }px ${ ext.level }px ${ ext.color }`
].join(', ');
} }
}; };

View File

@ -1,108 +0,0 @@
/**
* Utility class for drawing canvas shapes.
*/
const CanvasUtil = {
/**
* Draws a round rectangle with a glow. The glowWidth indicates the depth
* of the glow.
*
* @param drawContext the context of the canvas to draw to
* @param x the x coordinate of the round rectangle
* @param y the y coordinate of the round rectangle
* @param w the width of the round rectangle
* @param h the height of the round rectangle
* @param glowColor the color of the glow
* @param glowWidth the width of the glow
*/
drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context.
drawContext.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
// Draw a round rectangle.
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// Add a shadow around the rectangle
drawContext.shadowColor = glowColor;
drawContext.shadowBlur = glowWidth;
drawContext.shadowOffsetX = 0;
drawContext.shadowOffsetY = 0;
// Fill the shape.
drawContext.fill();
drawContext.save();
drawContext.restore();
// 1) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.globalCompositeOperation='destination-out';
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// 2) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.fill();
// Comment these two lines if choosing to do the same with composite
// operation above 1 and 2.
drawContext.clip();
drawContext.clearRect(0, 0, 277, 200);
// Restore the previous context state.
drawContext.restore();
},
/**
* Clones the given canvas.
*
* @return the new cloned canvas.
*/
cloneCanvas (oldCanvas) {
/*
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
* levels are frequently updated, the errors have been observed to pile
* into the console, strain the CPU.
*/
if (!oldCanvas)
return oldCanvas;
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
}
};
export default CanvasUtil;

View File

@ -116,6 +116,13 @@ export default function createRoomLocker (room) {
let password; let password;
let dialog = null; let dialog = null;
/**
* If the room was locked from someone other than us, we indicate it with
* this property in order to have correct roomLocker state of isLocked.
* @type {boolean} whether room is locked, but not from us.
*/
let lockedElsewhere = false;
function lock (newPass) { function lock (newPass) {
return room.lock(newPass).then(function () { return room.lock(newPass).then(function () {
password = newPass; password = newPass;
@ -135,13 +142,30 @@ export default function createRoomLocker (room) {
*/ */
return { return {
get isLocked () { get isLocked () {
return !!password; return !!password || lockedElsewhere;
}, },
get password () { get password () {
return password; return password;
}, },
/**
* Sets that the room is locked from another user, not us.
* @param {boolean} value locked/unlocked state
*/
set lockedElsewhere (value) {
lockedElsewhere = value;
},
/**
* Whether room is locked from someone else.
* @returns {boolean} whether room is not locked locally,
* but it is still locked.
*/
get lockedElsewhere () {
return lockedElsewhere;
},
/** /**
* Allows to remove password from the conference (asks user first). * Allows to remove password from the conference (asks user first).
* @returns {Promise} * @returns {Promise}
@ -185,6 +209,10 @@ export default function createRoomLocker (room) {
newPass => { password = newPass; } newPass => { password = newPass; }
).catch( ).catch(
reason => { reason => {
// user canceled, no pass was entered.
// clear, as if we use the same instance several times
// pass stays between attempts
password = null;
if (reason !== APP.UI.messageHandler.CANCEL) if (reason !== APP.UI.messageHandler.CANCEL)
console.error(reason); console.error(reason);
} }
@ -202,7 +230,7 @@ export default function createRoomLocker (room) {
dialog = null; dialog = null;
}; };
if (password) { if (this.isLocked) {
dialog = APP.UI.messageHandler dialog = APP.UI.messageHandler
.openMessageDialog(null, "dialog.passwordError", .openMessageDialog(null, "dialog.passwordError",
null, null, closeCallback); null, null, closeCallback);

View File

@ -0,0 +1,128 @@
/* global $, APP, config, interfaceConfig, JitsiMeetJS */
import UIEvents from "../../../service/UI/UIEvents";
import FeedabckWindow from "./FeedbackWindow";
/**
* Shows / hides the feedback button.
* @private
*/
function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
/**
* Shows / hides the feedback button.
* @param {show} set to {true} to show the feedback button or to {false}
* to hide it
* @private
*/
function _showFeedbackButton (show) {
var feedbackButton = $("#feedbackButtonDiv");
if (show)
feedbackButton.css("display", "block");
else
feedbackButton.css("display", "none");
}
/**
* Defines all methods in connection to the Feedback window.
*
* @type {{openFeedbackWindow: Function}}
*/
var Feedback = {
/**
* Initialise the Feedback functionality.
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function (emitter) {
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
// If enabled property is still undefined, i.e. it hasn't been set from
// some other module already, we set it to true by default.
if (typeof this.enabled == "undefined")
this.enabled = true;
_showFeedbackButton(this.enabled);
this.window = new FeedabckWindow({});
$("#feedbackButton").click(Feedback.openFeedbackWindow);
// Show / hide the feedback button whenever the film strip is
// shown / hidden.
emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
_toggleFeedbackIcon();
});
},
/**
* Enables/ disabled the feedback feature.
*/
enableFeedback: function (enable) {
if (this.enabled !== enable)
_showFeedbackButton(enable);
this.enabled = enable;
},
/**
* Indicates if the feedback functionality is enabled.
*
* @return true if the feedback functionality is enabled, false otherwise.
*/
isEnabled: function() {
return this.enabled && APP.conference.isCallstatsEnabled();
},
/**
* Returns true if the feedback window is currently visible and false
* otherwise.
* @return {boolean} true if the feedback window is visible, false
* otherwise
*/
isVisible: function() {
return $(".feedback").is(":visible");
},
/**
* Indicates if the feedback is submitted.
*
* @return {boolean} {true} to indicate if the feedback is submitted,
* {false} - otherwise
*/
isSubmitted: function() {
return Feedback.window.submitted;
},
/**
* Opens the feedback window.
*/
openFeedbackWindow: function (callback) {
Feedback.window.show(callback);
JitsiMeetJS.analytics.sendEvent('feedback.open');
},
/**
* Returns the feedback score.
*
* @returns {*}
*/
getFeedbackScore: function() {
return Feedback.window.feedbackScore;
},
/**
* Returns the feedback free text.
*
* @returns {null|*|message}
*/
getFeedbackText: function() {
return Feedback.window.feedbackText;
}
};
module.exports = Feedback;

View File

@ -0,0 +1,193 @@
/* global $, APP, interfaceConfig, AJS */
/* jshint -W101 */
const selector = '#aui-feedback-dialog';
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been clicked/selected.
*
* @param starCount the number of stars, for which to toggle the css class
*/
let toggleStars = function(starCount) {
$('#stars > a').each(function(index, el) {
if (index <= starCount) {
el.classList.add("starHover");
} else
el.classList.remove("starHover");
});
};
/**
* Constructs the html for the rated feedback window.
*
* @returns {string} the contructed html string
*/
let createRateFeedbackHTML = function (Feedback) {
let rateExperience
= APP.translation.translateString('dialog.rateExperience'),
feedbackHelp = APP.translation.translateString('dialog.feedbackHelp');
let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
? "icon-star shake-rotate"
: "icon-star";
return `
<div class="aui-dialog2-content feedback__content">
<form action="javascript:false;" onsubmit="return false;">
<div class="feedback__rating">
<h2>${ rateExperience }</h2>
<p class="star-label">&nbsp;</p>
<div id="stars" class="feedback-stars">
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
</div>
<p>&nbsp;</p>
<p>${ feedbackHelp }</p>
</div>
<textarea id="feedbackTextArea" rows="10" cols="40" autofocus></textarea>
</form>
<footer class="aui-dialog2-footer feedback__footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-close-button" class="aui-button aui-button_close">Close</button>
<button id="dialog-submit-button" class="aui-button aui-button_submit">Submit</button>
</div>
</footer>
</div>
`;
};
/**
* Callback for Rate Feedback
*
* @param Feedback
*/
let onLoadRateFunction = function (Feedback) {
$('#stars > a').each((index, el) => {
el.onmouseover = function(){
toggleStars(index);
};
el.onmouseleave = function(){
toggleStars(Feedback.feedbackScore - 1);
};
el.onclick = function(){
Feedback.feedbackScore = index + 1;
};
});
// Init stars to correspond to previously entered feedback.
if (Feedback.feedbackScore > 0) {
toggleStars(Feedback.feedbackScore - 1);
}
if (Feedback.feedbackText && Feedback.feedbackText.length > 0)
$('#feedbackTextArea').text(Feedback.feedbackText);
let submitBtn = Feedback.$el.find('#dialog-submit-button');
let closeBtn = Feedback.$el.find('#dialog-close-button');
if (submitBtn && submitBtn.length) {
submitBtn.on('click', (e) => {
e.preventDefault();
Feedback.onFeedbackSubmitted();
});
}
if (closeBtn && closeBtn.length) {
closeBtn.on('click', (e) => {
e.preventDefault();
Feedback.hide();
});
}
$('#feedbackTextArea').focus();
};
/**
* @class Dialog
*
*/
export default class Dialog {
constructor(options) {
this.feedbackScore = -1;
this.feedbackText = null;
this.submitted = false;
this.onCloseCallback = null;
this.states = {
rate_feedback: {
getHtml: createRateFeedbackHTML,
onLoad: onLoadRateFunction
}
};
this.state = options.state || 'rate_feedback';
this.window = AJS.dialog2(selector, {
closeOnOutsideClick: true
});
this.$el = this.window.$el;
AJS.dialog2(selector).on("hide", function() {
if (this.onCloseCallback) {
this.onCloseCallback();
this.onCloseCallback = null;
}
}.bind(this));
this.setState();
}
setState(state) {
let newState = state || this.state;
let htmlStr = this.states[newState].getHtml(this);
this.$el.html(htmlStr);
this.states[newState].onLoad(this);
}
show(cb) {
this.setState('rate_feedback');
if (typeof cb == 'function') {
this.onCloseCallback = cb;
}
this.window.show();
}
hide() {
this.window.hide();
}
onFeedbackSubmitted() {
let message = this.$el.find('textarea').val();
let self = this;
if (message && message.length > 0) {
self.feedbackText = message;
}
APP.conference.sendFeedback(self.feedbackScore,
self.feedbackText);
// TO DO: make sendFeedback return true or false.
self.submitted = true;
this.hide();
}
}

View File

@ -17,7 +17,7 @@
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from '../util/UIUtil'; import UIUtil from '../util/UIUtil';
import VideoLayout from '../videolayout/VideoLayout'; import VideoLayout from '../videolayout/VideoLayout';
import Feedback from '../Feedback.js'; import Feedback from '../feedback/Feedback.js';
import Toolbar from '../toolbars/Toolbar'; import Toolbar from '../toolbars/Toolbar';
/** /**
@ -270,6 +270,9 @@ var Recording = {
initRecordingButton(recordingType) { initRecordingButton(recordingType) {
let selector = $('#toolbar_button_record'); let selector = $('#toolbar_button_record');
let button = selector.get(0);
UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
if (recordingType === 'jibri') { if (recordingType === 'jibri') {
this.baseClass = "fa fa-play-circle"; this.baseClass = "fa fa-play-circle";
this.recordingTitle = "dialog.liveStreaming"; this.recordingTitle = "dialog.liveStreaming";

View File

@ -1,5 +1,21 @@
/* global $ */ /* global $, APP */
/* jshint -W101 */ /* jshint -W101 */
import UIEvents from "../../../service/UI/UIEvents";
/**
* Store the current ring overlay instance.
* Note: We want to have only 1 instance at a time.
*/
let overlay = null;
/**
* Handler for UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED event.
* @param {boolean} shown indicates whether the avatar on the large video is
* currently displayed or not.
*/
function onAvatarDisplayed(shown) {
overlay._changeBackground(shown);
}
/** /**
* Shows ring overlay * Shows ring overlay
@ -11,28 +27,49 @@ class RingOverlay {
constructor(callee) { constructor(callee) {
this._containerId = 'ringOverlay'; this._containerId = 'ringOverlay';
this._audioContainerId = 'ringOverlayRinging'; this._audioContainerId = 'ringOverlayRinging';
this.isRinging = true;
this.callee = callee; this.callee = callee;
this.render(); this.render();
this.audio = document.getElementById(this._audioContainerId); this.audio = document.getElementById(this._audioContainerId);
this.audio.play(); this.audio.play();
this._setAudioTimeout(); this._setAudioTimeout();
this._timeout = setTimeout(() => {
this.destroy();
this.render();
}, 30000);
}
/**
* Chagnes the background of the ring overlay.
* @param {boolean} solid - if true the new background will be the solid
* one, otherwise the background will be default one.
* NOTE: The method just toggles solidBG css class.
*/
_changeBackground(solid) {
const container = $("#" + this._containerId);
if(solid) {
container.addClass("solidBG");
} else {
container.removeClass("solidBG");
}
} }
/** /**
* Builds and appends the ring overlay to the html document * Builds and appends the ring overlay to the html document
*/ */
_getHtmlStr(callee) { _getHtmlStr(callee) {
let callingLabel = this.isRinging? "<p>Calling...</p>" : "";
let callerStateLabel = this.isRinging? "" : " isn't available";
return ` return `
<div id="${this._containerId}" class='ringing' > <div id="${this._containerId}" class='ringing' >
<div class='ringing__content'> <div class='ringing__content'>
<p>Calling...</p> ${callingLabel}
<img class='ringing__avatar' src="${callee.getAvatarUrl()}" /> <img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
<div class="ringing__caller-info"> <div class="ringing__caller-info">
<p>${callee.getName()}</p> <p>${callee.getName()}${callerStateLabel}</p>
</div> </div>
</div> </div>
<audio id="${this._audioContainerId}" src="/sounds/ring.ogg" /> <audio id="${this._audioContainerId}" src="./sounds/ring.ogg" />
</div>`; </div>`;
} }
@ -49,10 +86,7 @@ class RingOverlay {
* related to the ring overlay. * related to the ring overlay.
*/ */
destroy() { destroy() {
if (this.interval) { this._stopAudio();
clearInterval(this.interval);
}
this._detach(); this._detach();
} }
@ -64,6 +98,16 @@ class RingOverlay {
$(`#${this._containerId}`).remove(); $(`#${this._containerId}`).remove();
} }
_stopAudio() {
this.isRinging = false;
if (this.interval) {
clearInterval(this.interval);
}
if(this._timeout) {
clearTimeout(this._timeout);
}
}
/** /**
* Sets the interval that is going to play the ringing sound. * Sets the interval that is going to play the ringing sound.
*/ */
@ -74,12 +118,6 @@ class RingOverlay {
} }
} }
/**
* Store the current ring overlay instance.
* Note: We want to have only 1 instance at a time.
*/
let overlay = null;
export default { export default {
/** /**
* Shows the ring overlay for the passed callee. * Shows the ring overlay for the passed callee.
@ -92,6 +130,8 @@ export default {
} }
overlay = new RingOverlay(callee); overlay = new RingOverlay(callee);
APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
onAvatarDisplayed);
}, },
/** /**
@ -104,6 +144,8 @@ export default {
} }
overlay.destroy(); overlay.destroy();
overlay = null; overlay = null;
APP.UI.removeListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
onAvatarDisplayed);
return true; return true;
}, },

View File

@ -243,7 +243,7 @@ export default class SharedVideoManager {
let thumb = new SharedVideoThumb(self.url); let thumb = new SharedVideoThumb(self.url);
thumb.setDisplayName(player.getVideoData().title); thumb.setDisplayName(player.getVideoData().title);
VideoLayout.addParticipantContainer(self.url, thumb); VideoLayout.addRemoteVideoContainer(self.url, thumb);
let iframe = player.getIframe(); let iframe = player.getIframe();
self.sharedVideo = new SharedVideoContainer( self.sharedVideo = new SharedVideoContainer(
@ -567,10 +567,6 @@ class SharedVideoContainer extends LargeContainer {
this.player = player; this.player = player;
} }
get $video () {
return this.$iframe;
}
show () { show () {
let self = this; let self = this;
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -3,24 +3,26 @@
import {processReplacements, linkify} from './Replacement'; import {processReplacements, linkify} from './Replacement';
import CommandsProcessor from './Commands'; import CommandsProcessor from './Commands';
import ToolbarToggler from '../../toolbars/ToolbarToggler'; import ToolbarToggler from '../../toolbars/ToolbarToggler';
import VideoLayout from "../../videolayout/VideoLayout";
import UIUtil from '../../util/UIUtil'; import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
var smileys = require("./smileys.json").smileys; import { smileys } from './smileys';
var notificationInterval = false;
var unreadMessages = 0; var unreadMessages = 0;
/**
* The container id, which is and the element id.
*/
var CHAT_CONTAINER_ID = "chat_container";
/** /**
* Shows/hides a visual notification, indicating that a message has arrived. * Updates visual notification, indicating that a message has arrived.
*/ */
function setVisualNotification(show) { function updateVisualNotification() {
var unreadMsgElement = document.getElementById('unreadMessages'); var unreadMsgElement = document.getElementById('unreadMessages');
var glower = $('#toolbar_button_chat');
if (unreadMessages) { if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString(); unreadMsgElement.innerHTML = unreadMessages.toString();
@ -37,28 +39,12 @@ function setVisualNotification(show) {
'style', 'style',
'top:' + topIndent + 'top:' + topIndent +
'; left:' + leftIndent + ';'); '; left:' + leftIndent + ';');
if (!glower.hasClass('icon-chat-simple')) {
glower.removeClass('icon-chat');
glower.addClass('icon-chat-simple');
}
} }
else { else {
unreadMsgElement.innerHTML = ''; unreadMsgElement.innerHTML = '';
glower.removeClass('icon-chat-simple');
glower.addClass('icon-chat');
} }
if (show && !notificationInterval) { $(unreadMsgElement).parent()[unreadMessages > 0 ? 'show' : 'hide']();
notificationInterval = window.setInterval(function () {
glower.toggleClass('active');
}, 800);
}
else if (!show && notificationInterval) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('active');
}
} }
@ -131,7 +117,7 @@ function addSmileys() {
*/ */
function resizeChatConversation() { function resizeChatConversation() {
var msgareaHeight = $('#usermsg').outerHeight(); var msgareaHeight = $('#usermsg').outerHeight();
var chatspace = $('#chat_container'); var chatspace = $('#' + CHAT_CONTAINER_ID);
var width = chatspace.width(); var width = chatspace.width();
var chat = $('#chatconversation'); var chat = $('#chatconversation');
var smileys = $('#smileysarea'); var smileys = $('#smileysarea');
@ -187,13 +173,30 @@ var Chat = {
}; };
usermsg.autosize({callback: onTextAreaResize}); usermsg.autosize({callback: onTextAreaResize});
$("#chat_container").bind("shown", eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
function () { function(containerId, isVisible) {
if (containerId !== CHAT_CONTAINER_ID || !isVisible)
return;
unreadMessages = 0; unreadMessages = 0;
setVisualNotification(false); updateVisualNotification();
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible()) {
ToolbarToggler.dockToolbar(false);
}
// if we are in conversation mode focus on the text input
// if we are not, focus on the display name input
if (APP.settings.getDisplayName())
$('#usermsg').focus();
else
$('#nickinput').focus();
}); });
addSmileys(); addSmileys();
updateVisualNotification();
}, },
/** /**
@ -210,7 +213,7 @@ var Chat = {
if (!Chat.isVisible()) { if (!Chat.isVisible()) {
unreadMessages++; unreadMessages++;
UIUtil.playSoundNotification('chatNotification'); UIUtil.playSoundNotification('chatNotification');
setVisualNotification(true); updateVisualNotification();
} }
} }
@ -271,12 +274,18 @@ var Chat = {
/** /**
* Sets the chat conversation mode. * Sets the chat conversation mode.
* Conversation mode is the normal chat mode, non conversation mode is
* where we ask user to input its display name.
* @param {boolean} isConversationMode if chat should be in * @param {boolean} isConversationMode if chat should be in
* conversation mode or not. * conversation mode or not.
*/ */
setChatConversationMode (isConversationMode) { setChatConversationMode (isConversationMode) {
$('#chat_container') $('#' + CHAT_CONTAINER_ID)
.toggleClass('is-conversation-mode', isConversationMode); .toggleClass('is-conversation-mode', isConversationMode);
// this is needed when we transition from no conversation mode to
// conversation mode. When user enters his nickname and hits enter,
// to focus on the write area.
if (isConversationMode) { if (isConversationMode) {
$('#usermsg').focus(); $('#usermsg').focus();
} }
@ -286,7 +295,7 @@ var Chat = {
* Resizes the chat area. * Resizes the chat area.
*/ */
resizeChat (width, height) { resizeChat (width, height) {
$('#chat_container').width(width).height(height); $('#' + CHAT_CONTAINER_ID).width(width).height(height);
resizeChatConversation(); resizeChatConversation();
}, },
@ -296,7 +305,7 @@ var Chat = {
*/ */
isVisible () { isVisible () {
return UIUtil.isVisible( return UIUtil.isVisible(
document.getElementById("chat_container")); document.getElementById(CHAT_CONTAINER_ID));
}, },
/** /**
* Shows and hides the window with the smileys * Shows and hides the window with the smileys

View File

@ -1,5 +1,5 @@
/* jshint -W101 */ /* jshint -W101 */
var Smileys = require("./smileys.json"); import { regexes } from './smileys';
/** /**
* Processes links and smileys in "body" * Processes links and smileys in "body"
@ -29,7 +29,7 @@ export function linkify(inputText) {
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>'); replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links. //Change email addresses to mailto: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim; replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>'); replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
@ -44,10 +44,9 @@ function smilify(body) {
return body; return body;
} }
var regexs = Smileys.regexs; for(var smiley in regexes) {
for(var smiley in regexs) { if(regexes.hasOwnProperty(smiley)) {
if(regexs.hasOwnProperty(smiley)) { body = body.replace(regexes[smiley],
body = body.replace(regexs[smiley],
'<img class="smiley" src="images/smileys/' + smiley + '.svg">'); '<img class="smiley" src="images/smileys/' + smiley + '.svg">');
} }
} }

View File

@ -0,0 +1,47 @@
export const smileys = {
smiley1: ":)",
smiley2: ":(",
smiley3: ":D",
smiley4: "(y)",
smiley5: " :P",
smiley6: "(wave)",
smiley7: "(blush)",
smiley8: "(chuckle)",
smiley9: "(shocked)",
smiley10: ":*",
smiley11: "(n)",
smiley12: "(search)",
smiley13: " <3",
smiley14: "(oops)",
smiley15: "(angry)",
smiley16: "(angel)",
smiley17: "(sick)",
smiley18: ";(",
smiley19: "(bomb)",
smiley20: "(clap)",
smiley21: " ;)"
};
export const regexes = {
smiley2: /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
smiley3: /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
smiley1: /(:-\)|:\))/gi,
smiley4: /(\(y\)|\(Y\)|\(ok\))/gi,
smiley5: /(:-P|:P|:-p|:p)/gi,
smiley6: /(\(wave\))/gi,
smiley7: /(\(blush\))/gi,
smiley8: /(\(chuckle\))/gi,
smiley9: /(:-0|\(shocked\))/gi,
smiley10: /(:-\*|:\*|\(kiss\))/gi,
smiley11: /(\(n\))/gi,
smiley12: /(\(search\))/g,
smiley13: /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
smiley14: /(\(oops\))/gi,
smiley15: /(\(angry\))/gi,
smiley16: /(\(angel\))/gi,
smiley17: /(\(sick\))/gi,
smiley18: /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
smiley19: /(\(bomb\))/gi,
smiley20: /(\(clap\))/gi,
smiley21: /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
};

View File

@ -1,48 +0,0 @@
{
"smileys": {
"smiley1": ":)",
"smiley2": ":(",
"smiley3": ":D",
"smiley4": "(y)",
"smiley5": " :P",
"smiley6": "(wave)",
"smiley7": "(blush)",
"smiley8": "(chuckle)",
"smiley9": "(shocked)",
"smiley10": ":*",
"smiley11": "(n)",
"smiley12": "(search)",
"smiley13": " <3",
"smiley14": "(oops)",
"smiley15": "(angry)",
"smiley16": "(angel)",
"smiley17": "(sick)",
"smiley18": ";(",
"smiley19": "(bomb)",
"smiley20": "(clap)",
"smiley21": " ;)"
},
"regexs": {
"smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
"smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
"smiley1": /(:-\)|:\))/gi,
"smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
"smiley5": /(:-P|:P|:-p|:p)/gi,
"smiley6": /(\(wave\))/gi,
"smiley7": /(\(blush\))/gi,
"smiley8": /(\(chuckle\))/gi,
"smiley9": /(:-0|\(shocked\))/gi,
"smiley10": /(:-\*|:\*|\(kiss\))/gi,
"smiley11": /(\(n\))/gi,
"smiley12": /(\(search\))/g,
"smiley13": /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
"smiley14": /(\(oops\))/gi,
"smiley15": /(\(angry\))/gi,
"smiley16": /(\(angel\))/gi,
"smiley17": /(\(sick\))/gi,
"smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
"smiley19": /(\(bomb\))/gi,
"smiley20": /(\(clap\))/gi,
"smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
}
}

View File

@ -20,13 +20,11 @@ function updateNumberOfParticipants(delta) {
return; return;
} }
let buttonIndicatorText = (numberOfContacts === 1) ? '' : numberOfContacts; $("#numberOfParticipants").text(numberOfContacts);
$("#numberOfParticipants").text(buttonIndicatorText);
$("#contacts_container>div.title").text( $("#contacts_container>div.title").text(
APP.translation.translateString( APP.translation.translateString("contactlist")
"contactlist", {participants: numberOfContacts} + ' (' + numberOfContacts + ')');
));
} }
/** /**
@ -59,16 +57,6 @@ function createDisplayNameParagraph(key, displayName) {
return p; return p;
} }
function stopGlowing(glower) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('glowing');
if (!ContactList.isVisible()) {
glower.removeClass('active');
}
}
function getContactEl (id) { function getContactEl (id) {
return $(`#contacts>li[id="${id}"]`); return $(`#contacts>li[id="${id}"]`);
} }
@ -96,9 +84,9 @@ var ContactList = {
/** /**
* Adds a contact for the given id. * Adds a contact for the given id.
* * @param isLocal is an id for the local user.
*/ */
addContact (id) { addContact (id, isLocal) {
let contactlist = $('#contacts'); let contactlist = $('#contacts');
let newContact = document.createElement('li'); let newContact = document.createElement('li');
@ -112,7 +100,11 @@ var ContactList = {
if (interfaceConfig.SHOW_CONTACTLIST_AVATARS) if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
newContact.appendChild(createAvatar(id)); newContact.appendChild(createAvatar(id));
newContact.appendChild(createDisplayNameParagraph("participant"));
newContact.appendChild(
createDisplayNameParagraph(
isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
if (APP.conference.isLocalId(id)) { if (APP.conference.isLocalId(id)) {
contactlist.prepend(newContact); contactlist.prepend(newContact);

View File

@ -74,10 +74,7 @@ export default {
} }
}); });
// Only show the subtitle if this isn't the only setting section. UIUtil.showElement("deviceOptionsTitle");
if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
UIUtil.showElement("deviceOptionsTitle");
UIUtil.showElement("devicesOptions"); UIUtil.showElement("devicesOptions");
} }
@ -150,8 +147,7 @@ export default {
showStartMutedOptions (show) { showStartMutedOptions (show) {
if (show && UIUtil.isSettingEnabled('moderator')) { if (show && UIUtil.isSettingEnabled('moderator')) {
// Only show the subtitle if this isn't the only setting section. // Only show the subtitle if this isn't the only setting section.
if (!$("#moderatorOptionsTitle").is(":visible") if (!$("#moderatorOptionsTitle").is(":visible"))
&& interfaceConfig.SETTINGS_SECTIONS.length > 1)
UIUtil.showElement("moderatorOptionsTitle"); UIUtil.showElement("moderatorOptionsTitle");
UIUtil.showElement("startMutedOptions"); UIUtil.showElement("startMutedOptions");

View File

@ -7,7 +7,6 @@ import SideContainerToggler from "../side_pannels/SideContainerToggler";
let roomUrl = null; let roomUrl = null;
let emitter = null; let emitter = null;
/** /**
* Opens the invite link dialog. * Opens the invite link dialog.
*/ */
@ -21,36 +20,46 @@ function openLinkDialog () {
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\""; inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
} }
let inviteLinkId = "inviteLinkRef";
let focusInviteLink = function() {
$('#' + inviteLinkId).focus();
$('#' + inviteLinkId).select();
};
let title = APP.translation.generateTranslationHTML("dialog.shareLink"); let title = APP.translation.generateTranslationHTML("dialog.shareLink");
APP.UI.messageHandler.openTwoButtonDialog( APP.UI.messageHandler.openTwoButtonDialog(
null, null, null, null, title, null,
'<h2>' + title + '</h2>' '<input id="' + inviteLinkId + '" type="text" '
+ '<input id="inviteLinkRef" type="text" ' + inviteAttributes + ' readonly/>',
+ inviteAttributes + ' onclick="this.select();" readonly>', false, "dialog.copy",
false, "dialog.Invite",
function (e, v) { function (e, v) {
if (v && roomUrl) { if (v && roomUrl) {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.button'); JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
emitter.emit(UIEvents.USER_INVITED, roomUrl);
focusInviteLink();
document.execCommand('copy');
} }
else { else {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel'); JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
} }
}, },
function (event) { function (event) {
if (roomUrl) { if (!roomUrl) {
document.getElementById('inviteLinkRef').select();
} else {
if (event && event.target) { if (event && event.target) {
$(event.target).find('button[value=true]') $(event.target).find('button[value=true]')
.prop('disabled', true); .prop('disabled', true);
} }
} }
else {
focusInviteLink();
}
}, },
function (e, v, m, f) { function (e, v, m, f) {
if(!v && !m && !f) if(!v && !m && !f)
JitsiMeetJS.analytics.sendEvent('toolbar.invite.close'); JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
} },
'Copy' // Focus Copy button.
); );
} }
@ -181,6 +190,7 @@ const buttonHandlers = {
const defaultToolbarButtons = { const defaultToolbarButtons = {
'microphone': { 'microphone': {
id: 'toolbar_button_mute', id: 'toolbar_button_mute',
tooltipKey: 'toolbar.mute',
className: "button icon-microphone", className: "button icon-microphone",
shortcut: 'M', shortcut: 'M',
shortcutAttr: 'mutePopover', shortcutAttr: 'mutePopover',
@ -211,6 +221,7 @@ const defaultToolbarButtons = {
}, },
'camera': { 'camera': {
id: 'toolbar_button_camera', id: 'toolbar_button_camera',
tooltipKey: 'toolbar.videomute',
className: "button icon-camera", className: "button icon-camera",
shortcut: 'V', shortcut: 'V',
shortcutAttr: 'toggleVideoPopover', shortcutAttr: 'toggleVideoPopover',
@ -224,6 +235,7 @@ const defaultToolbarButtons = {
}, },
'desktop': { 'desktop': {
id: 'toolbar_button_desktopsharing', id: 'toolbar_button_desktopsharing',
tooltipKey: 'toolbar.sharescreen',
className: 'button icon-share-desktop', className: 'button icon-share-desktop',
shortcut: 'D', shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover', shortcutAttr: 'toggleDesktopSharingPopover',
@ -236,16 +248,19 @@ const defaultToolbarButtons = {
i18n: '[content]toolbar.sharescreen' i18n: '[content]toolbar.sharescreen'
}, },
'security': { 'security': {
id: 'toolbar_button_security' id: 'toolbar_button_security',
tooltipKey: 'toolbar.lock'
}, },
'invite': { 'invite': {
id: 'toolbar_button_link', id: 'toolbar_button_link',
tooltipKey: 'toolbar.invite',
className: 'button icon-link', className: 'button icon-link',
content: 'Invite others', content: 'Invite others',
i18n: '[content]toolbar.invite' i18n: '[content]toolbar.invite'
}, },
'chat': { 'chat': {
id: 'toolbar_button_chat', id: 'toolbar_button_chat',
tooltipKey: 'toolbar.chat',
shortcut: 'C', shortcut: 'C',
shortcutAttr: 'toggleChatPopover', shortcutAttr: 'toggleChatPopover',
shortcutFunc: function() { shortcutFunc: function() {
@ -257,40 +272,47 @@ const defaultToolbarButtons = {
}, },
'contacts': { 'contacts': {
id: 'toolbar_contact_list', id: 'toolbar_contact_list',
tooltipKey: 'bottomtoolbar.contactlist',
sideContainerId: 'contacts_container' sideContainerId: 'contacts_container'
}, },
'profile': { 'profile': {
id: 'toolbar_button_profile', id: 'toolbar_button_profile',
tooltipKey: 'profile.setDisplayNameLabel',
sideContainerId: 'profile_container' sideContainerId: 'profile_container'
}, },
'etherpad': { 'etherpad': {
id: 'toolbar_button_etherpad' id: 'toolbar_button_etherpad',
tooltipKey: 'toolbar.etherpad',
}, },
'fullscreen': { 'fullscreen': {
id: 'toolbar_button_fullScreen', id: 'toolbar_button_fullScreen',
tooltipKey: 'toolbar.fullscreen',
className: "button icon-full-screen", className: "button icon-full-screen",
shortcut: 'F', shortcut: 'S',
shortcutAttr: 'toggleFullscreenPopover', shortcutAttr: 'toggleFullscreenPopover',
shortcutFunc: function() { shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled'); JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
APP.UI.toggleFullScreen(); APP.UI.toggleFullScreen();
}, },
shortcutDescription: "keyboardShortcuts.toggleChat", shortcutDescription: "keyboardShortcuts.fullScreen",
content: "Enter / Exit Full Screen", content: "Enter / Exit Full Screen",
i18n: "[content]toolbar.fullscreen" i18n: "[content]toolbar.fullscreen"
}, },
'settings': { 'settings': {
id: 'toolbar_button_settings', id: 'toolbar_button_settings',
tooltipKey: 'toolbar.Settings',
sideContainerId: "settings_container" sideContainerId: "settings_container"
}, },
'hangup': { 'hangup': {
id: 'toolbar_button_hangup', id: 'toolbar_button_hangup',
tooltipKey: 'toolbar.hangup',
className: "button icon-hangup", className: "button icon-hangup",
content: "Hang Up", content: "Hang Up",
i18n: "[content]toolbar.hangup" i18n: "[content]toolbar.hangup"
}, },
'filmstrip': { 'filmstrip': {
id: 'toolbar_film_strip', id: 'toolbar_film_strip',
tooltipKey: 'toolbar.filmstrip',
shortcut: "F", shortcut: "F",
shortcutAttr: "filmstripPopover", shortcutAttr: "filmstripPopover",
shortcutFunc: function() { shortcutFunc: function() {
@ -301,6 +323,7 @@ const defaultToolbarButtons = {
}, },
'raisehand': { 'raisehand': {
id: "toolbar_button_raisehand", id: "toolbar_button_raisehand",
tooltipKey: 'toolbar.raiseHand',
className: "button icon-raised-hand", className: "button icon-raised-hand",
shortcut: "R", shortcut: "R",
shortcutAttr: "raiseHandPopover", shortcutAttr: "raiseHandPopover",
@ -357,7 +380,17 @@ const Toolbar = {
Object.keys(defaultToolbarButtons).forEach( Object.keys(defaultToolbarButtons).forEach(
id => { id => {
if (UIUtil.isButtonEnabled(id)) { if (UIUtil.isButtonEnabled(id)) {
var button = defaultToolbarButtons[id]; let button = defaultToolbarButtons[id];
let buttonElement = document.getElementById(button.id);
let tooltipPosition
= (interfaceConfig.MAIN_TOOLBAR_BUTTONS
.indexOf(id) > -1)
? "bottom" : "right";
UIUtil.setTooltip( buttonElement,
button.tooltipKey,
tooltipPosition);
if (button.shortcut) if (button.shortcut)
APP.keyboardshortcut.registerShortcut( APP.keyboardshortcut.registerShortcut(
@ -382,8 +415,15 @@ const Toolbar = {
isVisible); isVisible);
}); });
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
function(isRaisedHand) {
Toolbar._toggleRaiseHand(isRaisedHand);
});
if(!APP.tokenData.isGuest) { if(!APP.tokenData.isGuest) {
$("#toolbar_button_profile").addClass("unclickable"); $("#toolbar_button_profile").addClass("unclickable");
UIUtil.removeTooltip(
document.getElementById('toolbar_button_profile'));
} }
}, },
/** /**
@ -458,9 +498,11 @@ const Toolbar = {
// Shows or hides the 'shared video' button. // Shows or hides the 'shared video' button.
showSharedVideoButton () { showSharedVideoButton () {
let $element = $('#toolbar_button_sharedvideo');
if (UIUtil.isButtonEnabled('sharedvideo') if (UIUtil.isButtonEnabled('sharedvideo')
&& config.disableThirdPartyRequests !== true) { && config.disableThirdPartyRequests !== true) {
$('#toolbar_button_sharedvideo').css({display: "inline-block"}); $element.css({display: "inline-block"});
UIUtil.setTooltip($element.get(0), 'toolbar.sharedvideo', 'right');
} else { } else {
$('#toolbar_button_sharedvideo').css({display: "none"}); $('#toolbar_button_sharedvideo').css({display: "none"});
} }
@ -545,6 +587,13 @@ const Toolbar = {
} }
}, },
/**
* Toggles / untoggles the view for raised hand.
*/
_toggleRaiseHand(isRaisedHand) {
$('#toolbar_button_raisehand').toggleClass("glow", isRaisedHand);
},
/** /**
* Marks video icon as muted or not. * Marks video icon as muted or not.
* @param {boolean} muted if icon should look like muted or not * @param {boolean} muted if icon should look like muted or not
@ -750,7 +799,6 @@ const Toolbar = {
buttonElement.setAttribute("data-i18n", button.i18n); buttonElement.setAttribute("data-i18n", button.i18n);
buttonElement.setAttribute("data-container", "body"); buttonElement.setAttribute("data-container", "body");
buttonElement.setAttribute("data-toggle", "popover");
buttonElement.setAttribute("data-placement", "bottom"); buttonElement.setAttribute("data-placement", "bottom");
this._addPopups(buttonElement, button.popups); this._addPopups(buttonElement, button.popups);
@ -771,4 +819,4 @@ const Toolbar = {
} }
}; };
export default Toolbar; export default Toolbar;

View File

@ -1,4 +1,22 @@
/* global $, config, interfaceConfig */ /* global $, APP, config, AJS, interfaceConfig */
import KeyboardShortcut from '../../keyboardshortcut/keyboardshortcut';
/**
* Associates tooltip element position (in the terms of
* {@link UIUtil#setTooltip} which do not look like CSS <tt>position</tt>) with
* AUI tooltip <tt>gravity</tt>.
*/
const TOOLTIP_POSITIONS = {
'bottom': 'n',
'bottom-left': 'ne',
'bottom-right': 'nw',
'left': 'e',
'right': 'w',
'top': 's',
'top-left': 'se',
'top-right': 'sw'
};
/** /**
* Created by hristo on 12/22/14. * Created by hristo on 12/22/14.
@ -82,12 +100,71 @@
context.putImageData(imgData, 0, 0); context.putImageData(imgData, 0, 0);
}, },
/**
* Sets a global handler for all tooltips. Once invoked, create a new
* tooltip by merely updating a DOM node with the appropriate class (e.g.
* <tt>tooltip-n</tt>) and the attribute <tt>content</tt>.
*/
activateTooltips() {
AJS.$('[data-tooltip]').tooltip({
gravity() {
return this.getAttribute('data-tooltip');
},
title() {
return this.getAttribute('content');
},
html: true, // Handle multiline tooltips.
// The following two prevent tooltips from being stuck:
hoverable: false, // Make custom tooltips behave like native ones.
live: true // Attach listener to document element.
});
},
/**
* Sets the tooltip to the given element.
*
* @param element the element to set the tooltip to
* @param key the tooltip data-i18n key
* @param position the position of the tooltip in relation to the element
*/
setTooltip: function (element, key, position) { setTooltip: function (element, key, position) {
element.setAttribute("data-i18n", "[data-content]" + key); element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
element.setAttribute("data-toggle", "popover"); element.setAttribute('data-i18n', '[content]' + key);
element.setAttribute("data-placement", position);
element.setAttribute("data-html", true); APP.translation.translateElement($(element));
element.setAttribute("data-container", "body"); },
/**
* Removes the tooltip to the given element.
*
* @param element the element to remove the tooltip from
*/
removeTooltip: function (element) {
AJS.$(element).tooltip('destroy');
element.setAttribute('data-tooltip', '');
element.setAttribute('data-i18n','');
element.setAttribute('content','');
element.setAttribute('shortcut','');
},
/**
* Internal util function for generating tooltip title.
*
* @param element
* @returns {string|*}
* @private
*/
_getTooltipText: function (element) {
let title = element.getAttribute('content');
let shortcut = element.getAttribute('shortcut');
if(shortcut) {
let shortcutString = KeyboardShortcut.getShortcutTooltip(shortcut);
title += ` ${shortcutString}`;
}
return title;
}, },
/** /**
@ -233,6 +310,23 @@
*/ */
parseCssInt(cssValue) { parseCssInt(cssValue) {
return parseInt(cssValue) || 0; return parseInt(cssValue) || 0;
},
/**
* Adds href value to 'a' link jquery object. If link value is null,
* undefined or empty string, disables the link.
* @param {object} aLinkElement the jquery object
* @param {string} link the link value
*/
setLinkHref(aLinkElement, link) {
if (link) {
aLinkElement.attr('href', link);
} else {
aLinkElement.css({
"pointer-events": "none",
"cursor": "default"
});
}
} }
}; };

View File

@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
}; };
function createIcon(classes) { function createIcon(classes, iconClass) {
var icon = document.createElement("span"); var icon = document.createElement("span");
for(var i in classes) { for(var i in classes) {
icon.classList.add(classes[i]); icon.classList.add(classes[i]);
} }
icon.appendChild( icon.appendChild(
document.createElement("i")).classList.add("icon-connection"); document.createElement("i")).classList.add(iconClass);
return icon; return icon;
} }
@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
}.bind(this); }.bind(this);
this.emptyIcon = this.connectionIndicatorContainer.appendChild( this.emptyIcon = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_empty"])); createIcon(["connection", "connection_empty"], "icon-connection"));
this.fullIcon = this.connectionIndicatorContainer.appendChild( this.fullIcon = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_full"])); createIcon(["connection", "connection_full"], "icon-connection"));
this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_lost"],"icon-connection-lost"));
$(this.interruptedIndicator).hide();
}; };
/** /**
@ -298,6 +301,27 @@ ConnectionIndicator.prototype.remove = function() {
this.popover.forceHide(); this.popover.forceHide();
}; };
/**
* Updates the UI which displays warning about user's connectivity problems.
*
* @param {boolean} isActive true if the connection is working fine or false if
* the user is having connectivity issues.
*/
ConnectionIndicator.prototype.updateConnectionStatusIndicator
= function (isActive) {
this.isConnectionActive = isActive;
if (this.isConnectionActive) {
$(this.interruptedIndicator).hide();
$(this.emptyIcon).show();
$(this.fullIcon).show();
} else {
$(this.interruptedIndicator).show();
$(this.emptyIcon).hide();
$(this.fullIcon).hide();
this.updateConnectionQuality(0 /* zero bars */);
}
};
/** /**
* Updates the data of the indicator * Updates the data of the indicator
* @param percent the percent of connection quality * @param percent the percent of connection quality
@ -312,15 +336,16 @@ ConnectionIndicator.prototype.updateConnectionQuality =
} else { } else {
if(this.connectionIndicatorContainer.style.display == "none") { if(this.connectionIndicatorContainer.style.display == "none") {
this.connectionIndicatorContainer.style.display = "block"; this.connectionIndicatorContainer.style.display = "block";
this.videoContainer.updateIconPositions();
} }
} }
this.bandwidth = object.bandwidth; if (object) {
this.bitrate = object.bitrate; this.bandwidth = object.bandwidth;
this.packetLoss = object.packetLoss; this.bitrate = object.bitrate;
this.transport = object.transport; this.packetLoss = object.packetLoss;
if (object.resolution) { this.transport = object.transport;
this.resolution = object.resolution; if (object.resolution) {
this.resolution = object.resolution;
}
} }
for (var quality in ConnectionIndicator.connectionQualityValues) { for (var quality in ConnectionIndicator.connectionQualityValues) {
if (percent >= quality) { if (percent >= quality) {
@ -328,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
ConnectionIndicator.connectionQualityValues[quality]; ConnectionIndicator.connectionQualityValues[quality];
} }
} }
if (object.isResolutionHD) { if (object && typeof object.isResolutionHD === 'boolean') {
this.isResolutionHD = object.isResolutionHD; this.isResolutionHD = object.isResolutionHD;
} }
this.updateResolutionIndicator(); this.updateResolutionIndicator();

View File

@ -3,8 +3,6 @@
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
const thumbAspectRatio = 1 / 1;
const FilmStrip = { const FilmStrip = {
/** /**
* *
@ -26,7 +24,7 @@ const FilmStrip = {
*/ */
toggleFilmStrip (visible) { toggleFilmStrip (visible) {
if (typeof visible === 'boolean' if (typeof visible === 'boolean'
&& this.isFilmStripVisible() == visible) { && this.isFilmStripVisible() == visible) {
return; return;
} }
@ -36,8 +34,8 @@ const FilmStrip = {
var eventEmitter = this.eventEmitter; var eventEmitter = this.eventEmitter;
if (eventEmitter) { if (eventEmitter) {
eventEmitter.emit( eventEmitter.emit(
UIEvents.TOGGLED_FILM_STRIP, UIEvents.TOGGLED_FILM_STRIP,
this.isFilmStripVisible()); this.isFilmStripVisible());
} }
}, },
@ -66,13 +64,52 @@ const FilmStrip = {
- parseInt(this.filmStrip.css('paddingRight'), 10); - parseInt(this.filmStrip.css('paddingRight'), 10);
}, },
/** calculateThumbnailSize() {
* Calculates the thumbnail size. let availableSizes = this.calculateAvailableSize();
*/ let width = availableSizes.availableWidth;
calculateThumbnailSize () { let height = availableSizes.availableHeight;
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let numvids = this.getThumbs(true).length; return this.calculateThumbnailSizeFromAvailable(width, height);
},
/**
* Normalizes local and remote thumbnail ratios
*/
normalizeThumbnailRatio () {
let remoteHeightRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_HEIGHT;
let remoteWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_WIDTH;
let localHeightRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_HEIGHT;
let localWidthRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_WIDTH;
let commonHeightRatio = remoteHeightRatio * localHeightRatio;
let localRatioCoefficient = localWidthRatio / localHeightRatio;
let remoteRatioCoefficient = remoteWidthRatio / remoteHeightRatio;
remoteWidthRatio = commonHeightRatio * remoteRatioCoefficient;
remoteHeightRatio = commonHeightRatio;
localWidthRatio = commonHeightRatio * localRatioCoefficient;
localHeightRatio = commonHeightRatio;
let localRatio = {
widthRatio: localWidthRatio,
heightRatio: localHeightRatio
};
let remoteRatio = {
widthRatio: remoteWidthRatio,
heightRatio: remoteHeightRatio
};
return { localRatio, remoteRatio };
},
calculateAvailableSize() {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let thumbs = this.getThumbs(true);
let numvids = thumbs.remoteThumbs.length;
let localVideoContainer = $("#localVideoContainer"); let localVideoContainer = $("#localVideoContainer");
@ -83,20 +120,19 @@ const FilmStrip = {
*/ */
let videoAreaAvailableWidth let videoAreaAvailableWidth
= UIUtil.getAvailableVideoWidth() = UIUtil.getAvailableVideoWidth()
- UIUtil.parseCssInt(this.filmStrip.css('right'), 10) - UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10) - UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10) - UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10) - UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10) - UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
- 5; - 5;
let availableWidth = videoAreaAvailableWidth; let availableWidth = videoAreaAvailableWidth;
// If the number of videos is 0 or undefined we don't need to calculate // If local thumb is not hidden
// further. if(thumbs.localThumb) {
if (numvids)
availableWidth = Math.floor( availableWidth = Math.floor(
(videoAreaAvailableWidth - numvids * ( (videoAreaAvailableWidth - (
UIUtil.parseCssInt( UIUtil.parseCssInt(
localVideoContainer.css('borderLeftWidth'), 10) localVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt( + UIUtil.parseCssInt(
@ -109,45 +145,99 @@ const FilmStrip = {
localVideoContainer.css('marginLeft'), 10) localVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt( + UIUtil.parseCssInt(
localVideoContainer.css('marginRight'), 10))) localVideoContainer.css('marginRight'), 10)))
/ numvids); );
}
// If the number of videos is 0 or undefined we don't need to calculate
// further.
if (numvids) {
let remoteVideoContainer = thumbs.remoteThumbs.eq(0);
availableWidth = Math.floor(
(videoAreaAvailableWidth - numvids * (
UIUtil.parseCssInt(
remoteVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('borderRightWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingRight'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginRight'), 10)))
);
}
let maxHeight let maxHeight
// If the MAX_HEIGHT property hasn't been specified // If the MAX_HEIGHT property hasn't been specified
// we have the static value. // we have the static value.
= Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120, = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight); availableHeight);
availableHeight availableHeight
= Math.min( maxHeight, window.innerHeight - 18); = Math.min(maxHeight, window.innerHeight - 18);
if (availableHeight < availableWidth) { return { availableWidth, availableHeight };
availableWidth = availableHeight; },
calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
let { localRatio, remoteRatio } = this.normalizeThumbnailRatio();
let { remoteThumbs } = this.getThumbs(true);
let remoteProportion = remoteRatio.widthRatio * remoteThumbs.length;
let widthProportion = remoteProportion + localRatio.widthRatio;
let heightUnit = availableHeight / localRatio.heightRatio;
let widthUnit = availableWidth / widthProportion;
if (heightUnit < widthUnit) {
widthUnit = heightUnit;
} }
else else
availableHeight = availableWidth; heightUnit = widthUnit;
let localVideo = {
thumbWidth: widthUnit * localRatio.widthRatio,
thumbHeight: heightUnit * localRatio.heightRatio
};
let remoteVideo = {
thumbWidth: widthUnit * remoteRatio.widthRatio,
thumbHeight: widthUnit * remoteRatio.heightRatio
};
return { return {
thumbWidth: availableWidth, localVideo,
thumbHeight: availableHeight remoteVideo
}; };
}, },
resizeThumbnails (thumbWidth, thumbHeight, resizeThumbnails (local, remote,
animate = false, forceUpdate = false) { animate = false, forceUpdate = false) {
return new Promise(resolve => { return new Promise(resolve => {
this.getThumbs(!forceUpdate).animate({ let thumbs = this.getThumbs(!forceUpdate);
height: thumbHeight, if(thumbs.localThumb)
width: thumbWidth thumbs.localThumb.animate({
}, { height: local.thumbHeight,
queue: false, width: local.thumbWidth
duration: animate ? 500 : 0, }, {
complete: resolve queue: false,
}); duration: animate ? 500 : 0,
complete: resolve
});
if(thumbs.remoteThumbs)
thumbs.remoteThumbs.animate({
height: remote.thumbHeight,
width: remote.thumbWidth
}, {
queue: false,
duration: animate ? 500 : 0,
complete: resolve
});
this.filmStrip.animate({ this.filmStrip.animate({
// adds 2 px because of small video 1px border // adds 2 px because of small video 1px border
height: thumbHeight + 2 height: remote.thumbHeight + 2
}, { }, {
queue: false, queue: false,
duration: animate ? 500 : 0 duration: animate ? 500 : 0
@ -165,13 +255,19 @@ const FilmStrip = {
selector += ':visible'; selector += ':visible';
} }
let localThumb = $("#localVideoContainer");
let remoteThumbs = this.filmStrip.children(selector)
.not("#localVideoContainer");
// Exclude the local video container if it has been hidden. // Exclude the local video container if it has been hidden.
if ($("#localVideoContainer").hasClass("hidden")) if (localThumb.hasClass("hidden")) {
return this.filmStrip.children(selector) return { remoteThumbs };
.not("#localVideoContainer"); } else {
else return { remoteThumbs, localThumb };
return this.filmStrip.children(selector); }
} }
}; };
export default FilmStrip; export default FilmStrip;

View File

@ -0,0 +1,501 @@
/* global $, APP, interfaceConfig */
/* jshint -W101 */
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import UIUtil from "../util/UIUtil";
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import LargeContainer from "./LargeContainer";
import AudioLevels from "../audio_levels/AudioLevels";
/**
* Manager for all Large containers.
*/
export default class LargeVideoManager {
constructor (emitter) {
/**
* The map of <tt>LargeContainer</tt>s where the key is the video
* container type.
* @type {Object.<string, LargeContainer>}
*/
this.containers = {};
this.state = VIDEO_CONTAINER_TYPE;
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle and desktop tracks
this.addContainer("desktop", this.videoContainer);
this.width = 0;
this.height = 0;
this.$container = $('#largeVideoContainer');
this.$container.css({
display: 'inline-block'
});
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
let leftWatermarkDiv
= this.$container.find("div.watermark.leftwatermark");
leftWatermarkDiv.css({display: 'block'});
UIUtil.setLinkHref(
leftWatermarkDiv.parent(),
interfaceConfig.JITSI_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
let rightWatermarkDiv
= this.$container.find("div.watermark.rightwatermark");
rightWatermarkDiv.css({
display: 'block',
backgroundImage: 'url(images/rightwatermark.png)'
});
UIUtil.setLinkHref(
rightWatermarkDiv.parent(),
interfaceConfig.BRAND_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_POWERED_BY) {
this.$container.children("a.poweredby").css({display: 'block'});
}
this.$container.hover(
e => this.onHoverIn(e),
e => this.onHoverOut(e)
);
}
onHoverIn (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverIn(e);
}
onHoverOut (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverOut(e);
}
/**
* Called when the media connection has been interrupted.
*/
onVideoInterrupted () {
this.enableLocalConnectionProblemFilter(true);
this._setLocalConnectionMessage("connection.RECONNECTING");
// Show the message only if the video is currently being displayed
this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
}
/**
* Called when the media connection has been restored.
*/
onVideoRestored () {
this.enableLocalConnectionProblemFilter(false);
this.showLocalConnectionMessage(false);
}
get id () {
let container = this.getContainer(this.state);
return container.id;
}
scheduleLargeVideoUpdate () {
if (this.updateInProcess || !this.newStreamData) {
return;
}
this.updateInProcess = true;
let container = this.getContainer(this.state);
// Include hide()/fadeOut only if we're switching between users
let preUpdate;
let isUserSwitch = this.newStreamData.id != this.id;
if (isUserSwitch) {
preUpdate = container.hide();
} else {
preUpdate = Promise.resolve();
}
preUpdate.then(() => {
let {id, stream, videoType, resolve} = this.newStreamData;
this.newStreamData = null;
console.info("hover in %s", id);
this.state = videoType;
let container = this.getContainer(this.state);
container.setStream(stream, videoType);
// change the avatar url on large
this.updateAvatar(Avatar.getAvatarUrl(id));
// FIXME that does not really make sense, because the videoType
// (camera or desktop) is a completely different thing than
// the video container type (Etherpad, SharedVideo, VideoContainer).
// ----------------------------------------------------------------
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false
let showAvatar = false;
if (videoType == VIDEO_CONTAINER_TYPE)
showAvatar = stream ? stream.isMuted() : true;
// If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if
// there was a user switch(image is lost on stream detach) or if
// the video was not rendered, before the connection has failed.
let isHavingConnectivityIssues
= APP.conference.isParticipantConnectionActive(id) === false;
if (isHavingConnectivityIssues
&& (isUserSwitch | !container.wasVideoRendered)) {
showAvatar = true;
}
let promise;
// do not show stream if video is muted
// but we still should show watermark
if (showAvatar) {
this.showWatermark(true);
// If the intention of this switch is to show the avatar
// we need to make sure that the video is hidden
promise = container.hide();
} else {
promise = container.show();
}
// show the avatar on large if needed
container.showAvatar(showAvatar);
// Make sure no notification about remote failure is shown as
// it's UI conflicts with the one for local connection interrupted.
if (APP.conference.isConnectionInterrupted()) {
this.updateParticipantConnStatusIndication(id, true);
} else {
this.updateParticipantConnStatusIndication(
id, !isHavingConnectivityIssues);
}
// resolve updateLargeVideo promise after everything is done
promise.then(resolve);
return promise;
}).then(() => {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.scheduleLargeVideoUpdate();
});
}
/**
* Shows/hides notification about participant's connectivity issues to be
* shown on the large video area.
*
* @param {string} id the id of remote participant(MUC nickname)
* @param {boolean} isConnected true if the connection is active or false
* when the user is having connectivity issues.
*
* @private
*/
updateParticipantConnStatusIndication (id, isConnected) {
// Apply grey filter on the large video
this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
if (isConnected) {
// Hide the message
this.showRemoteConnectionMessage(false);
} else {
// Get user's display name
let displayName
= APP.conference.getParticipantDisplayName(id);
this._setRemoteConnectionMessage(
"connection.USER_CONNECTION_INTERRUPTED",
{ displayName: displayName });
// Show it now only if the VideoContainer is on top
this.showRemoteConnectionMessage(
this.state === VIDEO_CONTAINER_TYPE);
}
}
/**
* Update large video.
* Switches to large video even if previously other container was visible.
* @param userID the userID of the participant associated with the stream
* @param {JitsiTrack?} stream new stream
* @param {string?} videoType new video type
* @returns {Promise}
*/
updateLargeVideo (userID, stream, videoType) {
if (this.newStreamData) {
this.newStreamData.reject();
}
this.newStreamData = createDeferred();
this.newStreamData.id = userID;
this.newStreamData.stream = stream;
this.newStreamData.videoType = videoType;
this.scheduleLargeVideoUpdate();
return this.newStreamData.promise;
}
/**
* Update container size.
*/
updateContainerSize () {
this.width = UIUtil.getAvailableVideoWidth();
this.height = window.innerHeight;
}
/**
* Resize Large container of specified type.
* @param {string} type type of container which should be resized.
* @param {boolean} [animate=false] if resize process should be animated.
*/
resizeContainer (type, animate = false) {
let container = this.getContainer(type);
container.resize(this.width, this.height, animate);
}
/**
* Resize all Large containers.
* @param {boolean} animate if resize process should be animated.
*/
resize (animate) {
// resize all containers
Object.keys(this.containers)
.forEach(type => this.resizeContainer(type, animate));
this.$container.animate({
width: this.width,
height: this.height
}, {
queue: false,
duration: animate ? 500 : 0
});
}
/**
* Enables/disables the filter indicating a video problem to the user caused
* by the problems with local media connection.
*
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
*/
enableLocalConnectionProblemFilter (enable) {
this.videoContainer.enableLocalConnectionProblemFilter(enable);
}
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar (avatarUrl) {
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
}
/**
* Updates the audio level indicator of the large video.
*
* @param lvl the new audio level to set
*/
updateLargeVideoAudioLevel (lvl) {
AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
}
/**
* Show or hide watermark.
* @param {boolean} show
*/
showWatermark (show) {
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
}
/**
* Shows/hides the message indicating problems with local media connection.
* @param {boolean|null} show(optional) tells whether the message is to be
* displayed or not. If missing the condition will be based on the value
* obtained from {@link APP.conference.isConnectionInterrupted}.
*/
showLocalConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isConnectionInterrupted();
}
if (show) {
$('#localConnectionMessage').css({display: "block"});
// Avatar message conflicts with 'videoConnectionMessage',
// so it must be hidden
this.showRemoteConnectionMessage(false);
} else {
$('#localConnectionMessage').css({display: "none"});
}
}
/**
* Shows hides the "avatar" message which is to be displayed either in
* the middle of the screen or below the avatar image.
*
* @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
* message or <tt>false</tt> to hide it. If not provided then the connection
* status of the user currently on the large video will be obtained form
* "APP.conference" and the message will be displayed if the user's
* connection is interrupted.
*/
showRemoteConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isParticipantConnectionActive(this.id);
}
if (show) {
$('#remoteConnectionMessage').css({display: "block"});
// 'videoConnectionMessage' message conflicts with 'avatarMessage',
// so it must be hidden
this.showLocalConnectionMessage(false);
} else {
$('#remoteConnectionMessage').hide();
}
}
/**
* Updates the text which describes that the remote user is having
* connectivity issues.
*
* @param {string} msgKey the translation key which will be used to get
* the message text.
* @param {object} msgOptions translation options object.
*
* @private
*/
_setRemoteConnectionMessage (msgKey, msgOptions) {
if (msgKey) {
let text = APP.translation.translateString(msgKey, msgOptions);
$('#remoteConnectionMessage')
.attr("data-i18n", msgKey).text(text);
}
this.videoContainer.positionRemoteConnectionMessage();
}
/**
* Updated the text which is to be shown on the top of large video, when
* local media connection is interrupted.
*
* @param {string} msgKey the translation key which will be used to get
* the message text to be displayed on the large video.
* @param {object} msgOptions translation options object
*
* @private
*/
_setLocalConnectionMessage (msgKey, msgOptions) {
$('#localConnectionMessage')
.attr("data-i18n", msgKey)
.text(APP.translation.translateString(msgKey, msgOptions));
}
/**
* Add container of specified type.
* @param {string} type container type
* @param {LargeContainer} container container to add.
*/
addContainer (type, container) {
if (this.containers[type]) {
throw new Error(`container of type ${type} already exist`);
}
this.containers[type] = container;
this.resizeContainer(type);
}
/**
* Get Large container of specified type.
* @param {string} type container type.
* @returns {LargeContainer}
*/
getContainer (type) {
let container = this.containers[type];
if (!container) {
throw new Error(`container of type ${type} doesn't exist`);
}
return container;
}
/**
* Remove Large container of specified type.
* @param {string} type container type.
*/
removeContainer (type) {
if (!this.containers[type]) {
throw new Error(`container of type ${type} doesn't exist`);
}
delete this.containers[type];
}
/**
* Show Large container of specified type.
* Does nothing if such container is already visible.
* @param {string} type container type.
* @returns {Promise}
*/
showContainer (type) {
if (this.state === type) {
return Promise.resolve();
}
let oldContainer = this.containers[this.state];
// FIXME when video is being replaced with other content we need to hide
// companion icons/messages. It would be best if the container would
// be taking care of it by itself, but that is a bigger refactoring
if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
this.showLocalConnectionMessage(false);
this.showRemoteConnectionMessage(false);
}
oldContainer.hide();
this.state = type;
let container = this.getContainer(type);
return container.show().then(() => {
if (type === VIDEO_CONTAINER_TYPE) {
// FIXME when video appears on top of other content we need to
// show companion icons/messages. It would be best if
// the container would be taking care of it by itself, but that
// is a bigger refactoring
this.showWatermark(true);
// "avatar" and "video connection" can not be displayed both
// at the same time, but the latter is of higher priority and it
// will hide the avatar one if will be displayed.
this.showRemoteConnectionMessage(/* fet the current state */);
this.showLocalConnectionMessage(/* fetch the current state */);
}
});
}
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
}

View File

@ -11,7 +11,6 @@ function LocalVideo(VideoLayout, emitter) {
this.videoSpanId = "localVideoContainer"; this.videoSpanId = "localVideoContainer";
this.container = $("#localVideoContainer").get(0); this.container = $("#localVideoContainer").get(0);
this.localVideoId = null; this.localVideoId = null;
this.bindHoverHandler();
if(config.enableLocalVideoFlip) if(config.enableLocalVideoFlip)
this._buildContextMenu(); this._buildContextMenu();
this.isLocal = true; this.isLocal = true;
@ -29,6 +28,7 @@ function LocalVideo(VideoLayout, emitter) {
this.setDisplayName(); this.setDisplayName();
this.createConnectionIndicator(); this.createConnectionIndicator();
this.addAudioLevelIndicator();
} }
LocalVideo.prototype = Object.create(SmallVideo.prototype); LocalVideo.prototype = Object.create(SmallVideo.prototype);
@ -44,7 +44,7 @@ function createEditDisplayNameButton() {
editButton.className = 'displayname'; editButton.className = 'displayname';
UIUtil.setTooltip(editButton, UIUtil.setTooltip(editButton,
"videothumbnail.editnickname", "videothumbnail.editnickname",
"top"); "left");
editButton.innerHTML = '<i class="icon-edit"></i>'; editButton.innerHTML = '<i class="icon-edit"></i>';
return editButton; return editButton;
@ -61,7 +61,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
return; return;
} }
var nameSpan = $('#' + this.videoSpanId + '>span.displayname'); var nameSpan = $('#' + this.videoSpanId + ' .displayname');
var defaultLocalDisplayName = APP.translation.generateTranslationHTML( var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME); interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
@ -72,7 +72,10 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
if (displayName && displayName.length > 0) { if (displayName && displayName.length > 0) {
meHTML = APP.translation.generateTranslationHTML("me"); meHTML = APP.translation.generateTranslationHTML("me");
$('#localDisplayName').html( $('#localDisplayName').html(
UIUtil.escapeHtml(displayName) + ' (' + meHTML + ')' `${UIUtil.escapeHtml(displayName)} (${meHTML})`
);
$('#editDisplayName').val(
`${UIUtil.escapeHtml(displayName)}`
); );
} else { } else {
$('#localDisplayName').html(defaultLocalDisplayName); $('#localDisplayName').html(defaultLocalDisplayName);
@ -80,11 +83,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
} }
this.updateView(); this.updateView();
} else { } else {
var editButton = createEditDisplayNameButton();
nameSpan = document.createElement('span'); nameSpan = document.createElement('span');
nameSpan.className = 'displayname'; nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan); document.getElementById(this.videoSpanId)
.querySelector('.videocontainer__toolbar')
.appendChild(nameSpan);
if (displayName && displayName.length > 0) { if (displayName && displayName.length > 0) {
@ -97,12 +100,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
nameSpan.id = 'localDisplayName'; nameSpan.id = 'localDisplayName';
this.container.appendChild(editButton);
//translates popover of edit button //translates popover of edit button
APP.translation.translateElement($("a.displayname")); APP.translation.translateElement($("a.displayname"));
var editableText = document.createElement('input'); var editableText = document.createElement('input');
editableText.className = 'displayname'; editableText.className = 'editdisplayname';
editableText.type = 'text'; editableText.type = 'text';
editableText.id = 'editDisplayName'; editableText.id = 'editDisplayName';
@ -119,26 +121,30 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
JSON.stringify({name: "Jane Pink"})); JSON.stringify({name: "Jane Pink"}));
editableText.setAttribute("placeholder", defaultNickname); editableText.setAttribute("placeholder", defaultNickname);
this.container.appendChild(editableText); this.container
.querySelector('.videocontainer__toolbar')
.appendChild(editableText);
var self = this; var self = this;
$('#localVideoContainer .displayname') $('#localVideoContainer .displayname')
.bind("click", function (e) { .bind("click", function (e) {
let $editDisplayName = $('#editDisplayName');
let $localDisplayName = $('#localDisplayName');
var editDisplayName = $('#editDisplayName');
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$('#localDisplayName').hide(); $localDisplayName.hide();
editDisplayName.show(); $editDisplayName.show();
editDisplayName.focus(); $editDisplayName.focus();
editDisplayName.select(); $editDisplayName.select();
editDisplayName.one("focusout", function (e) { $editDisplayName.one("focusout", function (e) {
self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value); self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
$('#editDisplayName').hide(); $editDisplayName.hide();
$localDisplayName.show();
}); });
editDisplayName.on('keydown', function (e) { $editDisplayName.on('keydown', function (e) {
if (e.keyCode === 13) { if (e.keyCode === 13) {
e.preventDefault(); e.preventDefault();
$('#editDisplayName').hide(); $('#editDisplayName').hide();
@ -199,7 +205,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
localVideoContainer.removeChild(localVideo); localVideoContainer.removeChild(localVideo);
// when removing only the video element and we are on stage // when removing only the video element and we are on stage
// update the stage // update the stage
if(this.VideoLayout.isCurrentlyOnLarge(this.id)) if(this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id); this.VideoLayout.updateLargeVideo(this.id);
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler); stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
}; };

View File

@ -8,19 +8,45 @@ import UIUtils from "../util/UIUtil";
import UIEvents from '../../../service/UI/UIEvents'; import UIEvents from '../../../service/UI/UIEvents';
import JitsiPopover from "../util/JitsiPopover"; import JitsiPopover from "../util/JitsiPopover";
function RemoteVideo(id, VideoLayout, emitter) { /**
this.id = id; * Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @param {VideoLayout} VideoLayout the video layout instance.
* @param {EventEmitter} emitter the event emitter which will be used by
* the new instance to emit events.
* @constructor
*/
function RemoteVideo(user, VideoLayout, emitter) {
this.user = user;
this.id = user.getId();
this.emitter = emitter; this.emitter = emitter;
this.videoSpanId = `participant_${id}`; this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout); SmallVideo.call(this, VideoLayout);
this.hasRemoteVideoMenu = false; this.hasRemoteVideoMenu = false;
this.addRemoteVideoContainer(); this.addRemoteVideoContainer();
this.connectionIndicator = new ConnectionIndicator(this, id); this.connectionIndicator = new ConnectionIndicator(this, this.id);
this.setDisplayName(); this.setDisplayName();
this.bindHoverHandler();
this.flipX = false; this.flipX = false;
this.isLocal = false; this.isLocal = false;
this.isMuted = false; /**
* The flag is set to <tt>true</tt> after the 'onplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
* when the stream is removed. It is used to determine whether the video
* playback has ever started.
* @type {boolean}
*/
this.wasVideoPlayed = false;
/**
* The flag is set to <tt>true</tt> if remote participant's video gets muted
* during his media connection disruption. This is to prevent black video
* being render on the thumbnail, because even though once the video has
* been played the image usually remains on the video element it seems that
* after longer period of the video element being hidden this image can be
* lost.
* @type {boolean}
*/
this.mutedWhileDisconnected = false;
} }
RemoteVideo.prototype = Object.create(SmallVideo.prototype); RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@ -34,13 +60,15 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
if (APP.conference.isModerator) { if (APP.conference.isModerator) {
this.addRemoteVideoMenu(); this.addRemoteVideoMenu();
} }
let {thumbWidth, thumbHeight} = this.VideoLayout.resizeThumbnails();
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight); let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
let { thumbHeight, thumbWidth } = remoteVideo;
this.addAudioLevelIndicator();
return this.container; return this.container;
}; };
/** /**
* Initializes the remote participant popup menu, by specifying previously * Initializes the remote participant popup menu, by specifying previously
* constructed popupMenuElement, containing all the menu items. * constructed popupMenuElement, containing all the menu items.
@ -50,7 +78,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
*/ */
RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) { RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
this.popover = new JitsiPopover( this.popover = new JitsiPopover(
$("#" + this.videoSpanId + " > .remotevideomenu"), $("#" + this.videoSpanId + " .remotevideomenu"),
{ content: popupMenuElement.outerHTML, { content: popupMenuElement.outerHTML,
skin: "black"}); skin: "black"});
@ -60,7 +88,7 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
this.popover.show = function () { this.popover.show = function () {
// update content by forcing it, to finish even if popover // update content by forcing it, to finish even if popover
// is not visible // is not visible
this.updateRemoteVideoMenu(this.isMuted, true); this.updateRemoteVideoMenu(this.isAudioMuted, true);
// call the original show, passing its actual this // call the original show, passing its actual this
origShowFunc.call(this.popover); origShowFunc.call(this.popover);
}.bind(this); }.bind(this);
@ -96,7 +124,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
muteLinkItem.id = "mutelink_" + this.id; muteLinkItem.id = "mutelink_" + this.id;
if (this.isMuted) { if (this.isAudioMuted) {
muteLinkItem.innerHTML = mutedHTML; muteLinkItem.innerHTML = mutedHTML;
muteLinkItem.className = 'mutelink disabled'; muteLinkItem.className = 'mutelink disabled';
} }
@ -108,7 +136,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
// Delegate event to the document. // Delegate event to the document.
$(document).on("click", "#mutelink_" + this.id, function(){ $(document).on("click", "#mutelink_" + this.id, function(){
if (this.isMuted) if (this.isAudioMuted)
return; return;
this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id); this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
@ -152,7 +180,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
*/ */
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) { RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
this.isMuted = isMuted; this.isAudioMuted = isMuted;
// generate content, translate it and add it to document only if // generate content, translate it and add it to document only if
// popover is visible or we force to do so. // popover is visible or we force to do so.
@ -161,6 +189,33 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
} }
}; };
/**
* @inheritDoc
*/
RemoteVideo.prototype.setMutedView = function(isMuted) {
SmallVideo.prototype.setMutedView.call(this, isMuted);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
};
/**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status.
*
* @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
* currently having connectivity issues or <tt>false</tt> otherwise.
*
* @private
*/
RemoteVideo.prototype._figureOutMutedWhileDisconnected
= function(isDisconnected) {
if (isDisconnected && this.isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (!isDisconnected && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
};
/** /**
* Adds the remote video menu element for the given <tt>id</tt> in the * Adds the remote video menu element for the given <tt>id</tt> in the
* given <tt>parentElement</tt>. * given <tt>parentElement</tt>.
@ -170,12 +225,16 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
*/ */
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
RemoteVideo.prototype.addRemoteVideoMenu = function () { RemoteVideo.prototype.addRemoteVideoMenu = function () {
var spanElement = document.createElement('div');
spanElement.className = 'remotevideomenu'; var spanElement = document.createElement('span');
this.container.appendChild(spanElement); spanElement.className = 'remotevideomenu toolbar-icon right';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(spanElement);
var menuElement = document.createElement('i'); var menuElement = document.createElement('i');
menuElement.className = 'fa fa-angle-down'; menuElement.className = 'icon-menu-up';
menuElement.title = 'Remote user controls'; menuElement.title = 'Remote user controls';
spanElement.appendChild(menuElement); spanElement.appendChild(menuElement);
@ -204,13 +263,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
var select = $('#' + elementID); var select = $('#' + elementID);
select.remove(); select.remove();
if (isVideo) {
this.wasVideoPlayed = false;
}
console.info((isVideo ? "Video" : "Audio") + console.info((isVideo ? "Video" : "Audio") +
" removed " + this.id, select); " removed " + this.id, select);
// when removing only the video element and we are on stage // when removing only the video element and we are on stage
// update the stage // update the stage
if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id)) if (isVideo && this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id); this.VideoLayout.updateLargeVideo(this.id);
else
// Missing video stream will affect display mode
this.updateView();
};
/**
* Checks whether the remote user associated with this <tt>RemoteVideo</tt>
* has connectivity issues.
*
* @return {boolean} <tt>true</tt> if the user's connection is fine or
* <tt>false</tt> otherwise.
*/
RemoteVideo.prototype.isConnectionActive = function() {
return this.user.isConnectionActive();
};
/**
* The remote video is considered "playable" once the stream has started
* according to the {@link #hasVideoStarted} result.
*
* @inheritdoc
* @override
*/
RemoteVideo.prototype.isVideoPlayable = function () {
return SmallVideo.prototype.isVideoPlayable.call(this)
&& this.hasVideoStarted() && !this.mutedWhileDisconnected;
};
/**
* @inheritDoc
*/
RemoteVideo.prototype.updateView = function () {
this.updateConnectionStatusIndicator(
null /* will obtain the status from 'conference' */);
// This must be called after 'updateConnectionStatusIndicator' because it
// affects the display mode by modifying 'mutedWhileDisconnected' flag
SmallVideo.prototype.updateView.call(this);
};
/**
* Updates the UI to reflect user's connectivity status.
* @param isActive {boolean|null} 'true' if user's connection is active or
* 'false' when the use is having some connectivity issues and a warning
* should be displayed. When 'null' is passed then the current value will be
* obtained from the conference instance.
*/
RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
// Check for initial value if 'isActive' is not defined
if (typeof isActive !== "boolean") {
isActive = this.isConnectionActive();
if (isActive === null) {
// Cancel processing at this point - no update
return;
}
}
console.debug(this.id + " thumbnail is connection active ? " + isActive);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(!isActive);
if(this.connectionIndicator)
this.connectionIndicator.updateConnectionStatusIndicator(isActive);
// Toggle thumbnail video problem filter
this.selectVideoElement().toggleClass(
"videoThumbnailProblemFilter", !isActive);
this.$avatar().toggleClass(
"videoThumbnailProblemFilter", !isActive);
}; };
/** /**
@ -241,22 +375,23 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
// when video playback starts // when video playback starts
var onPlayingHandler = function () { var onPlayingHandler = function () {
self.wasVideoPlayed = true;
self.VideoLayout.videoactive(streamElement, self.id); self.VideoLayout.videoactive(streamElement, self.id);
streamElement.onplaying = null; streamElement.onplaying = null;
// Refresh to show the video
self.updateView();
}; };
streamElement.onplaying = onPlayingHandler; streamElement.onplaying = onPlayingHandler;
}; };
/** /**
* Checks whether or not video stream exists and has started for this * Checks whether the video stream has started for this RemoteVideo instance.
* RemoteVideo instance. This is checked by trying to select video element in
* this container and checking if 'currentTime' field's value is greater than 0.
* *
* @returns {*|boolean} true if this RemoteVideo has active video stream running * @returns {boolean} true if this RemoteVideo has a video stream for which
* the playback has been started.
*/ */
RemoteVideo.prototype.hasVideoStarted = function () { RemoteVideo.prototype.hasVideoStarted = function () {
var videoSelector = this.selectVideoElement(); return this.wasVideoPlayed;
return videoSelector.length && videoSelector[0].currentTime > 0;
}; };
RemoteVideo.prototype.addRemoteStreamElement = function (stream) { RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
@ -381,7 +516,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
return; return;
} }
var nameSpan = $('#' + this.videoSpanId + '>span.displayname'); var nameSpan = $('#' + this.videoSpanId + ' .displayname');
// If we already have a display name for this video. // If we already have a display name for this video.
if (nameSpan.length > 0) { if (nameSpan.length > 0) {
@ -400,7 +535,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
} else { } else {
nameSpan = document.createElement('span'); nameSpan = document.createElement('span');
nameSpan.className = 'displayname'; nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan); $('#' + this.videoSpanId)[0]
.querySelector('.videocontainer__toolbar')
.appendChild(nameSpan);
if (displayName && displayName.length > 0) { if (displayName && displayName.length > 0) {
$(nameSpan).text(displayName); $(nameSpan).text(displayName);
@ -418,7 +555,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
* @param videoElementId the id of local or remote video element. * @param videoElementId the id of local or remote video element.
*/ */
RemoteVideo.prototype.removeRemoteVideoMenu = function() { RemoteVideo.prototype.removeRemoteVideoMenu = function() {
var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu'); var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
if (menuSpan.length) { if (menuSpan.length) {
this.popover.forceHide(); this.popover.forceHide();
menuSpan.remove(); menuSpan.remove();
@ -427,12 +564,16 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
}; };
RemoteVideo.createContainer = function (spanId) { RemoteVideo.createContainer = function (spanId) {
var container = document.createElement('span'); let container = document.createElement('span');
container.id = spanId; container.id = spanId;
container.className = 'videocontainer'; container.className = 'videocontainer';
let toolbar = document.createElement('div');
toolbar.className = "videocontainer__toolbar";
container.appendChild(toolbar);
var remotes = document.getElementById('remoteVideos'); var remotes = document.getElementById('remoteVideos');
return remotes.appendChild(container); return remotes.appendChild(container);
}; };
export default RemoteVideo; export default RemoteVideo;

View File

@ -1,13 +1,34 @@
/* global $, APP, JitsiMeetJS */ /* global $, APP, JitsiMeetJS, interfaceConfig */
/* jshint -W101 */
import Avatar from "../avatar/Avatar"; import Avatar from "../avatar/Avatar";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import AudioLevels from "../audio_levels/AudioLevels";
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
* @constant
*/
const DISPLAY_VIDEO = 0;
/**
* Display mode constant used when the user's avatar is being displayed on
* the small video.
* @type {number}
* @constant
*/
const DISPLAY_AVATAR = 1;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video.
* @type {number}
* @constant
*/
const DISPLAY_BLACKNESS = 2;
function SmallVideo(VideoLayout) { function SmallVideo(VideoLayout) {
this.isMuted = false; this.isAudioMuted = false;
this.hasAvatar = false; this.hasAvatar = false;
this.isVideoMuted = false; this.isVideoMuted = false;
this.videoStream = null; this.videoStream = null;
@ -40,7 +61,7 @@ SmallVideo.prototype.isVisible = function () {
}; };
SmallVideo.prototype.showDisplayName = function(isShow) { SmallVideo.prototype.showDisplayName = function(isShow) {
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0); var nameSpan = $('#' + this.videoSpanId + ' .displayname').get(0);
if (isShow) { if (isShow) {
if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
nameSpan.setAttribute("style", "display:inline-block;"); nameSpan.setAttribute("style", "display:inline-block;");
@ -171,26 +192,6 @@ SmallVideo.getStreamElementID = function (stream) {
return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId(); return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
}; };
/**
* Configures hoverIn/hoverOut handlers.
*/
SmallVideo.prototype.bindHoverHandler = function () {
// Add hover handler
var self = this;
$(this.container).hover(
function () {
self.showDisplayName(true);
},
function () {
// If the video has been "pinned" by the user we want to
// keep the display name on place.
if (!self.VideoLayout.isLargeVideoVisible() ||
!self.VideoLayout.isCurrentlyOnLarge(self.id))
self.showDisplayName(false);
}
);
};
/** /**
* Updates the data for the indicator * Updates the data for the indicator
* @param id the id of the indicator * @param id the id of the indicator
@ -209,121 +210,164 @@ SmallVideo.prototype.hideIndicator = function () {
/** /**
* Shows audio muted indicator over small videos. * Shows / hides the audio muted indicator over small videos.
* @param {string} isMuted *
* @param {boolean} isMuted indicates if the muted element should be shown
* or hidden
*/ */
SmallVideo.prototype.showAudioIndicator = function(isMuted) { SmallVideo.prototype.showAudioIndicator = function(isMuted) {
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
var audioMutedIndicator = this.getAudioMutedIndicator();
if (!isMuted) { if (!isMuted) {
if (audioMutedSpan.length > 0) { audioMutedIndicator.hide();
audioMutedSpan.popover('hide');
audioMutedSpan.remove();
}
} }
else { else {
if (!audioMutedSpan.length) { audioMutedIndicator.show();
audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted';
UIUtil.setTooltip(audioMutedSpan,
"videothumbnail.mute",
"top");
this.container.appendChild(audioMutedSpan);
APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
audioMutedSpan.appendChild(mutedIndicator);
}
this.updateIconPositions();
} }
this.isMuted = isMuted; this.isAudioMuted = isMuted;
};
/**
* Returns the audio muted indicator jquery object. If it doesn't exists -
* creates it.
*
* @returns {jQuery|HTMLElement} the audio muted indicator
*/
SmallVideo.prototype.getAudioMutedIndicator = function () {
var audioMutedSpan = $('#' + this.videoSpanId + ' .audioMuted');
if (audioMutedSpan.length) {
return audioMutedSpan;
}
audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted toolbar-icon';
UIUtil.setTooltip(audioMutedSpan,
"videothumbnail.mute",
"top");
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(audioMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
audioMutedSpan.appendChild(mutedIndicator);
return $('#' + this.videoSpanId + ' .audioMuted');
}; };
/** /**
* Shows video muted indicator over small videos and disables/enables avatar * Shows video muted indicator over small videos and disables/enables avatar
* if video muted. * if video muted.
*
* @param {boolean} isMuted indicates if we should set the view to muted view
* or not
*/ */
SmallVideo.prototype.setMutedView = function(isMuted) { SmallVideo.prototype.setVideoMutedView = function(isMuted) {
this.isVideoMuted = isMuted; this.isVideoMuted = isMuted;
this.updateView(); this.updateView();
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted'); var videoMutedSpan = this.getVideoMutedIndicator();
if (isMuted === false) { videoMutedSpan[isMuted ? 'show' : 'hide']();
if (videoMutedSpan.length > 0) {
videoMutedSpan.remove();
}
}
else {
if (!videoMutedSpan.length) {
videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted';
this.container.appendChild(videoMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
UIUtil.setTooltip(mutedIndicator,
"videothumbnail.videomute",
"top");
videoMutedSpan.appendChild(mutedIndicator);
//translate texts for muted indicator
APP.translation.translateElement($('#' + this.videoSpanId + " > span > i"));
}
this.updateIconPositions();
}
};
SmallVideo.prototype.updateIconPositions = function () {
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
if(connectionIndicator.length > 0 &&
connectionIndicator[0].style.display != "none") {
audioMutedSpan.css({right: "23px"});
videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
} else {
audioMutedSpan.css({right: "0px"});
videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
}
}; };
/** /**
* Creates the element indicating the moderator(owner) of the conference. * Returns the video muted indicator jquery object. If it doesn't exists -
* creates it.
*
* @returns {jQuery|HTMLElement} the video muted indicator
*/ */
SmallVideo.prototype.createModeratorIndicatorElement = function () { SmallVideo.prototype.getVideoMutedIndicator = function () {
var videoMutedSpan = $('#' + this.videoSpanId + ' .videoMuted');
if (videoMutedSpan.length) {
return videoMutedSpan;
}
videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted toolbar-icon';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(videoMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
UIUtil.setTooltip(mutedIndicator,
"videothumbnail.videomute",
"top");
videoMutedSpan.appendChild(mutedIndicator);
return $('#' + this.videoSpanId + ' .videoMuted');
};
/**
* Adds the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.addModeratorIndicator = function () {
// Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
return false;
// Show moderator indicator // Show moderator indicator
var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator'); var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
if (!indicatorSpan || indicatorSpan.length === 0) { if (indicatorSpan.length) {
indicatorSpan = document.createElement('span'); return;
indicatorSpan.className = 'focusindicator';
this.container.appendChild(indicatorSpan);
indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
} }
if (indicatorSpan.children().length !== 0) indicatorSpan = document.createElement('span');
return; indicatorSpan.className = 'focusindicator toolbar-icon right';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(indicatorSpan);
var moderatorIndicator = document.createElement('i'); var moderatorIndicator = document.createElement('i');
moderatorIndicator.className = 'icon-star'; moderatorIndicator.className = 'icon-star';
indicatorSpan[0].appendChild(moderatorIndicator);
UIUtil.setTooltip(indicatorSpan[0], UIUtil.setTooltip(moderatorIndicator,
"videothumbnail.moderator", "videothumbnail.moderator",
"top"); "top-left");
//translates text in focus indicators indicatorSpan.appendChild(moderatorIndicator);
APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator')); };
/**
* Adds the element indicating the audio level of the participant.
*/
SmallVideo.prototype.addAudioLevelIndicator = function () {
var audioSpan = $('#' + this.videoSpanId + ' .audioindicator');
if (audioSpan.length) {
return;
}
this.container.appendChild(
AudioLevels.createThumbnailAudioLevelIndicator());
};
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
*/
SmallVideo.prototype.updateAudioLevelIndicator = function (lvl) {
AudioLevels.updateThumbnailAudioLevel(this.videoSpanId, lvl);
}; };
/** /**
* Removes the element indicating the moderator(owner) of the conference. * Removes the element indicating the moderator(owner) of the conference.
*/ */
SmallVideo.prototype.removeModeratorIndicatorElement = function () { SmallVideo.prototype.removeModeratorIndicator = function () {
$('#' + this.videoSpanId + ' .focusindicator').remove(); $('#' + this.videoSpanId + ' .focusindicator').remove();
}; };
@ -341,6 +385,16 @@ SmallVideo.prototype.selectVideoElement = function () {
return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0])); return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
}; };
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
SmallVideo.prototype.$avatar = function () {
return $('#' + this.videoSpanId + ' .userAvatar');
};
/** /**
* Enables / disables the css responsible for focusing/pinning a video * Enables / disables the css responsible for focusing/pinning a video
* thumbnail. * thumbnail.
@ -363,6 +417,47 @@ SmallVideo.prototype.hasVideo = function () {
return this.selectVideoElement().length !== 0; return this.selectVideoElement().length !== 0;
}; };
/**
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
* being displayed on the "large video".
*
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
return this.VideoLayout.isCurrentlyOnLarge(this.id);
};
/**
* Checks whether there is a playable video stream available for the user
* associated with this <tt>SmallVideo</tt>.
*
* @return {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isVideoPlayable = function() {
return this.videoStream // Is there anything to display ?
&& !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
&& (this.isLocal || this.VideoLayout.isInLastN(this.id));
};
/**
* Determines what should be display on the thumbnail.
*
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
* or <tt>DISPLAY_BLACKNESS</tt>.
*/
SmallVideo.prototype.selectDisplayMode = function() {
// Display name is always and only displayed when user is on the stage
if (this.isCurrentlyOnLargeVideo()) {
return DISPLAY_BLACKNESS;
} else if (this.isVideoPlayable() && this.selectVideoElement().length) {
return DISPLAY_VIDEO;
} else {
return DISPLAY_AVATAR;
}
};
/** /**
* Hides or shows the user's avatar. * Hides or shows the user's avatar.
* This update assumes that large video had been updated and we will * This update assumes that large video had been updated and we will
@ -382,48 +477,28 @@ SmallVideo.prototype.updateView = function () {
} }
} }
let video = this.selectVideoElement(); // Determine whether video, avatar or blackness should be displayed
let displayMode = this.selectDisplayMode();
let avatar = $('#' + this.videoSpanId + ' .userAvatar'); // Show/hide video
setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id); // Show/hide the avatar
setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
var showAvatar;
if ((!this.isLocal
&& !this.VideoLayout.isInLastN(this.id))
|| this.isVideoMuted) {
showAvatar = true;
} else {
// We want to show the avatar when the video is muted or not exists
// that is when 'true' or 'null' is returned
showAvatar = !this.videoStream || this.videoStream.isMuted();
}
showAvatar = showAvatar && !isCurrentlyOnLarge;
if (video && video.length > 0) {
setVisibility(video, showVideo);
}
setVisibility(avatar, showAvatar);
this.showDisplayName(!showVideo && !showAvatar);
}; };
SmallVideo.prototype.avatarChanged = function (avatarUrl) { SmallVideo.prototype.avatarChanged = function (avatarUrl) {
var thumbnail = $('#' + this.videoSpanId); var thumbnail = $('#' + this.videoSpanId);
var avatar = $('#' + this.videoSpanId + ' .userAvatar'); var avatarSel = this.$avatar();
this.hasAvatar = true; this.hasAvatar = true;
// set the avatar in the thumbnail // set the avatar in the thumbnail
if (avatar && avatar.length > 0) { if (avatarSel && avatarSel.length > 0) {
avatar[0].src = avatarUrl; avatarSel[0].src = avatarUrl;
} else { } else {
if (thumbnail && thumbnail.length > 0) { if (thumbnail && thumbnail.length > 0) {
avatar = document.createElement('img'); var avatarElement = document.createElement('img');
avatar.className = 'userAvatar'; avatarElement.className = 'userAvatar';
avatar.src = avatarUrl; avatarElement.src = avatarUrl;
thumbnail.append(avatar); thumbnail.append(avatarElement);
} }
} }
}; };
@ -445,7 +520,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function (show) {
indicatorSpan.innerHTML indicatorSpan.innerHTML
= "<i id='indicatoricon' class='fa fa-bullhorn'></i>"; = "<i id='indicatoricon' class='fa fa-bullhorn'></i>";
// adds a tooltip // adds a tooltip
UIUtil.setTooltip(indicatorSpan, "speaker", "left"); UIUtil.setTooltip(indicatorSpan, "speaker", "top");
APP.translation.translateElement($(indicatorSpan)); APP.translation.translateElement($(indicatorSpan));
$(indicatorSpan).css("visibility", show ? "visible" : "hidden"); $(indicatorSpan).css("visibility", show ? "visible" : "hidden");
@ -465,12 +540,11 @@ SmallVideo.prototype.showRaisedHandIndicator = function (show) {
var indicatorSpanId = "raisehandindicator"; var indicatorSpanId = "raisehandindicator";
var indicatorSpan = this.getIndicatorSpan(indicatorSpanId); var indicatorSpan = this.getIndicatorSpan(indicatorSpanId);
indicatorSpan.style.background = "#D6D61E";
indicatorSpan.innerHTML indicatorSpan.innerHTML
= "<i id='indicatoricon' class='fa fa-hand-paper-o'></i>"; = "<i id='indicatoricon' class='icon-raised-hand'></i>";
// adds a tooltip // adds a tooltip
UIUtil.setTooltip(indicatorSpan, "raisedHand", "left"); UIUtil.setTooltip(indicatorSpan, "raisedHand", "top");
APP.translation.translateElement($(indicatorSpan)); APP.translation.translateElement($(indicatorSpan));
$(indicatorSpan).css("visibility", show ? "visible" : "hidden"); $(indicatorSpan).css("visibility", show ? "visible" : "hidden");

View File

@ -1,17 +1,16 @@
/* global $, APP, interfaceConfig */ /* global $, APP, interfaceConfig */
/* jshint -W101 */ /* jshint -W101 */
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import LargeContainer from './LargeContainer';
import FilmStrip from './FilmStrip'; import FilmStrip from './FilmStrip';
import Avatar from "../avatar/Avatar"; import LargeContainer from './LargeContainer';
import {createDeferred} from '../../util/helpers'; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
// FIXME should be 'video'
export const VIDEO_CONTAINER_TYPE = "camera";
const FADE_DURATION_MS = 300; const FADE_DURATION_MS = 300;
export const VIDEO_CONTAINER_TYPE = "camera";
/** /**
* Get stream id. * Get stream id.
* @param {JitsiTrack?} stream * @param {JitsiTrack?} stream
@ -20,7 +19,8 @@ function getStreamOwnerId(stream) {
if (!stream) { if (!stream) {
return; return;
} }
if (stream.isLocal()) { // local stream doesn't have method "getParticipantId" // local stream doesn't have method "getParticipantId"
if (stream.isLocal()) {
return APP.conference.getMyUserId(); return APP.conference.getMyUserId();
} else { } else {
return stream.getParticipantId(); return stream.getParticipantId();
@ -154,7 +154,7 @@ function getDesktopVideoPosition(videoWidth,
/** /**
* Container for user video. * Container for user video.
*/ */
class VideoContainer extends LargeContainer { export class VideoContainer extends LargeContainer {
// FIXME: With Temasys we have to re-select everytime // FIXME: With Temasys we have to re-select everytime
get $video () { get $video () {
return $('#largeVideo'); return $('#largeVideo');
@ -164,23 +164,61 @@ class VideoContainer extends LargeContainer {
return getStreamOwnerId(this.stream); return getStreamOwnerId(this.stream);
} }
constructor (onPlay) { constructor (onPlay, emitter) {
super(); super();
this.stream = null; this.stream = null;
this.videoType = null; this.videoType = null;
this.localFlipX = true; this.localFlipX = true;
this.emitter = emitter;
this.isVisible = false; this.isVisible = false;
/**
* Flag indicates whether or not the avatar is currently displayed.
* @type {boolean}
*/
this.avatarDisplayed = false;
this.$avatar = $('#dominantSpeaker'); this.$avatar = $('#dominantSpeaker');
/**
* A jQuery selector of the remote connection message.
* @type {jQuery|HTMLElement}
*/
this.$remoteConnectionMessage = $('#remoteConnectionMessage');
/**
* Indicates whether or not the video stream attached to the video
* element has started(which means that there is any image rendered
* even if the video is stalled).
* @type {boolean}
*/
this.wasVideoRendered = false;
this.$wrapper = $('#largeVideoWrapper'); this.$wrapper = $('#largeVideoWrapper');
this.avatarHeight = $("#dominantSpeakerAvatar").height(); this.avatarHeight = $("#dominantSpeakerAvatar").height();
var onPlayCallback = function (event) {
if (typeof onPlay === 'function') {
onPlay(event);
}
this.wasVideoRendered = true;
}.bind(this);
// This does not work with Temasys plugin - has to be a property to be // This does not work with Temasys plugin - has to be a property to be
// copied between new <object> elements // copied between new <object> elements
//this.$video.on('play', onPlay); //this.$video.on('play', onPlay);
this.$video[0].onplay = onPlay; this.$video[0].onplay = onPlayCallback;
}
/**
* Enables a filter on the video which indicates that there are some
* problems with the local media connection.
*
* @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
* <tt>false</tt> otherwise.
*/
enableLocalConnectionProblemFilter (enable) {
this.$video.toggleClass("videoProblemFilter", enable);
} }
/** /**
@ -205,14 +243,14 @@ class VideoContainer extends LargeContainer {
let { width, height } = this.getStreamSize(); let { width, height } = this.getStreamSize();
if (this.stream && this.isScreenSharing()) { if (this.stream && this.isScreenSharing()) {
return getDesktopVideoSize( width, return getDesktopVideoSize( width,
height, height,
containerWidth, containerWidth,
containerHeight); containerHeight);
} else { } else {
return getCameraVideoSize( width, return getCameraVideoSize( width,
height, height,
containerWidth, containerWidth,
containerHeight); containerHeight);
} }
} }
@ -228,29 +266,55 @@ class VideoContainer extends LargeContainer {
getVideoPosition (width, height, containerWidth, containerHeight) { getVideoPosition (width, height, containerWidth, containerHeight) {
if (this.stream && this.isScreenSharing()) { if (this.stream && this.isScreenSharing()) {
return getDesktopVideoPosition( width, return getDesktopVideoPosition( width,
height, height,
containerWidth, containerWidth,
containerHeight); containerHeight);
} else { } else {
return getCameraVideoPosition( width, return getCameraVideoPosition( width,
height, height,
containerWidth, containerWidth,
containerHeight); containerHeight);
} }
} }
/**
* Update position of the remote connection message which describes that
* the remote user is having connectivity issues.
*/
positionRemoteConnectionMessage () {
if (this.avatarDisplayed) {
let $avatarImage = $("#dominantSpeakerAvatar");
this.$remoteConnectionMessage.css(
'top',
$avatarImage.offset().top + $avatarImage.height() + 10);
} else {
let height = this.$remoteConnectionMessage.height();
let parentHeight = this.$remoteConnectionMessage.parent().height();
this.$remoteConnectionMessage.css(
'top', (parentHeight/2) - (height/2));
}
let width = this.$remoteConnectionMessage.width();
let parentWidth = this.$remoteConnectionMessage.parent().width();
this.$remoteConnectionMessage.css(
'left', ((parentWidth/2) - (width/2)));
}
resize (containerWidth, containerHeight, animate = false) { resize (containerWidth, containerHeight, animate = false) {
let [width, height] let [width, height]
= this.getVideoSize(containerWidth, containerHeight); = this.getVideoSize(containerWidth, containerHeight);
let { horizontalIndent, verticalIndent } let { horizontalIndent, verticalIndent }
= this.getVideoPosition(width, height, = this.getVideoPosition(width, height,
containerWidth, containerHeight); containerWidth, containerHeight);
// update avatar position // update avatar position
let top = containerHeight / 2 - this.avatarHeight / 4 * 3; let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
this.$avatar.css('top', top); this.$avatar.css('top', top);
this.positionRemoteConnectionMessage();
this.$wrapper.animate({ this.$wrapper.animate({
width: width, width: width,
height: height, height: height,
@ -272,6 +336,14 @@ class VideoContainer extends LargeContainer {
* @param {string} videoType video type * @param {string} videoType video type
*/ */
setStream (stream, videoType) { setStream (stream, videoType) {
if (this.stream === stream) {
return;
} else {
// The stream has changed, so the image will be lost on detach
this.wasVideoRendered = false;
}
// detach old stream // detach old stream
if (this.stream) { if (this.stream) {
this.stream.detach(this.$video[0]); this.stream.detach(this.$video[0]);
@ -327,6 +399,21 @@ class VideoContainer extends LargeContainer {
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000"); (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
this.$avatar.css("visibility", show ? "visible" : "hidden"); this.$avatar.css("visibility", show ? "visible" : "hidden");
this.avatarDisplayed = show;
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
}
/**
* Indicates that the remote user who is currently displayed by this video
* container is having connectivity issues.
*
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
* the indication.
*/
showRemoteConnectionProblemIndicator (show) {
this.$video.toggleClass("remoteVideoProblemFilter", show);
this.$avatar.toggleClass("remoteVideoProblemFilter", show);
} }
// We are doing fadeOut/fadeIn animations on parent div which wraps // We are doing fadeOut/fadeIn animations on parent div which wraps
@ -380,304 +467,3 @@ class VideoContainer extends LargeContainer {
return false; return false;
} }
} }
/**
* Manager for all Large containers.
*/
export default class LargeVideoManager {
constructor () {
this.containers = {};
this.state = VIDEO_CONTAINER_TYPE;
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle and desktop tracks
this.addContainer("desktop", this.videoContainer);
this.width = 0;
this.height = 0;
this.$container = $('#largeVideoContainer');
this.$container.css({
display: 'inline-block'
});
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
let leftWatermarkDiv
= this.$container.find("div.watermark.leftwatermark");
leftWatermarkDiv.css({display: 'block'});
leftWatermarkDiv.parent().attr(
'href', interfaceConfig.JITSI_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
let rightWatermarkDiv
= this.$container.find("div.watermark.rightwatermark");
rightWatermarkDiv.css({
display: 'block',
backgroundImage: 'url(images/rightwatermark.png)'
});
rightWatermarkDiv.parent().attr(
'href', interfaceConfig.BRAND_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_POWERED_BY) {
this.$container.children("a.poweredby").css({display: 'block'});
}
this.$container.hover(
e => this.onHoverIn(e),
e => this.onHoverOut(e)
);
}
onHoverIn (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverIn(e);
}
onHoverOut (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverOut(e);
}
get id () {
let container = this.getContainer(this.state);
return container.id;
}
scheduleLargeVideoUpdate () {
if (this.updateInProcess || !this.newStreamData) {
return;
}
this.updateInProcess = true;
let container = this.getContainer(this.state);
// Include hide()/fadeOut only if we're switching between users
let preUpdate;
if (this.newStreamData.id != this.id) {
preUpdate = container.hide();
} else {
preUpdate = Promise.resolve();
}
preUpdate.then(() => {
let {id, stream, videoType, resolve} = this.newStreamData;
this.newStreamData = null;
console.info("hover in %s", id);
this.state = videoType;
let container = this.getContainer(this.state);
container.setStream(stream, videoType);
// change the avatar url on large
this.updateAvatar(Avatar.getAvatarUrl(id));
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false
let isVideoMuted = false;
if (videoType == VIDEO_CONTAINER_TYPE)
isVideoMuted = stream ? stream.isMuted() : true;
// show the avatar on large if needed
container.showAvatar(isVideoMuted);
let promise;
// do not show stream if video is muted
// but we still should show watermark
if (isVideoMuted) {
this.showWatermark(true);
promise = Promise.resolve();
} else {
promise = container.show();
}
// resolve updateLargeVideo promise after everything is done
promise.then(resolve);
return promise;
}).then(() => {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.scheduleLargeVideoUpdate();
});
}
/**
* Update large video.
* Switches to large video even if previously other container was visible.
* @param userID the userID of the participant associated with the stream
* @param {JitsiTrack?} stream new stream
* @param {string?} videoType new video type
* @returns {Promise}
*/
updateLargeVideo (userID, stream, videoType) {
if (this.newStreamData) {
this.newStreamData.reject();
}
this.newStreamData = createDeferred();
this.newStreamData.id = userID;
this.newStreamData.stream = stream;
this.newStreamData.videoType = videoType;
this.scheduleLargeVideoUpdate();
return this.newStreamData.promise;
}
/**
* Update container size.
*/
updateContainerSize () {
this.width = UIUtil.getAvailableVideoWidth();
this.height = window.innerHeight;
}
/**
* Resize Large container of specified type.
* @param {string} type type of container which should be resized.
* @param {boolean} [animate=false] if resize process should be animated.
*/
resizeContainer (type, animate = false) {
let container = this.getContainer(type);
container.resize(this.width, this.height, animate);
}
/**
* Resize all Large containers.
* @param {boolean} animate if resize process should be animated.
*/
resize (animate) {
// resize all containers
Object.keys(this.containers)
.forEach(type => this.resizeContainer(type, animate));
this.$container.animate({
width: this.width,
height: this.height
}, {
queue: false,
duration: animate ? 500 : 0
});
}
/**
* Enables/disables the filter indicating a video problem to the user.
*
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
*/
enableVideoProblemFilter (enable) {
let container = this.getContainer(this.state);
container.$video.toggleClass("videoProblemFilter", enable);
}
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar (avatarUrl) {
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
}
/**
* Show or hide watermark.
* @param {boolean} show
*/
showWatermark (show) {
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
}
/**
* Add container of specified type.
* @param {string} type container type
* @param {LargeContainer} container container to add.
*/
addContainer (type, container) {
if (this.containers[type]) {
throw new Error(`container of type ${type} already exist`);
}
this.containers[type] = container;
this.resizeContainer(type);
}
/**
* Get Large container of specified type.
* @param {string} type container type.
* @returns {LargeContainer}
*/
getContainer (type) {
let container = this.containers[type];
if (!container) {
throw new Error(`container of type ${type} doesn't exist`);
}
return container;
}
/**
* Remove Large container of specified type.
* @param {string} type container type.
*/
removeContainer (type) {
if (!this.containers[type]) {
throw new Error(`container of type ${type} doesn't exist`);
}
delete this.containers[type];
}
/**
* Show Large container of specified type.
* Does nothing if such container is already visible.
* @param {string} type container type.
* @returns {Promise}
*/
showContainer (type) {
if (this.state === type) {
return Promise.resolve();
}
let oldContainer = this.containers[this.state];
if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
}
oldContainer.hide();
this.state = type;
let container = this.getContainer(type);
return container.show().then(() => {
if (type === VIDEO_CONTAINER_TYPE) {
this.showWatermark(true);
}
});
}
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
}

View File

@ -1,14 +1,14 @@
/* global config, APP, $, interfaceConfig, JitsiMeetJS */ /* global config, APP, $, interfaceConfig, JitsiMeetJS */
/* jshint -W101 */ /* jshint -W101 */
import AudioLevels from "../audio_levels/AudioLevels";
import Avatar from "../avatar/Avatar"; import Avatar from "../avatar/Avatar";
import FilmStrip from "./FilmStrip"; import FilmStrip from "./FilmStrip";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
import RemoteVideo from "./RemoteVideo"; import RemoteVideo from "./RemoteVideo";
import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo"; import LargeVideoManager from "./LargeVideoManager";
import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo'; import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
import LocalVideo from "./LocalVideo"; import LocalVideo from "./LocalVideo";
@ -102,32 +102,37 @@ var VideoLayout = {
}); });
localVideoThumbnail = new LocalVideo(VideoLayout, emitter); localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video // sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
// if we do not resize the thumbs here, if there is no video device // if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel // the local video thumb maybe one pixel
let {thumbWidth, thumbHeight} = this.resizeThumbnails(false, true); let { localVideo } = this.resizeThumbnails(false, true);
AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked); emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
this.lastNCount = config.channelLastN; this.lastNCount = config.channelLastN;
}, },
initLargeVideo () { initLargeVideo () {
largeVideo = new LargeVideoManager(); largeVideo = new LargeVideoManager(eventEmitter);
if(localFlipX) { if(localFlipX) {
largeVideo.onLocalFlipXChange(localFlipX); largeVideo.onLocalFlipXChange(localFlipX);
} }
largeVideo.updateContainerSize(); largeVideo.updateContainerSize();
AudioLevels.init();
}, },
/**
* Sets the audio level of the video elements associated to the given id.
*
* @param id the video identifier in the form it comes from the library
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) { setAudioLevel(id, lvl) {
if (!largeVideo) { let smallVideo = this.getSmallVideo(id);
return; if (smallVideo)
} smallVideo.updateAudioLevelIndicator(lvl);
AudioLevels.updateAudioLevel(
id, lvl, largeVideo.id if (largeVideo && id === largeVideo.id)
); largeVideo.updateLargeVideoAudioLevel(lvl);
}, },
isInLastN (resource) { isInLastN (resource) {
@ -254,7 +259,8 @@ var VideoLayout = {
electLastVisibleVideo () { electLastVisibleVideo () {
// pick the last visible video in the row // pick the last visible video in the row
// if nobody else is left, this picks the local video // if nobody else is left, this picks the local video
let thumbs = FilmStrip.getThumbs(true).filter('[id!="mixedstream"]'); let remoteThumbs = FilmStrip.getThumbs(true).remoteThumbs;
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
let lastVisible = thumbs.filter(':visible:last'); let lastVisible = thumbs.filter(':visible:last');
if (lastVisible.length) { if (lastVisible.length) {
@ -268,7 +274,7 @@ var VideoLayout = {
} }
console.info("Last visible video no longer exists"); console.info("Last visible video no longer exists");
thumbs = FilmStrip.getThumbs(); thumbs = FilmStrip.getThumbs().remoteThumbs;
if (thumbs.length) { if (thumbs.length) {
let id = getPeerContainerResourceId(thumbs[0]); let id = getPeerContainerResourceId(thumbs[0]);
if (remoteVideos[id]) { if (remoteVideos[id]) {
@ -378,34 +384,49 @@ var VideoLayout = {
}, },
/** /**
* Creates a participant container for the given id and smallVideo. * Creates or adds a participant container for the given id and smallVideo.
* *
* @param id the id of the participant to add * @param {JitsiParticipant} user the participant to add
* @param {SmallVideo} smallVideo optional small video instance to add as a * @param {SmallVideo} smallVideo optional small video instance to add as a
* remote video, if undefined RemoteVideo will be created * remote video, if undefined <tt>RemoteVideo</tt> will be created
*/ */
addParticipantContainer (id, smallVideo) { addParticipantContainer (user, smallVideo) {
let id = user.getId();
let remoteVideo; let remoteVideo;
if(smallVideo) if(smallVideo)
remoteVideo = smallVideo; remoteVideo = smallVideo;
else else
remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter); remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
this.addRemoteVideoContainer(id, remoteVideo);
},
/**
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
*
* @param {string} the id of the video to add
* @param {SmallVideo} smallVideo the small video instance to add as a
* remote video
*/
addRemoteVideoContainer (id, remoteVideo) {
remoteVideos[id] = remoteVideo; remoteVideos[id] = remoteVideo;
let videoType = VideoLayout.getRemoteVideoType(id); let videoType = VideoLayout.getRemoteVideoType(id);
if (!videoType) { if (!videoType) {
// make video type the default one (camera) // make video type the default one (camera)
// FIXME container type is not a video type
videoType = VIDEO_CONTAINER_TYPE; videoType = VIDEO_CONTAINER_TYPE;
} }
remoteVideo.setVideoType(videoType); remoteVideo.setVideoType(videoType);
// In case this is not currently in the last n we don't show it. // In case this is not currently in the last n we don't show it.
if (localLastNCount && localLastNCount > 0 && if (localLastNCount && localLastNCount > 0 &&
FilmStrip.getThumbs().length >= localLastNCount + 2) { FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
remoteVideo.showPeerContainer('hide'); remoteVideo.showPeerContainer('hide');
} else { } else {
VideoLayout.resizeThumbnails(false, true); VideoLayout.resizeThumbnails(false, true);
} }
// Initialize the view
remoteVideo.updateView();
}, },
videoactive (videoelem, resourceJid) { videoactive (videoelem, resourceJid) {
@ -448,9 +469,9 @@ var VideoLayout = {
showModeratorIndicator () { showModeratorIndicator () {
let isModerator = APP.conference.isModerator; let isModerator = APP.conference.isModerator;
if (isModerator) { if (isModerator) {
localVideoThumbnail.createModeratorIndicatorElement(); localVideoThumbnail.addModeratorIndicator();
} else { } else {
localVideoThumbnail.removeModeratorIndicatorElement(); localVideoThumbnail.removeModeratorIndicator();
} }
APP.conference.listMembers().forEach(function (member) { APP.conference.listMembers().forEach(function (member) {
@ -460,9 +481,10 @@ var VideoLayout = {
return; return;
if (member.isModerator()) { if (member.isModerator()) {
remoteVideo.removeRemoteVideoMenu(); remoteVideo.addModeratorIndicator();
remoteVideo.createModeratorIndicatorElement(); }
} else if (isModerator) {
if (isModerator) {
// We are moderator, but user is not - add menu // We are moderator, but user is not - add menu
if(!remoteVideo.hasRemoteVideoMenu) { if(!remoteVideo.hasRemoteVideoMenu) {
remoteVideo.addRemoteVideoMenu(); remoteVideo.addRemoteVideoMenu();
@ -479,6 +501,18 @@ var VideoLayout = {
localVideoThumbnail.showAudioIndicator(isMuted); localVideoThumbnail.showAudioIndicator(isMuted);
}, },
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
showLocalConnectionInterrupted (isInterrupted) {
localVideoThumbnail.connectionIndicator
.updateConnectionStatusIndicator(!isInterrupted);
},
/** /**
* Resizes thumbnails. * Resizes thumbnails.
*/ */
@ -486,19 +520,18 @@ var VideoLayout = {
forceUpdate = false, forceUpdate = false,
onComplete = null) { onComplete = null) {
let {thumbWidth, thumbHeight} let { localVideo, remoteVideo }
= FilmStrip.calculateThumbnailSize(); = FilmStrip.calculateThumbnailSize();
$('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2); let {thumbWidth, thumbHeight} = remoteVideo;
FilmStrip.resizeThumbnails(thumbWidth, thumbHeight, FilmStrip.resizeThumbnails(localVideo, remoteVideo,
animate, forceUpdate) animate, forceUpdate)
.then(function () { .then(function () {
AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
if (onComplete && typeof onComplete === "function") if (onComplete && typeof onComplete === "function")
onComplete(); onComplete();
}); });
return {thumbWidth, thumbHeight}; return { localVideo, remoteVideo };
}, },
/** /**
@ -524,11 +557,11 @@ var VideoLayout = {
*/ */
onVideoMute (id, value) { onVideoMute (id, value) {
if (APP.conference.isLocalId(id)) { if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setMutedView(value); localVideoThumbnail.setVideoMutedView(value);
} else { } else {
let remoteVideo = remoteVideos[id]; let remoteVideo = remoteVideos[id];
if (remoteVideo) if (remoteVideo)
remoteVideo.setMutedView(value); remoteVideo.setVideoMutedView(value);
} }
if (this.isCurrentlyOnLarge(id)) { if (this.isCurrentlyOnLarge(id)) {
@ -610,6 +643,35 @@ var VideoLayout = {
} }
}, },
/**
* Shows/hides warning about remote user's connectivity issues.
*
* @param {string} id the ID of the remote participant(MUC nickname)
* @param {boolean} isActive true if the connection is ok or false when
* the user is having connectivity issues.
*/
onParticipantConnectionStatusChanged (id, isActive) {
// Show/hide warning on the large video
if (this.isCurrentlyOnLarge(id)) {
if (largeVideo) {
// We have to trigger full large video update to transition from
// avatar to video on connectivity restored.
this.updateLargeVideo(id, true /* force update */);
}
}
// Show/hide warning on the thumbnail
let remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Updating only connection status indicator is not enough, because
// when we the connection is restored while the avatar was displayed
// (due to 'muted while disconnected' condition) we may want to show
// the video stream again and in order to do that the display mode
// must be updated.
//remoteVideo.updateConnectionStatusIndicator(isActive);
remoteVideo.updateView();
}
},
/** /**
* On last N change event. * On last N change event.
* *
@ -656,7 +718,7 @@ var VideoLayout = {
var updateLargeVideo = false; var updateLargeVideo = false;
// Handle LastN/local LastN changes. // Handle LastN/local LastN changes.
FilmStrip.getThumbs().each(( index, element ) => { FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
var resourceJid = getPeerContainerResourceId(element); var resourceJid = getPeerContainerResourceId(element);
var smallVideo = remoteVideos[resourceJid]; var smallVideo = remoteVideos[resourceJid];
@ -945,28 +1007,18 @@ var VideoLayout = {
* Indicates that the video has been interrupted. * Indicates that the video has been interrupted.
*/ */
onVideoInterrupted () { onVideoInterrupted () {
this.enableVideoProblemFilter(true); if (largeVideo) {
let reconnectingKey = "connection.RECONNECTING"; largeVideo.onVideoInterrupted();
$('#videoConnectionMessage') }
.attr("data-i18n", reconnectingKey)
.text(APP.translation.translateString(reconnectingKey))
.css({display: "block"});
}, },
/** /**
* Indicates that the video has been restored. * Indicates that the video has been restored.
*/ */
onVideoRestored () { onVideoRestored () {
this.enableVideoProblemFilter(false); if (largeVideo) {
$('#videoConnectionMessage').css({display: "none"}); largeVideo.onVideoRestored();
},
enableVideoProblemFilter (enable) {
if (!largeVideo) {
return;
} }
largeVideo.enableVideoProblemFilter(enable);
}, },
isLargeVideoVisible () { isLargeVideoVisible () {
@ -994,6 +1046,7 @@ var VideoLayout = {
if (!isOnLarge || forceUpdate) { if (!isOnLarge || forceUpdate) {
let videoType = this.getRemoteVideoType(id); let videoType = this.getRemoteVideoType(id);
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) { if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id); eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
} }

View File

@ -2,6 +2,7 @@
var animateTimeout, updateTimeout; var animateTimeout, updateTimeout;
var RoomnameGenerator = require("../../util/RoomnameGenerator"); var RoomnameGenerator = require("../../util/RoomnameGenerator");
import UIUtil from "../util/UIUtil";
function enter_room() { function enter_room() {
var val = $("#enter_room_field").val(); var val = $("#enter_room_field").val();
@ -39,10 +40,10 @@ function setupWelcomePage() {
$("#welcome_page_header div[class='watermark leftwatermark']"); $("#welcome_page_header div[class='watermark leftwatermark']");
if(leftWatermarkDiv && leftWatermarkDiv.length > 0) { if(leftWatermarkDiv && leftWatermarkDiv.length > 0) {
leftWatermarkDiv.css({display: 'block'}); leftWatermarkDiv.css({display: 'block'});
leftWatermarkDiv.parent().get(0).href = UIUtil.setLinkHref(
interfaceConfig.JITSI_WATERMARK_LINK; leftWatermarkDiv.parent(),
interfaceConfig.JITSI_WATERMARK_LINK);
} }
} }
if (interfaceConfig.SHOW_BRAND_WATERMARK) { if (interfaceConfig.SHOW_BRAND_WATERMARK) {
@ -50,8 +51,9 @@ function setupWelcomePage() {
$("#welcome_page_header div[class='watermark rightwatermark']"); $("#welcome_page_header div[class='watermark rightwatermark']");
if(rightWatermarkDiv && rightWatermarkDiv.length > 0) { if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
rightWatermarkDiv.css({display: 'block'}); rightWatermarkDiv.css({display: 'block'});
rightWatermarkDiv.parent().get(0).href = UIUtil.setLinkHref(
interfaceConfig.BRAND_WATERMARK_LINK; rightWatermarkDiv.parent(),
interfaceConfig.BRAND_WATERMARK_LINK);
rightWatermarkDiv.get(0).style.backgroundImage = rightWatermarkDiv.get(0).style.backgroundImage =
"url(images/rightwatermark.png)"; "url(images/rightwatermark.png)";
} }

View File

@ -74,13 +74,6 @@ var KeyboardShortcut = {
} }
} }
}; };
$('body').popover({ selector: '[data-toggle=popover]',
trigger: 'click hover',
content: function() {
return this.getAttribute("content")
+ self._getShortcutTooltip(this.getAttribute("shortcut"));
}
});
}, },
/** /**
@ -128,7 +121,7 @@ var KeyboardShortcut = {
* or an empty string if the shortcutAttr is null, an empty string or not * or an empty string if the shortcutAttr is null, an empty string or not
* found in the shortcut mapping * found in the shortcut mapping
*/ */
_getShortcutTooltip: function (shortcutAttr) { getShortcutTooltip: function (shortcutAttr) {
if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) { if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) {
for (var key in _shortcuts) { for (var key in _shortcuts) {
if (_shortcuts.hasOwnProperty(key) if (_shortcuts.hasOwnProperty(key)

View File

@ -174,10 +174,12 @@ export default {
* Set device id of the camera which is currently in use. * Set device id of the camera which is currently in use.
* Empty string stands for default device. * Empty string stands for default device.
* @param {string} newId new camera device id * @param {string} newId new camera device id
* @param {boolean} whether we need to store the value
*/ */
setCameraDeviceId: function (newId = '') { setCameraDeviceId: function (newId, store) {
cameraDeviceId = newId; cameraDeviceId = newId;
window.localStorage.cameraDeviceId = newId; if (store)
window.localStorage.cameraDeviceId = newId;
}, },
/** /**
@ -192,10 +194,12 @@ export default {
* Set device id of the microphone which is currently in use. * Set device id of the microphone which is currently in use.
* Empty string stands for default device. * Empty string stands for default device.
* @param {string} newId new microphone device id * @param {string} newId new microphone device id
* @param {boolean} whether we need to store the value
*/ */
setMicDeviceId: function (newId = '') { setMicDeviceId: function (newId, store) {
micDeviceId = newId; micDeviceId = newId;
window.localStorage.micDeviceId = newId; if (store)
window.localStorage.micDeviceId = newId;
}, },
/** /**

View File

@ -16,23 +16,24 @@
"readmeFilename": "README.md", "readmeFilename": "README.md",
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)", "//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
"dependencies": { "dependencies": {
"@atlassian/aui": "^6.0.0",
"async": "0.9.0", "async": "0.9.0",
"autosize": "^1.18.13", "autosize": "^1.18.13",
"bootstrap": "3.1.1", "bootstrap": "3.1.1",
"events": "*", "events": "*",
"i18next-client": "1.7.7", "i18next-client": "1.7.7",
"jquery": "~2.1.1",
"jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0", "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
"lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git", "jquery": "~2.1.1",
"jquery-contextmenu": "*", "jquery-contextmenu": "*",
"jquery-ui": "1.10.5", "jquery-ui": "1.10.5",
"jssha": "1.5.0", "jssha": "1.5.0",
"jws": "*",
"lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
"postis": "^2.2.0",
"retry": "0.6.1", "retry": "0.6.1",
"strophe": "^1.2.2", "strophe": "^1.2.2",
"strophejs-plugins": "^0.0.6", "strophejs-plugins": "^0.0.6",
"toastr": "^2.0.3", "toastr": "^2.0.3"
"postis": "^2.2.0",
"jws": "*"
}, },
"devDependencies": { "devDependencies": {
"babel-polyfill": "*", "babel-polyfill": "*",
@ -83,7 +84,11 @@
"tooltip": "./node_modules/bootstrap/js/tooltip.js", "tooltip": "./node_modules/bootstrap/js/tooltip.js",
"popover": "./node_modules/bootstrap/js/popover.js", "popover": "./node_modules/bootstrap/js/popover.js",
"jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js", "jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js",
"autosize": "./node_modules/autosize/build/jquery.autosize.js" "autosize": "./node_modules/autosize/build/jquery.autosize.js",
"aui": "./node_modules/@atlassian/aui/dist/aui/js/aui.js",
"aui-experimental": "./node_modules/@atlassian/aui/dist/aui/js/aui-experimental.js",
"aui-css": "./node_modules/@atlassian/aui/dist/aui/css/aui.min.css",
"aui-experimental-css": "./node_modules/@atlassian/aui/dist/aui/css/aui-experimental.min.css"
}, },
"browserify-shim": { "browserify-shim": {
"jquery": [ "jquery": [
@ -109,6 +114,9 @@
"jQuery-Impromptu": { "jQuery-Impromptu": {
"depends": "jquery:jQuery" "depends": "jquery:jQuery"
}, },
"aui-experimental": {
"depends": "aui:AJS"
},
"jquery-contextmenu": { "jquery-contextmenu": {
"depends": "jquery:jQuery" "depends": "jquery:jQuery"
}, },

View File

@ -60,7 +60,7 @@ local function verify_user(session, stanza)
local token = session.auth_token; local token = session.auth_token;
local auth_room = session.jitsi_meet_room; local auth_room = session.jitsi_meet_room;
if room ~= auth_room and disableRoomNameConstraints ~= true then if disableRoomNameConstraints ~= true and room ~= string.lower(auth_room) then
log("error", "Token %s not allowed to join: %s", log("error", "Token %s not allowed to join: %s",
tostring(token), tostring(auth_room)); tostring(token), tostring(auth_room));
session.send( session.send(

View File

@ -29,7 +29,6 @@ export default {
*/ */
UPDATE_SHARED_VIDEO: "UI.update_shared_video", UPDATE_SHARED_VIDEO: "UI.update_shared_video",
ROOM_LOCK_CLICKED: "UI.room_lock_clicked", ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
USER_INVITED: "UI.user_invited",
USER_KICKED: "UI.user_kicked", USER_KICKED: "UI.user_kicked",
REMOTE_AUDIO_MUTED: "UI.remote_audio_muted", REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
FULLSCREEN_TOGGLE: "UI.fullscreen_toggle", FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
@ -105,5 +104,15 @@ export default {
* event must contain the identifier of the container that has been toggled * event must contain the identifier of the container that has been toggled
* and information about toggle on or off. * and information about toggle on or off.
*/ */
SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled" SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled",
/**
* Notifies that the raise hand has been changed.
*/
LOCAL_RAISE_HAND_CHANGED: "UI.local_raise_hand_changed",
/**
* Notifies that the avatar is displayed or not on the largeVideo.
*/
LARGE_VIDEO_AVATAR_DISPLAYED: "UI.large_video_avatar_displayed"
}; };

View File

@ -9,16 +9,19 @@ module.exports = {
return languages; return languages;
}, },
EN: "en", EN: "en",
BG: "bg", BG: "bg",
DE: "de", DE: "de",
TR: "tr",
FR: "fr",
ES: "es", ES: "es",
FR: "fr",
HY: "hy", HY: "hy",
IT: "it", IT: "it",
OC: "oc", OC: "oc",
PL: "pl",
PTBR: "ptBR", PTBR: "ptBR",
RU: "ru",
SK: "sk", SK: "sk",
SL: "sl", SL: "sl",
SV: "sv" SV: "sv",
TR: "tr"
}; };