diff --git a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js index bb17ee6a320488c2b647f8fb8c6b11a2fd39e09a..765bae598df15bac36713eb10c3189a089ae9878 100755 --- a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js +++ b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js @@ -534,12 +534,15 @@ function make_call(username, voiceBridge, server, callback, recall, isListenOnly currentSession.mediaHandler.on('iceConnectionConnected', function() { console.log('Received ICE status changed to connected'); if (callICEConnected === false) { - callICEConnected = true; - clearTimeout(iceConnectedTimeout); - if (callActive === true) { - callback({'status':'started'}); + // Edge is only ready once the status is 'completed' so we need to skip this step + if (!bowser.msedge) { + callICEConnected = true; + clearTimeout(iceConnectedTimeout); + if (callActive === true) { + callback({'status':'started'}); + } + clearTimeout(callTimeout); } - clearTimeout(callTimeout); } }); diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js index 9f48f5710b5e8464d70f25c0635125a1015a07a6..f52b06bc249bd6b08455f74818e88f36ca07f7d6 100755 --- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js +++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js @@ -3,7 +3,7 @@ const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; const isChrome = !!window.chrome && !isOpera; const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome; const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function' - || typeof navigator.mediaDevices.getDisplayMedia === 'function'); + || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); const kurentoHandler = null; const SEND_ROLE = "send"; const RECV_ROLE = "recv"; diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.js index 2a8bfab960387681317951b9428c19110d407a8a..22d8087010b9bef36f97f5da546941b078f42c91 100644 --- a/bigbluebutton-client/resources/prod/lib/kurento-utils.js +++ b/bigbluebutton-client/resources/prod/lib/kurento-utils.js @@ -424,7 +424,7 @@ function WebRtcPeer(mode, options, callback) { navigator.getDisplayMedia(recursive.apply(undefined, constraints)) .then(gDMCallback) .catch(callback); - } else if (typeof navigator.mediaDevices.getDisplayMedia === 'function') { + } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints)) .then(gDMCallback) .catch(callback); diff --git a/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js b/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js deleted file mode 100644 index a29923966fa58015032c35d3daa8f3e8c68d5696..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js +++ /dev/null @@ -1,601 +0,0 @@ - -var userID, callerIdName=null, conferenceVoiceBridge, userAgent=null, userMicMedia, userWebcamMedia, currentSession=null, callTimeout, callActive, callICEConnected, iceConnectedTimeout, callFailCounter, callPurposefullyEnded, uaConnected, transferTimeout, iceGatheringTimeout; -var inEchoTest = true; -var html5StunTurn = null; - -function webRTCCallback(message) { - switch (message.status) { - case 'succeded': - BBB.webRTCCallSucceeded(); - break; - case 'failed': - if (message.errorcode !== 1004) { - message.cause = null; - } - monitorTracksStop(); - BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause); - break; - case 'ended': - monitorTracksStop(); - BBB.webRTCCallEnded(inEchoTest); - break; - case 'started': - monitorTracksStart(); - BBB.webRTCCallStarted(inEchoTest); - break; - case 'connecting': - BBB.webRTCCallConnecting(inEchoTest); - break; - case 'waitingforice': - BBB.webRTCCallWaitingForICE(inEchoTest); - break; - case 'transferring': - BBB.webRTCCallTransferring(inEchoTest); - break; - case 'mediarequest': - BBB.webRTCMediaRequest(); - break; - case 'mediasuccess': - BBB.webRTCMediaSuccess(); - break; - case 'mediafail': - BBB.webRTCMediaFail(); - break; - } -} - -function callIntoConference(voiceBridge, callback, isListenOnly, stunTurn = null) { - // root of the call initiation process from the html5 client - // Flash will not pass in the listen only field. For html5 it is optional. Assume NOT listen only if no state passed - if (isListenOnly == null) { - isListenOnly = false; - } - - // if additional stun configuration is passed, store the information - if (stunTurn != null) { - html5StunTurn = { - stunServers: stunTurn.stun, - turnServers: stunTurn.turn, - }; - } - - // reset callerIdName - callerIdName = null; - if (!callerIdName) { - BBB.getMyUserInfo(function(userInfo) { - console.log("User info callback [myUserID=" + userInfo.myUserID - + ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL - + ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter - + ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge - + ",isListenOnly=" + isListenOnly + "]."); - userID = userInfo.myUserID; - callerIdName = userInfo.myUserID + "-bbbID-" + userInfo.myUsername; - if (isListenOnly) { - //prepend the callerIdName so it is recognized as a global audio user - callerIdName = "GLOBAL_AUDIO_" + callerIdName; - } - conferenceVoiceBridge = userInfo.voiceBridge - if (voiceBridge === "9196") { - voiceBridge = voiceBridge + conferenceVoiceBridge; - } else { - voiceBridge = conferenceVoiceBridge; - } - console.log(callerIdName); - webrtc_call(callerIdName, voiceBridge, callback, isListenOnly); - }); - } else { - if (voiceBridge === "9196") { - voiceBridge = voiceBridge + conferenceVoiceBridge; - } else { - voiceBridge = conferenceVoiceBridge; - } - webrtc_call(callerIdName, voiceBridge, callback, isListenOnly); - } -} - -function joinWebRTCVoiceConference() { - console.log("Joining to the voice conference directly"); - inEchoTest = false; - // set proper callbacks to previously created user agent - if(userAgent) { - setUserAgentListeners(webRTCCallback); - } - callIntoConference(conferenceVoiceBridge, webRTCCallback); -} - -function leaveWebRTCVoiceConference() { - console.log("Leaving the voice conference"); - - webrtc_hangup(); -} - -function startWebRTCAudioTest(){ - console.log("Joining the audio test first"); - inEchoTest = true; - callIntoConference("9196", webRTCCallback); -} - -function stopWebRTCAudioTest(){ - console.log("Stopping webrtc audio test"); - - webrtc_hangup(); -} - -function stopWebRTCAudioTestJoinConference(){ - console.log("Transferring from audio test to conference"); - - webRTCCallback({'status': 'transferring'}); - - transferTimeout = setTimeout( function() { - console.log("Call transfer failed. No response after 3 seconds"); - webRTCCallback({'status': 'failed', 'errorcode': 1008}); - releaseUserMedia(); - currentSession = null; - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - }, 5000); - - BBB.listen("UserJoinedVoiceEvent", userJoinedVoiceHandler); - - currentSession.dtmf(1); - inEchoTest = false; -} - -function userJoinedVoiceHandler(event) { - console.log("UserJoinedVoiceHandler - " + event); - if (inEchoTest === false && userID === event.userID) { - BBB.unlisten("UserJoinedVoiceEvent", userJoinedVoiceHandler); - clearTimeout(transferTimeout); - webRTCCallback({'status': 'started'}); - } -} - -function createUA(username, server, callback, makeCallFunc) { - if (userAgent) { - console.log("User agent already created"); - return; - } - - console.log("Fetching STUN/TURN server info for user agent"); - - console.log(html5StunTurn); - if (html5StunTurn != null) { - createUAWithStuns(username, server, callback, html5StunTurn, makeCallFunc); - return; - } - - BBB.getSessionToken(function(sessionToken) { - $.ajax({ - dataType: 'json', - url: '/bigbluebutton/api/stuns', - data: {sessionToken:sessionToken} - }).done(function(data) { - var stunsConfig = {}; - stunsConfig['stunServers'] = ( data['stunServers'] ? data['stunServers'].map(function(data) { - return data['url']; - }) : [] ); - stunsConfig['turnServers'] = ( data['turnServers'] ? data['turnServers'].map(function(data) { - return { - 'urls': data['url'], - 'username': data['username'], - 'password': data['password'] - }; - }) : [] ); - //stunsConfig['remoteIceCandidates'] = ( data['remoteIceCandidates'] ? data['remoteIceCandidates'].map(function(data) { - // return data['ip']; - //}) : [] ); - createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc); - }).fail(function(data, textStatus, errorThrown) { - BBBLog.error("Could not fetch stun/turn servers", {error: textStatus, user: callerIdName, voiceBridge: conferenceVoiceBridge}); - callback({'status':'failed', 'errorcode': 1009}); - }); - }); -} - -function createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc) { - console.log("Creating new user agent"); - - /* VERY IMPORTANT - * - You must escape the username because spaces will cause the connection to fail - * - We are connecting to the websocket through an nginx redirect instead of directly to 5066 - */ - var configuration = { - uri: 'sip:' + encodeURIComponent(username) + '@' + server, - wsServers: ('https:' == document.location.protocol ? 'wss://' : 'ws://') + server + '/ws', - displayName: username, - register: false, - traceSip: true, - autostart: false, - userAgentString: "BigBlueButton", - stunServers: stunsConfig['stunServers'], - turnServers: stunsConfig['turnServers'], - //artificialRemoteIceCandidates: stunsConfig['remoteIceCandidates'] - }; - - uaConnected = false; - - userAgent = new SIP.UA(configuration); - setUserAgentListeners(callback, makeCallFunc); - userAgent.start(); -}; - -function setUserAgentListeners(callback, makeCallFunc) { - console.log("resetting UA callbacks"); - userAgent.removeAllListeners('connected'); - userAgent.on('connected', function() { - uaConnected = true; - callback({'status':'succeded'}); - makeCallFunc(); - }); - userAgent.removeAllListeners('disconnected'); - userAgent.on('disconnected', function() { - if (userAgent) { - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - - if (uaConnected) { - callback({'status':'failed', 'errorcode': 1001}); // WebSocket disconnected - } else { - callback({'status':'failed', 'errorcode': 1002}); // Could not make a WebSocket connection - } - } - }); -}; - -function getUserMicMedia(getUserMicMediaSuccess, getUserMicMediaFailure) { - if (userMicMedia == undefined) { - if (SIP.WebRTC.isSupported()) { - SIP.WebRTC.getUserMedia({audio:true, video:false}, getUserMicMediaSuccess, getUserMicMediaFailure); - } else { - console.log("getUserMicMedia: webrtc not supported"); - getUserMicMediaFailure("WebRTC is not supported"); - } - } else { - console.log("getUserMicMedia: mic already set"); - getUserMicMediaSuccess(userMicMedia); - } -}; - -function webrtc_call(username, voiceBridge, callback, isListenOnly) { - if (!isWebRTCAvailable()) { - callback({'status': 'failed', 'errorcode': 1003}); // Browser version not supported - return; - } - if (isListenOnly == null) { // assume NOT listen only unless otherwise stated - isListenOnly = false; - } - - var server = window.document.location.hostname; - console.log("user " + username + " calling to " + voiceBridge); - - var makeCallFunc = function() { - // only make the call when both microphone and useragent have been created - // for listen only, stating listen only is a viable substitute for acquiring user media control - if ((isListenOnly||userMicMedia) && userAgent) - make_call(username, voiceBridge, server, callback, false, isListenOnly); - }; - - // Reset userAgent so we can successfully switch between listenOnly and listen+speak modes - userAgent = null; - if (!userAgent) { - createUA(username, server, callback, makeCallFunc); - } - // if the user requests to proceed as listen only (does not require media) or media is already acquired, - // proceed with making the call - if (isListenOnly || userMicMedia != null) { - makeCallFunc(); - } else { - callback({'status':'mediarequest'}); - getUserMicMedia(function(stream) { - console.log("getUserMicMedia: success"); - userMicMedia = stream; - callback({'status':'mediasuccess'}); - makeCallFunc(); - }, function(e) { - console.error("getUserMicMedia: failure - " + e); - callback({'status':'mediafail', 'cause': e}); - } - ); - } -} - -function make_call(username, voiceBridge, server, callback, recall, isListenOnly) { - if (isListenOnly == null) { - isListenOnly = false; - } - - if (userAgent == null) { - console.log("userAgent is still null. Delaying call"); - var callDelayTimeout = setTimeout( function() { - make_call(username, voiceBridge, server, callback, recall, isListenOnly); - }, 100); - return; - } - - if (!userAgent.isConnected()) { - console.log("Trying to make call, but UserAgent hasn't connected yet. Delaying call"); - userAgent.once('connected', function() { - console.log("UserAgent has now connected, retrying the call"); - make_call(username, voiceBridge, server, callback, recall, isListenOnly); - }); - return; - } - - if (currentSession) { - console.log('Active call detected ignoring second make_call'); - return; - } - - // Make an audio/video call: - console.log("Setting options.. "); - - var options = {}; - if (isListenOnly) { - // create necessary options for a listen only stream - var stream = null; - // handle the web browser - // create a stream object through the browser separated from user media - if (typeof webkitMediaStream !== 'undefined') { - // Google Chrome - stream = new webkitMediaStream; - } else { - // Firefox - audioContext = new window.AudioContext; - stream = audioContext.createMediaStreamDestination().stream; - } - - options = { - media: { - stream: stream, // use the stream created above - constraints: { - audio: true, - video: false - }, - render: { - remote: document.getElementById('remote-media') - } - }, - // a list of our RTC Connection constraints - RTCConstraints: { - // our constraints are mandatory. We must received audio and must not receive audio - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false - } - } - }; - } else { - options = { - media: { - stream: userMicMedia, - constraints: { - audio: true, - video: false - }, - render: { - remote: document.getElementById('remote-media') - } - } - }; - } - - callTimeout = setTimeout(function() { - console.log('Ten seconds without updates sending timeout code'); - callback({'status':'failed', 'errorcode': 1006}); // Failure on call - releaseUserMedia(); - currentSession = null; - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - }, 10000); - - callActive = false; - callICEConnected = false; - callPurposefullyEnded = false; - callFailCounter = 0; - console.log("Calling to " + voiceBridge + "...."); - currentSession = userAgent.invite('sip:' + voiceBridge + '@' + server, options); - - // Only send the callback if it's the first try - if (recall === false) { - console.log('call connecting'); - callback({'status':'connecting'}); - } else { - console.log('call connecting again'); - } - - /* - iceGatheringTimeout = setTimeout(function() { - console.log('Thirty seconds without ICE gathering finishing'); - callback({'status':'failed', 'errorcode': 1011}); // ICE Gathering Failed - releaseUserMedia(); - currentSession = null; - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - }, 30000); - */ - - currentSession.mediaHandler.on('iceGatheringComplete', function() { - clearTimeout(iceGatheringTimeout); - }); - - // The connecting event fires before the listener can be added - currentSession.on('connecting', function(){ - clearTimeout(callTimeout); - }); - currentSession.on('progress', function(response){ - console.log('call progress: ' + response); - clearTimeout(callTimeout); - }); - currentSession.on('failed', function(response, cause){ - console.log('call failed with cause: '+ cause); - - if (currentSession) { - releaseUserMedia(); - if (callActive === false) { - callback({'status':'failed', 'errorcode': 1004, 'cause': cause}); // Failure on call - currentSession = null; - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - } else { - callActive = false; - //currentSession.bye(); - currentSession = null; - if (userAgent != null) { - userAgent.stop(); - } - } - } - clearTimeout(callTimeout); - }); - currentSession.on('bye', function(request){ - callActive = false; - - if (currentSession) { - console.log('call ended ' + currentSession.endTime); - releaseUserMedia(); - if (callPurposefullyEnded === true) { - callback({'status':'ended'}); - } else { - callback({'status':'failed', 'errorcode': 1005}); // Call ended unexpectedly - } - clearTimeout(callTimeout); - currentSession = null; - } else { - console.log('bye event already received'); - } - }); - currentSession.on('cancel', function(request) { - callActive = false; - - if (currentSession) { - console.log('call canceled'); - releaseUserMedia(); - clearTimeout(callTimeout); - currentSession = null; - } else { - console.log('cancel event already received'); - } - }); - currentSession.on('accepted', function(data){ - callActive = true; - console.log('BigBlueButton call accepted'); - - if (callICEConnected === true) { - callback({'status':'started'}); - } else { - callback({'status':'waitingforice'}); - console.log('Waiting for ICE negotiation'); - iceConnectedTimeout = setTimeout(function() { - console.log('5 seconds without ICE finishing'); - callback({'status':'failed', 'errorcode': 1010}); // ICE negotiation timeout - releaseUserMedia(); - currentSession = null; - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - }, 5000); - } - clearTimeout(callTimeout); - }); - currentSession.mediaHandler.on('iceConnectionFailed', function() { - console.log('received ice negotiation failed'); - callback({'status':'failed', 'errorcode': 1007}); // Failure on call - releaseUserMedia(); - currentSession = null; - clearTimeout(iceConnectedTimeout); - if (userAgent != null) { - var userAgentTemp = userAgent; - userAgent = null; - userAgentTemp.stop(); - } - - clearTimeout(callTimeout); - }); - - // Some browsers use status of 'connected', others use 'completed', and a couple use both - - currentSession.mediaHandler.on('iceConnectionConnected', function() { - console.log('Received ICE status changed to connected'); - if (callICEConnected === false) { - callICEConnected = true; - clearTimeout(iceConnectedTimeout); - if (callActive === true) { - callback({'status':'started'}); - } - clearTimeout(callTimeout); - } - }); - - currentSession.mediaHandler.on('iceConnectionCompleted', function() { - console.log('Received ICE status changed to completed'); - if (callICEConnected === false) { - callICEConnected = true; - clearTimeout(iceConnectedTimeout); - if (callActive === true) { - callback({'status':'started'}); - } - clearTimeout(callTimeout); - } - }); -} - -function webrtc_hangup(callback) { - callPurposefullyEnded = true; - - console.log("Hanging up current session"); - if (callback) { - currentSession.on('bye', callback); - } - try { - currentSession.bye(); - } catch (err) { - console.log("Forcing to cancel current session"); - currentSession.cancel(); - } -} - -function releaseUserMedia() { - if (!!userMicMedia) { - console.log("Releasing media tracks"); - - userMicMedia.getAudioTracks().forEach(function(track) { - track.stop(); - }); - - userMicMedia.getVideoTracks().forEach(function(track) { - track.stop(); - }); - - userMicMedia = null; - } -} - -function isWebRTCAvailable() { - if (bowser.msedge) { - return false; - } else { - return SIP.WebRTC.isSupported(); - } -} - -function getCallStatus() { - return currentSession; -} - diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js index 0a181e56cfe3e370016420300b12f28d67ccf32d..8c167fbb790681baabf7e0bdc3eade81cd808501 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-extension.js +++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js @@ -4,7 +4,7 @@ const isChrome = !!window.chrome && !isOpera; const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome; const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1; const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function' - || typeof navigator.mediaDevices.getDisplayMedia === 'function'); + || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); const kurentoHandler = null; Kurento = function ( diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js index 7e118d2d9d17b6a9a30dedc7a6251e75c0f3c40d..bf621aff2c463c0da73f64af7dcc9a8c94b19752 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-utils.js +++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js @@ -475,7 +475,7 @@ function WebRtcPeer(mode, options, callback) { navigator.getDisplayMedia(recursive.apply(undefined, constraints)) .then(gDMCallback) .catch(callback); - } else if (typeof navigator.mediaDevices.getDisplayMedia === 'function') { + } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints)) .then(gDMCallback) .catch(callback); diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 6c7492068c871171e9162a321043740dc672d95a..2892e505818075c975b467c9ccf299f97ad9cfd6 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import BaseAudioBridge from './base'; import logger from '/imports/startup/client/logger'; import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers'; - +import browser from 'browser-detect'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -279,7 +279,14 @@ export default class SIPBridge extends BaseAudioBridge { return new Promise((resolve) => { const { mediaHandler } = currentSession; - const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected']; + let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected']; + // Edge sends a connected first and then a completed, but the call isn't ready until + // the completed comes in. Due to the way that we have the listeners set up, the only + // way to ignore one status is to not listen for it. + if (browser().name === 'edge') { + connectionCompletedEvents = ['iceConnectionCompleted']; + } + const handleConnectionCompleted = () => { connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted)); // We have to delay notifying that the call is connected because it is sometimes not diff --git a/bigbluebutton-html5/imports/api/external-videos/index.js b/bigbluebutton-html5/imports/api/external-videos/index.js index 8653beaffe732de32ca2f5bd0ef48ef14d71d75c..7b2a427b4a8bd3c3ee396be4882ca8837164f338 100644 --- a/bigbluebutton-html5/imports/api/external-videos/index.js +++ b/bigbluebutton-html5/imports/api/external-videos/index.js @@ -2,6 +2,4 @@ import { Meteor } from 'meteor/meteor'; const Streamer = new Meteor.Streamer('videos'); -export default Streamer - - +export default Streamer; diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index 4310a9fe0b0ab6c10a15f3b056b6879ed20f5e4d..e69961db74dbb8b2f0c01103fbc3e079fd481197 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -17,7 +17,7 @@ export default function addUserSettings(credentials, meetingId, userId, settings 'forceListenOnly', 'skipCheck', 'clientTitle', - 'lockOnJoin', // NOT IMPLEMENTED YET + 'lockOnJoin', 'askForFeedbackOnLogout', // BRANDING 'displayBrandingArea', diff --git a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js index 5d9f55fb0e98792130cb21a2144bcd952d1ef31d..de7a3e4d787cb1a54afeab9384f9d646b2f4ce68 100644 --- a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js @@ -20,4 +20,3 @@ RedisPubSub.on('GuestsWaitingForApprovalEvtMsg', handleGuestsWaitingForApproval) RedisPubSub.on('GuestsWaitingApprovedEvtMsg', handleGuestApproved); RedisPubSub.on('UserEjectedFromMeetingEvtMsg', handleUserEjected); RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole); - diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js index 739c8d1dbf6486c08474c5c7e638998cf97e906e..6736d3a1ba081353377d606318719386d0bd21fa 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods.js +++ b/bigbluebutton-html5/imports/api/users/server/methods.js @@ -4,6 +4,7 @@ import setEmojiStatus from './methods/setEmojiStatus'; import assignPresenter from './methods/assignPresenter'; import changeRole from './methods/changeRole'; import removeUser from './methods/removeUser'; +import toggleUserLock from './methods/toggleUserLock'; Meteor.methods({ setEmojiStatus, @@ -11,4 +12,5 @@ Meteor.methods({ changeRole, removeUser, validateAuthToken, + toggleUserLock, }); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js new file mode 100644 index 0000000000000000000000000000000000000000..0919194b9261f371210f5981ea3fdca69be95476 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; + +export default function toggleUserLock(credentials, userId, lock) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'LockUserInMeetingCmdMsg'; + + const { meetingId, requesterUserId: lockedBy } = credentials; + + check(meetingId, String); + check(lockedBy, String); + check(userId, String); + check(lock, Boolean); + + const payload = { + lockedBy, + userId, + lock, + }; + + Logger.verbose(`User ${lockedBy} updated lock status from ${userId} to ${lock} + in meeting ${meetingId}`); + + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload); +} diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index b400dc9894ba8f41c0562f66045a49bbebd3ad23..a1407f8d2b7d8afa9c74a9b20843bd2b29d2cc97 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -5,41 +5,43 @@ import Settings from '/imports/ui/services/settings'; import LoadingScreen from '/imports/ui/components/loading-screen/component'; // currently supported locales. -import en from 'react-intl/locale-data/en'; -import uk from 'react-intl/locale-data/uk'; -import zh from 'react-intl/locale-data/zh'; -import ru from 'react-intl/locale-data/ru'; +import bg from 'react-intl/locale-data/bg'; import de from 'react-intl/locale-data/de'; -import fr from 'react-intl/locale-data/fr'; -import pt from 'react-intl/locale-data/pt'; -import fa from 'react-intl/locale-data/fa'; -import tr from 'react-intl/locale-data/tr'; -import ja from 'react-intl/locale-data/ja'; -import km from 'react-intl/locale-data/km'; +import el from 'react-intl/locale-data/el'; +import en from 'react-intl/locale-data/en'; import es from 'react-intl/locale-data/es'; +import fa from 'react-intl/locale-data/fa'; +import fr from 'react-intl/locale-data/fr'; import id from 'react-intl/locale-data/id'; -import el from 'react-intl/locale-data/el'; import it from 'react-intl/locale-data/it'; -import bg from 'react-intl/locale-data/bg'; +import ja from 'react-intl/locale-data/ja'; +import km from 'react-intl/locale-data/km'; +import pl from 'react-intl/locale-data/pl'; +import pt from 'react-intl/locale-data/pt'; +import ru from 'react-intl/locale-data/ru'; +import tr from 'react-intl/locale-data/tr'; +import uk from 'react-intl/locale-data/uk'; +import zh from 'react-intl/locale-data/zh'; addLocaleData([ - ...en, - ...uk, - ...zh, - ...ru, + ...bg, ...de, - ...fr, - ...pt, - ...fa, - ...tr, - ...ja, - ...km, + ...el, + ...en, ...es, + ...fa, + ...fr, ...id, - ...el, ...it, - ...bg, + ...ja, + ...km, + ...pl, + ...pt, + ...ru, + ...tr, + ...uk, + ...zh, ]); const propTypes = { diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index c8aeebdeb2c1ff6f44b4facb3e3437e9215c8048..b69f2f73affe5252b2c4423e7f853b7a03fa6151 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -145,8 +145,6 @@ class ActionsDropdown extends Component { pollBtnDesc, presentationLabel, presentationDesc, - startRecording, - stopRecording, createBreakoutRoom, createBreakoutRoomDesc, invitationItem, @@ -158,6 +156,10 @@ class ActionsDropdown extends Component { formatMessage, } = intl; + const canCreateBreakout = isUserModerator + && !meetingIsBreakout + && !hasBreakoutRoom; + const canInviteUsers = isUserModerator && !meetingIsBreakout && hasBreakoutRoom @@ -209,12 +211,12 @@ class ActionsDropdown extends Component { /> ) : null), - (isUserModerator && !meetingIsBreakout && !hasBreakoutRoom + (canCreateBreakout ? ( <DropdownListItem icon="rooms" - label={intl.formatMessage(intlMessages.createBreakoutRoom)} - description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)} + label={formatMessage(createBreakoutRoom)} + description={formatMessage(createBreakoutRoomDesc)} key={this.createBreakoutRoomId} onClick={this.onCreateBreakouts} /> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index e6195e3aa32dee3b9cd18a07d560a61f3fa1837e..7614f4c115c68a507cc83d466ba4284e9481d9c7 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -4,7 +4,9 @@ import { styles } from './styles.scss'; import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; -import JoinVideoOptionsContainer from '../video-provider/video-menu/container'; +import JoinVideoOptionsContainer from '../video-provider/video-button/container'; + +import PresentationOptionsContainer from './presentation-options/component'; class ActionsBar extends React.PureComponent { render() { @@ -25,6 +27,8 @@ class ActionsBar extends React.PureComponent { hasBreakoutRoom, meetingName, users, + isLayoutSwapped, + toggleSwapLayout, getUsersNotAssigned, sendInvitation, getBreakouts, @@ -89,6 +93,16 @@ class ActionsBar extends React.PureComponent { }} /> </div> + <div className={styles.right}> + { isLayoutSwapped + ? ( + <PresentationOptionsContainer + toggleSwapLayout={toggleSwapLayout} + /> + ) + : null + } + </div> </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index ad7eac73e8ce91b3e711edd68e9c78f0ac3d6c2e..d6bda329699bd5cc16122e788a63c61f396f508c 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -9,6 +9,8 @@ import Service from './service'; import VideoService from '../video-provider/service'; import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/service'; +import MediaService, { getSwapLayout } from '../media/service'; + const ActionsBarContainer = props => <ActionsBar {...props} />; export default withTracker(() => { @@ -42,6 +44,8 @@ export default withTracker(() => { hasBreakoutRoom: Service.hasBreakoutRoom(), meetingName: Service.meetingName(), users: Service.users(), + isLayoutSwapped: getSwapLayout(), + toggleSwapLayout: MediaService.toggleSwapLayout, sendInvitation: Service.sendInvitation, getBreakouts: Service.getBreakouts, getUsersNotAssigned: Service.getUsersNotAssigned, diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx index 22276a96008f85189fc5865c986e7d446e36f5d1..9ba4aceedcdb90864406df5c94514e7b6714e144 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx @@ -127,6 +127,10 @@ class BreakoutRoom extends Component { preventClosing: true, valid: true, }; + + this.breakoutFormId = _.uniqueId('breakout-form-'); + this.freeJoinId = _.uniqueId('free-join-check-'); + this.btnLevelId = _.uniqueId('btn-set-level-'); } componentDidMount() { @@ -155,7 +159,7 @@ class BreakoutRoom extends Component { freeJoin, } = this.state; - if (users.length === this.getUserByRoom(0).length) { + if (users.length === this.getUserByRoom(0).length && !freeJoin) { this.setState({ valid: false }); return; } @@ -295,7 +299,7 @@ class BreakoutRoom extends Component { }; return ( - <div className={styles.boxContainer}> + <div className={styles.boxContainer} key="rooms-grid-"> <label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}> <p className={styles.freeJoinLabel} @@ -340,7 +344,7 @@ class BreakoutRoom extends Component { if (isInvitation) return null; return ( - <div className={styles.breakoutSettings}> + <div className={styles.breakoutSettings} key={this.breakoutFormId}> <label htmlFor="numberOfRooms"> <p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p> <select @@ -422,7 +426,7 @@ class BreakoutRoom extends Component { if (isInvitation) return null; const { freeJoin } = this.state; return ( - <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}> + <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}> <input type="checkbox" className={styles.freeJoinCheckbox} @@ -534,6 +538,7 @@ class BreakoutRoom extends Component { size="lg" label={label} onClick={() => this.setState({ formFillLevel: level })} + key={this.btnLevelId} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..f46adc2b4b44114862917f84441e73c51311a932 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import { styles } from '../styles'; + +const propTypes = { + intl: intlShape.isRequired, + toggleSwapLayout: PropTypes.func.isRequired, +}; + +const intlMessages = defineMessages({ + restorePresentationLabel: { + id: 'app.actionsBar.actionsDropdown.restorePresentationLabel', + description: 'Restore Presentation option label', + }, + restorePresentationDesc: { + id: 'app.actionsBar.actionsDropdown.restorePresentationDesc', + description: 'button to restore presentation after it has been closed', + }, +}); + +const PresentationOptionsContainer = ({ intl, toggleSwapLayout }) => ( + <Button + className={styles.button} + icon="presentation" + label={intl.formatMessage(intlMessages.restorePresentationLabel)} + description={intl.formatMessage(intlMessages.restorePresentationDesc)} + color="primary" + hideLabel + circle + size="lg" + onClick={toggleSwapLayout} + id="restore-presentation" + /> +); + +PresentationOptionsContainer.propTypes = propTypes; +export default injectIntl(PresentationOptionsContainer); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 7619bf7adca27ee4fba95d6505aed73b602ed7be..31a39e40089a87cef80de64cf3a5f25d942efffd 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -2,13 +2,15 @@ .actionsbar, .left, -.center { +.center, +.right { display: flex; flex-direction: row; } .left, -.center { +.center, +.right { flex: 1; justify-content: center; @@ -29,6 +31,12 @@ } } +.right { + position: absolute; + bottom: var(--sm-padding-x); + right: var(--sm-padding-x); +} + .centerWithActions { @include mq($xsmall-only) { justify-content: flex-end; @@ -57,4 +65,4 @@ span:hover { border: 1.5px solid rgba(255,255,255, .5) !important; } -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 435c131a01bacec52b51747ddcd91bf00fd131c8..1a7b13e38e989abf15758a600ad6ddcfcc18db10 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -194,7 +194,7 @@ class App extends Component { render() { const { - customStyle, customStyleUrl, micsLocked, openPanel, + customStyle, customStyleUrl, openPanel, } = this.props; return ( @@ -211,7 +211,7 @@ class App extends Component { </section> <PollingContainer /> <ModalContainer /> - {micsLocked ? null : <AudioContainer />} + <AudioContainer /> <ToastContainer /> <ChatAlertContainer /> {customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null} diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index a289c016da41c7b4223d0759631d5548842497b8..999b09ea160d0cd0f9ce65d3b9ccb3406de1d40f 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -116,7 +116,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) chatIsOpen: Session.equals('openPanel', 'chat'), openPanel: Session.get('openPanel'), userListIsOpen: !Session.equals('openPanel', ''), - micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic), }; })(AppContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index 11cf83edb83fdbec7114f39fc599dbf03d27d205..3c97857f9e30eba8d2de72eede71e7add3946469 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -44,12 +44,7 @@ export default withModalMounter(withTracker(({ mountModal }) => ({ glow: Service.isTalking() && !Service.isMuted(), handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(), handleJoinAudio: () => { - const meeting = Meetings.findOne({ meetingId: Auth.meetingID }); - const currentUser = Users.findOne({ userId: Auth.userID }); - const currentUserIsLocked = mapUser(currentUser).isLocked; - const micsLocked = (currentUserIsLocked && meeting.lockSettingsProp.disableMic); - - return micsLocked ? Service.joinListenOnly() : mountModal(<AudioModalContainer />); + return Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />); }, handleLeaveAudio: () => Service.exitAudio(), }))(AudioControlsContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index d06f46b3838985f73343da7e388d2769b10e0a59..5bd10c2965b2006f9eb136c71ae85c20fbd5ad7e 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -129,6 +129,7 @@ class AudioModal extends Component { joinFullAudioImmediately, joinFullAudioEchoTest, forceListenOnlyAttendee, + audioLocked, } = this.props; if (joinFullAudioImmediately) { @@ -139,7 +140,7 @@ class AudioModal extends Component { this.handleGoToEchoTest(); } - if (forceListenOnlyAttendee) { + if (forceListenOnlyAttendee || audioLocked) { this.handleJoinListenOnly(); } } @@ -266,9 +267,11 @@ class AudioModal extends Component { audioLocked, } = this.props; + const showMicrophone = forceListenOnlyAttendee || audioLocked; + return ( <span className={styles.audioOptions}> - {!forceListenOnlyAttendee + {!showMicrophone ? ( <Button className={styles.audioBtn} diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss index 85e292eaf0d7fa2385bef9a14a5ad40457965085..a0bb51b40dbf86e75bb556a147c70dc444fd1e9b 100644 --- a/bigbluebutton-html5/imports/ui/components/button/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss @@ -260,7 +260,7 @@ } .circle { - --btn-sm-padding-x: calc(var(--lg-padding-x) / 2.75); + --btn-sm-padding-x: calc(var(--sm-padding-x) / 2.75); --btn-md-padding-x: calc(var(--md-padding-x) / 2.75); --btn-lg-padding-x: calc(var(--lg-padding-x) / 2.75); --btn-jumbo-padding-x: calc(var(--jumbo-padding-x) / 2.75); diff --git a/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx index 1a5952544ea418c0b50287cd494e99dd0ec525ac..31d7219095ac83ed88a3ea5eea6dc8887b0f7066 100644 --- a/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx @@ -1,9 +1,17 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component'; import { styles } from './styles.scss'; -class ClosedCaptions extends Component { +const intlMessages = defineMessages({ + noLocaleSelected: { + id: 'app.submenu.closedCaptions.noLocaleSelected', + description: 'label for selected language for closed captions', + }, +}); + +class ClosedCaptions extends React.PureComponent { constructor(props) { super(props); @@ -25,13 +33,14 @@ class ClosedCaptions extends Component { } renderCaptions(caption) { + const { fontFamily, fontSize, fontColor } = this.props; const text = caption.captions; const captionStyles = { whiteSpace: 'pre-wrap', wordWrap: 'break-word', - fontFamily: this.props.fontFamily, - fontSize: this.props.fontSize, - color: this.props.fontColor, + fontFamily, + fontSize, + color: fontColor, }; return ( @@ -48,12 +57,15 @@ class ClosedCaptions extends Component { locale, captions, backgroundColor, + intl, } = this.props; return ( <div disabled className={styles.ccbox}> <div className={styles.title}> - <p> {locale} </p> + <p> + { locale || intl.formatMessage(intlMessages.noLocaleSelected) } + </p> </div> <div ref={(ref) => { this.refCCScrollArea = ref; }} @@ -69,7 +81,7 @@ class ClosedCaptions extends Component { } } -export default injectWbResizeEvent(ClosedCaptions); +export default injectIntl(injectWbResizeEvent(ClosedCaptions)); ClosedCaptions.propTypes = { backgroundColor: PropTypes.string.isRequired, @@ -83,12 +95,15 @@ ClosedCaptions.propTypes = { ).isRequired, }).isRequired, ).isRequired, - locale: PropTypes.string.isRequired, + locale: PropTypes.string, fontColor: PropTypes.string.isRequired, fontSize: PropTypes.string.isRequired, fontFamily: PropTypes.string.isRequired, + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, }; ClosedCaptions.defaultProps = { - locale: 'Locale is not selected', + locale: undefined, }; diff --git a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js index da2e0e10fc1d2a79b821cb80bd1aa5c927ce9f26..d2bd52338129dfa6f422f1ab8e09645e246a68a4 100644 --- a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js +++ b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js @@ -4,7 +4,7 @@ import Settings from '/imports/ui/services/settings'; import _ from 'lodash'; const getCCData = () => { - const meetingID = Auth.meetingID; + const { meetingID } = Auth; const ccSettings = Settings.cc; diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx index 261a55ebf214d1555272465f8a7e53a5323dc5a6..604dc502b6a3f9fae624fae8070a72216d46205f 100755 --- a/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx @@ -50,10 +50,6 @@ const intlMessages = defineMessages({ id: 'app.lock-viewers.PrivateChatLable', description: 'description for close button', }, - layoutLable: { - id: 'app.lock-viewers.Layout', - description: 'description for close button', - }, }); class LockViewersComponent extends React.PureComponent { @@ -197,28 +193,6 @@ class LockViewersComponent extends React.PureComponent { </div> </div> </div> - <div className={styles.row}> - <div className={styles.col} aria-hidden="true"> - <div className={styles.formElement}> - <div className={styles.label}> - {intl.formatMessage(intlMessages.layoutLable)} - </div> - </div> - </div> - <div className={styles.col}> - <div className={cx(styles.formElement, styles.pullContentRight)}> - <Toggle - icons={false} - defaultChecked={meeting.lockSettingsProp.lockedLayout} - onChange={() => { - meeting.lockSettingsProp.lockedLayout = !meeting.lockSettingsProp.lockedLayout; - toggleLockSettings(meeting); - }} - ariaLabel={intl.formatMessage(intlMessages.layoutLable)} - /> - </div> - </div> - </div> </div> </div> </Modal> diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx index ccf3135ba9d79227b73b1b20ddb4ab04e54d046c..451af4b68500f1a4892a662bfdd6cd91f56a65cb 100755 --- a/bigbluebutton-html5/imports/ui/components/media/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx @@ -131,7 +131,11 @@ export default withModalMounter(withTracker(() => { if (data.swapLayout) { data.floatingOverlay = true; - data.hideOverlay = hidePresentation; + data.hideOverlay = true; + } + + if (data.isScreensharing) { + data.floatingOverlay = false; } if (MediaService.shouldShowExternalVideo()) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index 889ba8f50d13bf20fe40ac0e8f0ffc3fb916c72a..1455624126947e13405c2f733f3aa0b967753cc3 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -10,6 +10,9 @@ import PresentationToolbarContainer from './presentation-toolbar/container'; import PresentationOverlayContainer from './presentation-overlay/container'; import Slide from './slide/component'; import { styles } from './styles.scss'; +import MediaService from '../media/service'; +import PresentationCloseButton from './presentation-close-button/component'; +import FullscreenButton from '../video-provider/fullscreen-button/component'; export default class PresentationArea extends Component { constructor() { @@ -171,9 +174,7 @@ export default class PresentationArea extends Component { // renders the whole presentation area renderPresentationArea() { - // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?) - if (!this.props.currentSlide - || !this.props.currentSlide.calculatedData) { + if (!this.isPresentationAccessible()) { return null; } // to control the size of the svg wrapper manually @@ -182,6 +183,9 @@ export default class PresentationArea extends Component { // a reference to the slide object const slideObj = this.props.currentSlide; + const presentationCloseButton = this.renderPresentationClose(); + const presentationFullscreenButton = this.renderPresentationFullscreen(); + // retrieving the pre-calculated data from the slide object const { x, @@ -193,8 +197,10 @@ export default class PresentationArea extends Component { imageUri, } = slideObj.calculatedData; const svgDimensions = this.state.fitToWidth ? { + position: 'absolute', width: 'inherit', } : { + position: 'absolute', width: adjustedSizes.width, height: adjustedSizes.height, }; @@ -202,6 +208,8 @@ export default class PresentationArea extends Component { <div style={svgDimensions} > + {presentationCloseButton} + {presentationFullscreenButton} <TransitionGroup> <CSSTransition key={slideObj.id} @@ -314,6 +322,31 @@ export default class PresentationArea extends Component { ); } + isPresentationAccessible() { + // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?) + return this.props.currentSlide && this.props.currentSlide.calculatedData; + }; + + isFullscreen() { + return document.fullscreenElement !== null; + } + + renderPresentationClose() { + if (!MediaService.shouldEnableSwapLayout() || this.isFullscreen()) { + return null; + } + return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />; + }; + + renderPresentationFullscreen() { + if (this.isFullscreen()) { + return null; + } + const full = () => this.refPresentationContainer.requestFullscreen(); + + return <FullscreenButton handleFullscreen={full} dark />; + } + renderPresentationToolbar() { if (!this.props.currentSlide) { return null; @@ -332,8 +365,7 @@ export default class PresentationArea extends Component { } renderWhiteboardToolbar() { - if (!this.props.currentSlide - || !this.props.currentSlide.calculatedData) { + if (!this.isPresentationAccessible()) { return null; } @@ -348,7 +380,9 @@ export default class PresentationArea extends Component { render() { return ( - <div className={styles.presentationContainer}> + <div + ref={(ref) => { this.refPresentationContainer = ref; }} + className={styles.presentationContainer}> <div ref={(ref) => { this.refPresentationArea = ref; }} className={styles.presentationArea} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..8ea9abbaad32f7e08e445d9f2a14f5e7d38fe785 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import { styles } from './styles'; + +const intlMessages = defineMessages({ + closePresentationLabel: { + id: 'app.presentation.close', + description: 'Close presentation label', + }, +}); + +const ClosePresentationComponent = ({ intl, toggleSwapLayout }) => ( + <Button + role="button" + aria-labelledby="closeLabel" + aria-describedby="closeDesc" + color="primary" + icon="close" + size="sm" + onClick={toggleSwapLayout} + label={intl.formatMessage(intlMessages.closePresentationLabel)} + hideLabel + className={styles.button} + /> +); + +export default injectIntl(ClosePresentationComponent); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c3384c4cd6fd8ddf103a4a3068f6a820f2779e3 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss @@ -0,0 +1,7 @@ + +.button { + position: absolute; + top: 0; + right: 0; + cursor: pointer; +} diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 906e66c6aa1cd91f31516ad6b00f9f676b488239..6eb1a2c6d30e2dc3970b5ac46273f48267cd9567 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -1,5 +1,5 @@ import React from 'react'; - +import FullscreenButton from '../video-provider/fullscreen-button/component'; import { styles } from './styles'; export default class ScreenshareComponent extends React.Component { @@ -11,26 +11,57 @@ export default class ScreenshareComponent extends React.Component { this.onVideoLoad = this.onVideoLoad.bind(this); } + componentDidMount() { this.props.presenterScreenshareHasStarted(); } + componentWillReceiveProps(nextProps) { if (this.props.isPresenter && !nextProps.isPresenter) { this.props.unshareScreen(); } } + componentWillUnmount() { this.props.presenterScreenshareHasEnded(); this.props.unshareScreen(); } + onVideoLoad() { this.setState({ loaded: true }); } + renderFullscreenButton() { + const full = () => { + if (!this.videoTag) { + return; + } + + this.videoTag.requestFullscreen(); + }; + return <FullscreenButton handleFullscreen={full} />; + } + render() { + const style = { + right: 0, + bottom: 0, + }; + return ( - [!this.state.loaded ? (<div key="screenshareArea" className={styles.connecting} />) : null, - (<video key="screenshareVideo" id="screenshareVideo" style={{ maxHeight: '100%', width: '100%' }} autoPlay playsInline onLoadedData={this.onVideoLoad} />)] + [!this.state.loaded ? (<div key="screenshareArea" innerStyle={style} className={styles.connecting} />) : null, + this.renderFullscreenButton(), + ( + <video + key="screenshareVideo" + id="screenshareVideo" + style={{ maxHeight: '100%', width: '100%' }} + autoPlay + playsInline + onLoadedData={this.onVideoLoad} + ref={(ref) => { this.videoTag = ref; }} + /> + )] ); } } diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index fd0dc75ec88c25c000e0c89c15c9cf126c798c44..8aeda4d68d75b7585baddd425c8fa4f5fe95913e 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -148,13 +148,17 @@ class Settings extends Component { } renderModalContent() { - const { intl } = this.props; + const { + intl, + locales, + } = this.props; + const { selectedTab, availableLocales, current, - locales, } = this.state; + return ( <Tabs className={styles.tabs} diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx index 12f4f34be11a35682c57dba80f741c3160fb8a82..c227d044f4d17b59a3d5267416914e1e0f470542 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx @@ -198,15 +198,15 @@ class ClosedCaptionsMenu extends BaseMenu { <select defaultValue={locales ? locales.indexOf(this.state.settings.locale) : -1} className={styles.select} - onChange={this.handleSelectChange.bind(this, 'locale', this.props.locales)} + onChange={this.handleSelectChange.bind(this, 'locale', locales)} > <option> - { this.props.locales - && this.props.locales.length + { locales + && locales.length ? intl.formatMessage(intlMessages.localeOptionLabel) : intl.formatMessage(intlMessages.noLocaleOptionLabel) } </option> - {this.props.locales ? this.props.locales.map((locale, index) => ( + {locales ? locales.map((locale, index) => ( <option key={index} value={index}> {locale} </option> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index 1a00e4a275eac3378a64129e9e994821ad69c6df..04e126d05a577801d78b8de110645ab982624b0a 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -31,6 +31,7 @@ const propTypes = { roving: PropTypes.func.isRequired, getGroupChatPrivate: PropTypes.func.isRequired, showBranding: PropTypes.bool.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; const defaultProps = { @@ -67,6 +68,7 @@ class UserList extends PureComponent { hasBreakoutRoom, getUsersId, hasPrivateChatBetweenUsers, + toggleUserLock, } = this.props; return ( @@ -103,6 +105,7 @@ class UserList extends PureComponent { hasBreakoutRoom, getUsersId, hasPrivateChatBetweenUsers, + toggleUserLock, } } />} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx index c2cf35d53fa9712170fdfec55dda34d682e3624d..8fcc687aaed1d69a9e0d8a30d1b6ed3b98848559 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx @@ -24,6 +24,7 @@ const propTypes = { changeRole: PropTypes.func.isRequired, roving: PropTypes.func.isRequired, getGroupChatPrivate: PropTypes.func.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; const UserListContainer = props => <UserList {...props} />; @@ -56,4 +57,5 @@ export default withTracker(({ chatID, compact }) => ({ getEmoji: Service.getEmoji(), showBranding: getFromUserSettings('displayBrandingArea', Meteor.settings.public.app.branding.displayBrandingArea), hasPrivateChatBetweenUsers: Service.hasPrivateChatBetweenUsers, + toggleUserLock: Service.toggleUserLock, }))(UserListContainer); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 17fde8ed15e66d4956ca2a526b4c5aeb372aed30..487997f5a3ecab798ccfbbd54983b2210554b603 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -260,6 +260,24 @@ const getActiveChats = (chatID) => { const isVoiceOnlyUser = userId => userId.toString().startsWith('v_'); +const isMeetingLocked = (id) => { + const meeting = Meetings.findOne({ meetingId: id }); + let isLocked = false; + + if (meeting.lockSettingsProp !== undefined) { + const lockSettings = meeting.lockSettingsProp; + + if (lockSettings.disableCam + || lockSettings.disableMic + || lockSettings.disablePrivChat + || lockSettings.disablePubChat) { + isLocked = true; + } + } + + return isLocked; +}; + const getAvailableActions = (currentUser, user, isBreakoutRoom) => { const isDialInUser = isVoiceOnlyUser(user.id) || user.isPhoneUser; @@ -301,6 +319,9 @@ const getAvailableActions = (currentUser, user, isBreakoutRoom) => { const allowedToChangeStatus = user.isCurrent; + const allowedToChangeUserLockStatus = currentUser.isModerator + && !user.isModerator && isMeetingLocked(Auth.meetingID); + return { allowedToChatPrivately, allowedToMuteAudio, @@ -311,6 +332,7 @@ const getAvailableActions = (currentUser, user, isBreakoutRoom) => { allowedToPromote, allowedToDemote, allowedToChangeStatus, + allowedToChangeUserLockStatus, }; }; @@ -325,24 +347,6 @@ const normalizeEmojiName = emoji => ( emoji in EMOJI_STATUSES ? EMOJI_STATUSES[emoji] : emoji ); -const isMeetingLocked = (id) => { - const meeting = Meetings.findOne({ meetingId: id }); - let isLocked = false; - - if (meeting.lockSettingsProp !== undefined) { - const lockSettings = meeting.lockSettingsProp; - - if (lockSettings.disableCam - || lockSettings.disableMic - || lockSettings.disablePrivChat - || lockSettings.disablePubChat) { - isLocked = true; - } - } - - return isLocked; -}; - const setEmojiStatus = (data) => { const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(data)); @@ -448,6 +452,10 @@ const isUserModerator = (userId) => { return u ? u.moderator : false; }; +const toggleUserLock = (userId, lockStatus) => { + makeCall('toggleUserLock', userId, lockStatus); +}; + export default { setEmojiStatus, assignPresenter, @@ -473,4 +481,5 @@ export default { getEmojiList: () => EMOJI_STATUSES, getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, hasPrivateChatBetweenUsers, + toggleUserLock, }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx index 9a12a071955681f3e6fd014c00c9c1f00a0dff32..2332e2d8258be41b126cef2ff8c77d8acb1046f0 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx @@ -32,6 +32,7 @@ const propTypes = { getUsersId: PropTypes.func.isRequired, pollIsOpen: PropTypes.bool.isRequired, forcePollOpen: PropTypes.bool.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; const defaultProps = { @@ -68,6 +69,7 @@ class UserContent extends PureComponent { hasBreakoutRoom, getUsersId, hasPrivateChatBetweenUsers, + toggleUserLock, } = this.props; return ( @@ -121,6 +123,7 @@ class UserContent extends PureComponent { getGroupChatPrivate, getUsersId, hasPrivateChatBetweenUsers, + toggleUserLock, }} /> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index aa2276dad51ae0a7db4386af9e8dbc2758ee5f91..d77c8e349feffb8541da330ff6b8d739019b850b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -31,6 +31,7 @@ const propTypes = { normalizeEmojiName: PropTypes.func.isRequired, isMeetingLocked: PropTypes.func.isRequired, roving: PropTypes.func.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; const defaultProps = { @@ -126,6 +127,7 @@ class UserParticipants extends Component { getEmoji, users, hasPrivateChatBetweenUsers, + toggleUserLock, } = this.props; let index = -1; @@ -161,6 +163,7 @@ class UserParticipants extends Component { changeRole, getGroupChatPrivate, hasPrivateChatBetweenUsers, + toggleUserLock, }} userId={u} getScrollContainerRef={this.getScrollContainerRef} @@ -200,20 +203,24 @@ class UserParticipants extends Component { <div className={styles.container}> <h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.usersTitle)} - ( + ( {users.length} -) - + ) </h2> - <UserOptionsContainer {...{ - users, - muteAllUsers, - muteAllExceptPresenter, - setEmojiStatus, - meeting, - currentUser, - }} - /> + {currentUser.isModerator + ? ( + <UserOptionsContainer {...{ + users, + muteAllUsers, + muteAllExceptPresenter, + setEmojiStatus, + meeting, + currentUser, + }} + /> + ) : null + } + </div> ) : <hr className={styles.separator} /> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx index b5ef848a0d23e708ebcd76bfe65add95a2980a35..2ccb85ca97353c3b63f2e1927dcd6a5f85f8760c 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx @@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data'; import Meetings from '/imports/api/meetings'; import UserParticipants from './component'; -const UserParticipantsContainer = ({ ...props }) => <UserParticipants {...props} />; +const UserParticipantsContainer = props => <UserParticipants {...props} />; export default withTracker(({ getUsersId }) => ({ meeting: Meetings.findOne({}), diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx index 701d0d6168e5e6409c8fc5cb154681f716ee1e02..2ffad3e01ade1d39633325289cbf0ba68cdc5c5d 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx @@ -18,6 +18,7 @@ const propTypes = { isMeetingLocked: PropTypes.func.isRequired, normalizeEmojiName: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; const defaultProps = { @@ -47,6 +48,7 @@ class UserListItem extends PureComponent { setEmojiStatus, toggleVoice, hasPrivateChatBetweenUsers, + toggleUserLock, } = this.props; const { meetingId, lockSettingsProp } = meeting; @@ -75,6 +77,7 @@ class UserListItem extends PureComponent { toggleVoice, user, hasPrivateChatBetweenUsers, + toggleUserLock, }} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index c5f5422c631728f5aa5b0506ec8bee3e41869168..7ab54c5fa60c0c3eacdd080ca6a2bac95b7918c0 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -81,6 +81,14 @@ const messages = defineMessages({ id: 'app.userList.menu.demoteUser.label', description: 'Forcefully demote this moderator to a viewer', }, + UnlockUserLabel: { + id: 'app.userList.menu.unlockUser.label', + description: 'Unlock individual user', + }, + LockUserLabel: { + id: 'app.userList.menu.lockUser.label', + description: 'Lock a unlocked user', + }, }); const propTypes = { @@ -92,6 +100,7 @@ const propTypes = { normalizeEmojiName: PropTypes.func.isRequired, isMeetingLocked: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired, + toggleUserLock: PropTypes.func.isRequired, }; class UserDropdown extends PureComponent { @@ -196,6 +205,7 @@ class UserDropdown extends PureComponent { changeRole, lockSettingsProp, hasPrivateChatBetweenUsers, + toggleUserLock, } = this.props; const { showNestedOptions } = this.state; @@ -213,6 +223,7 @@ class UserDropdown extends PureComponent { allowedToPromote, allowedToDemote, allowedToChangeStatus, + allowedToChangeUserLockStatus, } = actionPermissions; const { disablePrivChat } = lockSettingsProp; @@ -331,6 +342,16 @@ class UserDropdown extends PureComponent { )); } + if (allowedToChangeUserLockStatus) { + actions.push(this.makeDropdownItem( + 'unlockUser', + user.isLocked ? intl.formatMessage(messages.UnlockUserLabel, { 0: user.name }) + : intl.formatMessage(messages.LockUserLabel, { 0: user.name }), + () => this.onActionsHide(toggleUserLock(user.id, !user.isLocked)), + user.isLocked ? 'unlock' : 'lock', + )); + } + return actions; } @@ -438,7 +459,7 @@ class UserDropdown extends PureComponent { voice={user.isVoiceUser} color={user.color} > - {isVoiceOnly ? iconVoiceOnlyUser : iconUser } + {isVoiceOnly ? iconVoiceOnlyUser : iconUser} </UserAvatar> ); } @@ -491,7 +512,7 @@ class UserDropdown extends PureComponent { > <div className={styles.userItemContents}> <div className={styles.userAvatar}> - { this.renderUserAvatar() } + {this.renderUserAvatar()} </div> {<UserName {...{ diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index 0db0a225fdb68c2733af006b26b875f7e7561395..26229f2cf00ec0988b265413ceea44b0c2c19cad 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -10,6 +10,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import LockViewersContainer from '/imports/ui/components/lock-viewers/container'; +import BreakoutRoom from '/imports/ui/components/actions-bar/create-breakout-room/component'; import { styles } from './styles'; const propTypes = { @@ -21,6 +22,11 @@ const propTypes = { toggleMuteAllUsersExceptPresenter: PropTypes.func.isRequired, toggleStatus: PropTypes.func.isRequired, mountModal: PropTypes.func.isRequired, + users: PropTypes.arrayOf(Object).isRequired, + meetingName: PropTypes.string.isRequired, + createBreakoutRoom: PropTypes.func.isRequired, + meetingIsBreakout: PropTypes.bool.isRequired, + hasBreakoutRoom: PropTypes.bool.isRequired, }; const intlMessages = defineMessages({ @@ -68,6 +74,18 @@ const intlMessages = defineMessages({ id: 'app.userList.userOptions.muteAllExceptPresenterDesc', description: 'Mute all except presenter description', }, + createBreakoutRoom: { + id: 'app.actionsBar.actionsDropdown.createBreakoutRoom', + description: 'Create breakout room option', + }, + createBreakoutRoomDesc: { + id: 'app.actionsBar.actionsDropdown.createBreakoutRoomDesc', + description: 'Description of create breakout room option', + }, + invitationItem: { + id: 'app.invitation.title', + description: 'invitation to breakout title', + }, }); class UserOptions extends PureComponent { @@ -78,55 +96,19 @@ class UserOptions extends PureComponent { isUserOptionsOpen: false, }; + this.clearStatusId = _.uniqueId('list-item-'); + this.muteId = _.uniqueId('list-item-'); + this.muteAllId = _.uniqueId('list-item-'); + this.lockId = _.uniqueId('list-item-'); + this.createBreakoutId = _.uniqueId('list-item-'); + this.onActionsShow = this.onActionsShow.bind(this); this.onActionsHide = this.onActionsHide.bind(this); this.alterMenu = this.alterMenu.bind(this); - } - - componentWillMount() { - const { - intl, - isMeetingMuted, - mountModal, - toggleStatus, - toggleMuteAllUsers, - toggleMuteAllUsersExceptPresenter, - } = this.props; - - this.menuItems = _.compact([ - (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="clear_status" - label={intl.formatMessage(intlMessages.clearAllLabel)} - description={intl.formatMessage(intlMessages.clearAllDesc)} - onClick={toggleStatus} - />), - (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllLabel)} - description={intl.formatMessage(intlMessages.muteAllDesc)} - onClick={toggleMuteAllUsers} - />), - (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} - description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} - onClick={toggleMuteAllUsersExceptPresenter} - />), - (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="lock" - label={intl.formatMessage(intlMessages.lockViewersLabel)} - description={intl.formatMessage(intlMessages.lockViewersDesc)} - onClick={() => mountModal(<LockViewersContainer />)} - />), - ]); - - if (isMeetingMuted) { - this.alterMenu(); - } + this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this); + this.onCreateBreakouts = this.onCreateBreakouts.bind(this); + this.onInvitationUsers = this.onInvitationUsers.bind(this); + this.renderMenuItems = this.renderMenuItems.bind(this); } componentDidUpdate(prevProps) { @@ -148,6 +130,40 @@ class UserOptions extends PureComponent { }); } + onCreateBreakouts() { + return this.handleCreateBreakoutRoomClick(false); + } + + onInvitationUsers() { + return this.handleCreateBreakoutRoomClick(true); + } + + handleCreateBreakoutRoomClick(isInvitation) { + const { + createBreakoutRoom, + mountModal, + meetingName, + users, + getUsersNotAssigned, + getBreakouts, + sendInvitation, + } = this.props; + + return mountModal( + <BreakoutRoom + {...{ + createBreakoutRoom, + meetingName, + users, + getUsersNotAssigned, + isInvitation, + getBreakouts, + sendInvitation, + }} + />, + ); + } + alterMenu() { const { intl, @@ -186,9 +202,92 @@ class UserOptions extends PureComponent { } } + renderMenuItems() { + const { + intl, + isMeetingMuted, + mountModal, + toggleStatus, + toggleMuteAllUsers, + toggleMuteAllUsersExceptPresenter, + meetingIsBreakout, + hasBreakoutRoom, + getUsersNotAssigned, + isUserModerator, + users, + } = this.props; + + const canCreateBreakout = isUserModerator + && !meetingIsBreakout + && !hasBreakoutRoom; + + const canInviteUsers = isUserModerator + && !meetingIsBreakout + && hasBreakoutRoom + && getUsersNotAssigned(users).length; + + this.menuItems = _.compact([ + (<DropdownListItem + key={this.clearStatusId} + icon="clear_status" + label={intl.formatMessage(intlMessages.clearAllLabel)} + description={intl.formatMessage(intlMessages.clearAllDesc)} + onClick={toggleStatus} + />), + (<DropdownListItem + key={this.muteAllId} + icon="mute" + label={intl.formatMessage(intlMessages.muteAllLabel)} + description={intl.formatMessage(intlMessages.muteAllDesc)} + onClick={toggleMuteAllUsers} + />), + (<DropdownListItem + key={this.muteId} + icon="mute" + label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} + description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} + onClick={toggleMuteAllUsersExceptPresenter} + />), + (<DropdownListItem + key={this.lockId} + icon="lock" + label={intl.formatMessage(intlMessages.lockViewersLabel)} + description={intl.formatMessage(intlMessages.lockViewersDesc)} + onClick={() => mountModal(<LockViewersContainer />)} + />), + (canCreateBreakout + ? ( + <DropdownListItem + key={this.createBreakoutId} + icon="rooms" + label={intl.formatMessage(intlMessages.createBreakoutRoom)} + description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)} + onClick={this.onCreateBreakouts} + /> + ) : null + ), + (canInviteUsers + ? ( + <DropdownListItem + icon="rooms" + label={intl.formatMessage(intlMessages.invitationItem)} + key={this.createBreakoutId} + onClick={this.onInvitationUsers} + /> + ) + : null), + ]); + + if (isMeetingMuted) { + this.alterMenu(); + } + + return this.menuItems; + } + render() { - const { intl } = this.props; const { isUserOptionsOpen } = this.state; + const { intl } = this.props; return ( <Dropdown @@ -217,7 +316,7 @@ class UserOptions extends PureComponent { > <DropdownList> { - this.menuItems + this.renderMenuItems() } </DropdownList> </DropdownContent> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx index 24d71cfeff48042756b4a29637520d2aa5f2f5af..472655c338b7fe12a5f9dcabea9e718312d4c06a 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx @@ -1,67 +1,47 @@ -import React, { PureComponent } from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; import PropTypes from 'prop-types'; import Auth from '/imports/ui/services/auth'; +import Service from '/imports/ui/components/actions-bar/service'; import UserOptions from './component'; - const propTypes = { users: PropTypes.arrayOf(Object).isRequired, muteAllUsers: PropTypes.func.isRequired, muteAllExceptPresenter: PropTypes.func.isRequired, setEmojiStatus: PropTypes.func.isRequired, meeting: PropTypes.shape({}).isRequired, + currentUser: PropTypes.shape({ + isModerator: PropTypes.bool.isRequired, + }).isRequired, }; -export default class UserOptionsContainer extends PureComponent { - constructor(props) { - super(props); - - const { meeting } = this.props; - - this.state = { - meetingMuted: meeting.voiceProp.muteOnStart, - }; - - this.muteMeeting = this.muteMeeting.bind(this); - this.muteAllUsersExceptPresenter = this.muteAllUsersExceptPresenter.bind(this); - this.handleClearStatus = this.handleClearStatus.bind(this); - } - - muteMeeting() { - const { muteAllUsers } = this.props; - muteAllUsers(Auth.userID); - } - - muteAllUsersExceptPresenter() { - const { muteAllExceptPresenter } = this.props; - muteAllExceptPresenter(Auth.userID); - } - - handleClearStatus() { - const { users, setEmojiStatus } = this.props; - - users.forEach((id) => { - setEmojiStatus(id, 'none'); - }); - } - - render() { - const { currentUser } = this.props; - const currentUserIsModerator = currentUser.isModerator; - - const { meetingMuted } = this.state; - - return ( - currentUserIsModerator - ? ( - <UserOptions - toggleMuteAllUsers={this.muteMeeting} - toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter} - toggleStatus={this.handleClearStatus} - isMeetingMuted={meetingMuted} - />) : null - ); - } -} +const UserOptionsContainer = withTracker((props) => { + const { + meeting, + users, + setEmojiStatus, + muteAllExceptPresenter, + muteAllUsers, + } = props; + + return { + toggleMuteAllUsers: () => muteAllUsers(Auth.userID), + toggleMuteAllUsersExceptPresenter: () => muteAllExceptPresenter(Auth.userID), + toggleStatus: () => users.forEach(id => setEmojiStatus(id, 'none')), + isMeetingMuted: meeting.voiceProp.muteOnStart, + isUserPresenter: Service.isUserPresenter(), + isUserModerator: Service.isUserModerator(), + createBreakoutRoom: Service.createBreakoutRoom, + meetingIsBreakout: Service.meetingIsBreakout(), + hasBreakoutRoom: Service.hasBreakoutRoom(), + meetingName: Service.meetingName(), + users: Service.users(), + getBreakouts: Service.getBreakouts, + sendInvitation: Service.sendInvitation, + getUsersNotAssigned: Service.getUsersNotAssigned, + }; +})(UserOptions); UserOptionsContainer.propTypes = propTypes; + +export default UserOptionsContainer; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..2305f3bf0538a2fc3b46cb16072567a3c562f57a --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import cx from 'classnames'; +import { styles } from './styles'; + +const intlMessages = defineMessages({ + fullscreenButton: { + id: 'app.fullscreenButton.label', + description: 'Fullscreen label', + }, +}); + +const FullscreenButtonComponent = ({ intl, handleFullscreen, dark }) => ( + <div className={cx(styles.wrapper, dark ? styles.dark : null)}> + <Button + role="button" + aria-labelledby="fullscreenButtonLabel" + aria-describedby="fullscreenButtonDesc" + color="default" + icon="fullscreen" + size="sm" + onClick={handleFullscreen} + label={intl.formatMessage(intlMessages.fullscreenButton)} + hideLabel + circle + className={styles.button} + /> + </div> +); + +export default injectIntl(FullscreenButtonComponent); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..644ed81a284e53a221634581d189ff504ea03ba7 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss @@ -0,0 +1,34 @@ +:root { + --color-transparent: #ff000000; + ::-webkit-media-controls { + display:none !important; + } +} + +.wrapper { + position: absolute; + right: 0; + background-color: var(--color-transparent); + cursor: pointer; + border: none !important; +} + +.button { + span, span:active, span:hover { + background-color: var(--color-transparent) !important; + border: none !important; + i { + border: none !important; + background-color: var(--color-transparent) !important; + font-weight: bold !important; + } + } +} + +.dark { + bottom: 0; +} + +.dark .button span i { + color: var(--color-black); +} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..f56bad50e5c61bf47c03b3a85c223325d2be8a45 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import cx from 'classnames'; +import Button from '/imports/ui/components/button/component'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; +import { styles } from './styles'; + +const intlMessages = defineMessages({ + joinVideo: { + id: 'app.video.joinVideo', + description: 'Join video button label', + }, + leaveVideo: { + id: 'app.video.leaveVideo', + description: 'Leave video button label', + }, + videoButtonDesc: { + id: 'app.video.videoButtonDesc', + description: 'video button description', + }, + videoDisabled: { + id: 'app.video.videoDisabled', + description: 'video disabled label', + }, +}); + + +const propTypes = { + intl: intlShape.isRequired, + isSharingVideo: PropTypes.bool.isRequired, +}; + +const JoinVideoButton = ({ + intl, + isSharingVideo, + isDisabled, + handleJoinVideo, + handleCloseVideo, +}) => { + + return ( + <Button + label={isDisabled ? + intl.formatMessage(intlMessages.videoDisabled) + : + (isSharingVideo ? + intl.formatMessage(intlMessages.leaveVideo) + : + intl.formatMessage(intlMessages.joinVideo) + ) + } + className={cx(styles.button, isSharingVideo || styles.ghostButton)} + onClick={isSharingVideo ? handleCloseVideo : handleJoinVideo} + hideLabel + aria-label={intl.formatMessage(intlMessages.videoButtonDesc)} + color={isSharingVideo ? 'primary' : 'default'} + icon={isSharingVideo ? 'video' : 'video_off'} + ghost={!isSharingVideo} + size="lg" + circle + disabled={isDisabled} + /> + ); +}; +JoinVideoButton.propTypes = propTypes; +export default injectIntl(JoinVideoButton); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx new file mode 100755 index 0000000000000000000000000000000000000000..b21d680e1f9c8995e529ffae76a5871180d959cc --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import { injectIntl } from 'react-intl'; +import { withModalMounter } from '/imports/ui/components/modal/service'; +import VideoPreviewContainer from '/imports/ui/components/video-preview/container'; +import JoinVideoButton from './component'; +import VideoButtonService from './service'; + +const JoinVideoOptionsContainer = (props) => { + const { + isSharingVideo, + isDisabled, + handleJoinVideo, + handleCloseVideo, + baseName, + intl, + mountModal, + ...restProps + } = props; + + const mountPreview = () => { mountModal(<VideoPreviewContainer />); }; + + return <JoinVideoButton {...{ handleJoinVideo: mountPreview, handleCloseVideo, isSharingVideo, isDisabled, ...restProps }} />; +}; + +export default withModalMounter(injectIntl(withTracker(() => ({ + baseName: VideoButtonService.baseName, + isSharingVideo: VideoButtonService.isSharingVideo(), + isDisabled: VideoButtonService.isDisabled(), + videoShareAllowed: VideoButtonService.videoShareAllowed(), +}))(JoinVideoOptionsContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js similarity index 86% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js rename to bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js index f17126feb08859f34f89af554897e86bc721addb..a25e62ccf4197c2ced022eba5c9212b2b2e0a855 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js @@ -2,7 +2,6 @@ import Settings from '/imports/ui/services/settings'; import mapUser from '/imports/ui/services/user/mapUser'; import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users/'; -import MediaService from '/imports/ui/components/media/service'; import VideoService from '../service'; const baseName = Meteor.settings.public.app.basename; @@ -37,7 +36,5 @@ export default { isSharingVideo, isDisabled, baseName, - toggleSwapLayout: MediaService.toggleSwapLayout, - swapLayoutAllowed: MediaService.shouldEnableSwapLayout, videoShareAllowed, }; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/styles.scss similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-menu/styles.scss rename to bigbluebutton-html5/imports/ui/components/video-provider/video-button/styles.scss diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index 92c434dc45388958c4ea20a21c0b647e2d24b300..497a95c1b88756b1294cc1643fc70e96602f5f66 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -11,6 +11,7 @@ import DropdownListItem from '/imports/ui/components/dropdown/list/item/componen import Icon from '/imports/ui/components/icon/component'; import Button from '/imports/ui/components/button/component'; import VideoListItemStats from './video-list-item-stats/component'; +import FullscreenButton from '../../fullscreen-button/component'; import { styles } from '../styles'; const intlMessages = defineMessages({ @@ -96,6 +97,13 @@ class VideoListItem extends Component { ]); } + renderFullscreenButton() { + const full = () => { + this.videoTag.requestFullscreen(); + }; + return <FullscreenButton handleFullscreen={full} />; + } + render() { const { showStats, stats } = this.state; const { user } = this.props; @@ -131,6 +139,7 @@ class VideoListItem extends Component { { user.isListenOnly ? <Icon className={styles.voice} iconName="listen" /> : null } </div> { showStats ? <VideoListItemStats toggleStats={this.toggleStats} stats={stats} /> : null } + { this.renderFullscreenButton() } </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx deleted file mode 100755 index 88ed1f14a20b9de2aa2fec811ccc76b07f1eaa65..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import _ from 'lodash'; -import cx from 'classnames'; -import Button from '/imports/ui/components/button/component'; -import { defineMessages, injectIntl, intlShape } from 'react-intl'; -import Dropdown from '/imports/ui/components/dropdown/component'; -import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; -import DropdownContent from '/imports/ui/components/dropdown/content/component'; -import DropdownList from '/imports/ui/components/dropdown/list/component'; -import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; -import { styles } from './styles'; - -const intlMessages = defineMessages({ - videoMenu: { - id: 'app.video.videoMenu', - description: 'video menu label', - }, - videoMenuDesc: { - id: 'app.video.videoMenuDesc', - description: 'video menu description', - }, - videoMenuDisabled: { - id: 'app.video.videoMenuDisabled', - description: 'video menu label', - }, -}); - - -const propTypes = { - intl: intlShape.isRequired, - isSharingVideo: PropTypes.bool.isRequired, - videoItems: PropTypes.arrayOf(PropTypes.object).isRequired, -}; - -const JoinVideoOptions = ({ - intl, - isSharingVideo, - videoItems, - videoShareAllowed, -}) => { - const menuItems = videoItems - .filter(item => !item.disabled) - .map(item => - ( - <DropdownListItem - key={_.uniqueId('video-menu-')} - className={styles.item} - description={item.description} - onClick={item.click} - tabIndex={-1} - id={item.id} - > - <img src={item.iconPath} className={styles.imageSize} alt="video menu icon" /> - <span className={styles.label}>{item.label}</span> - </DropdownListItem> - )); - return ( - <Dropdown - autoFocus - > - <DropdownTrigger tabIndex={0}> - <Button - label={!videoShareAllowed ? - intl.formatMessage(intlMessages.videoMenuDisabled) - : intl.formatMessage(intlMessages.videoMenu) - } - className={cx(styles.button, isSharingVideo || styles.ghostButton)} - onClick={() => null} - hideLabel - aria-label={intl.formatMessage(intlMessages.videoMenuDesc)} - color={isSharingVideo ? 'primary' : 'default'} - icon={isSharingVideo ? 'video' : 'video_off'} - ghost={!isSharingVideo} - size="lg" - circle - disabled={!videoShareAllowed} - /> - </DropdownTrigger> - <DropdownContent placement="top" > - <DropdownList horizontal> - {menuItems} - </DropdownList> - </DropdownContent> - </Dropdown> - ); -}; -JoinVideoOptions.propTypes = propTypes; -export default injectIntl(JoinVideoOptions); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx deleted file mode 100755 index 8e453a95ec9d0916cfb65b827475d652a04ca48c..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { withTracker } from 'meteor/react-meteor-data'; -import { defineMessages, injectIntl } from 'react-intl'; -import { withModalMounter } from '/imports/ui/components/modal/service'; -import VideoPreviewContainer from '/imports/ui/components/video-preview/container'; -import JoinVideoOptions from './component'; -import VideoMenuService from './service'; - -const intlMessages = defineMessages({ - joinVideo: { - id: 'app.video.joinVideo', - description: 'Join video button label', - }, - leaveVideo: { - id: 'app.video.leaveVideo', - description: 'Leave video button label', - }, - swapCam: { - id: 'app.video.swapCam', - description: 'Swap cam button label', - }, - swapCamDesc: { - id: 'app.video.swapCamDesc', - description: 'Swap cam button description', - }, -}); - -const JoinVideoOptionsContainer = (props) => { - const { - isSharingVideo, - isDisabled, - handleJoinVideo, - handleCloseVideo, - toggleSwapLayout, - swapLayoutAllowed, - baseName, - intl, - mountModal, - ...restProps - } = props; - const videoItems = [ - { - iconPath: `${baseName}/resources/images/video-menu/icon-swap.svg`, - description: intl.formatMessage(intlMessages.swapCamDesc), - label: intl.formatMessage(intlMessages.swapCam), - disabled: !swapLayoutAllowed, - click: toggleSwapLayout, - id: 'swap-button', - }, - { - iconPath: `${baseName}/resources/images/video-menu/icon-webcam-off.svg`, - description: intl.formatMessage(intlMessages[isSharingVideo ? 'leaveVideo' : 'joinVideo']), - label: intl.formatMessage(intlMessages[isSharingVideo ? 'leaveVideo' : 'joinVideo']), - disabled: isDisabled && !isSharingVideo, - click: isSharingVideo ? handleCloseVideo : () => { mountModal(<VideoPreviewContainer />); }, - id: isSharingVideo ? 'leave-video-button' : 'join-video-button', - }, - ]; - - return <JoinVideoOptions {...{ videoItems, isSharingVideo, ...restProps }} />; -}; - -export default withModalMounter(injectIntl(withTracker(() => ({ - baseName: VideoMenuService.baseName, - isSharingVideo: VideoMenuService.isSharingVideo(), - isDisabled: VideoMenuService.isDisabled(), - videoShareAllowed: VideoMenuService.videoShareAllowed(), - toggleSwapLayout: VideoMenuService.toggleSwapLayout, - swapLayoutAllowed: VideoMenuService.swapLayoutAllowed(), -}))(JoinVideoOptionsContainer))); diff --git a/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js b/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js index 7a5a550b3105c162ea59cfb6f6ada89ae0a1c610..f4f8cc0eac8ab93e34037163ec3062b86f91b96c 100644 --- a/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js +++ b/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js @@ -4,7 +4,7 @@ const iosWebviewAudioPolyfills = function () { window.RTCPeerConnection.prototype.getRemoteStreams = function () { return this._remoteStreams ? this._remoteStreams : []; }; - } + } if (!('onaddstream' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { get: function get() { diff --git a/bigbluebutton-html5/imports/utils/safari-webrtc.js b/bigbluebutton-html5/imports/utils/safari-webrtc.js index 524ceb737bacf40d57f0ab7c945d7ed3be7ffe48..e7dcb725584bf2824ee6cfd61e43b317f2b81977 100644 --- a/bigbluebutton-html5/imports/utils/safari-webrtc.js +++ b/bigbluebutton-html5/imports/utils/safari-webrtc.js @@ -37,7 +37,7 @@ export function canGenerateIceCandidates() { resolve(); return; } - + getIceServersList().catch((e) => { reject(); }).then((iceServersReceived) => { @@ -53,16 +53,16 @@ export function canGenerateIceCandidates() { Session.set('canGenerateIceCandidates', true); resolve(); } - } + }; pc.onicegatheringstatechange = function (e) { if (e.currentTarget.iceGatheringState == 'complete' && countIceCandidates == 0) reject(); - } + }; - setTimeout(function () { + setTimeout(() => { pc.close(); if (!countIceCandidates) reject(); - }, 5000); + }, 5000); const p = pc.createOffer({ offerToReceiveVideo: true }); p.then((answer) => { pc.setLocalDescription(answer); }); @@ -75,7 +75,7 @@ export function tryGenerateIceCandidates() { canGenerateIceCandidates().then((ok) => { resolve(); }).catch((e) => { - navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function (stream) { + navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => { canGenerateIceCandidates().then((ok) => { resolve(); }).catch((e) => { @@ -86,4 +86,4 @@ export function tryGenerateIceCandidates() { }); }); }); -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json index ba68eac7d1a9682f9256a7642f5e619567320e68..5e074a700f48436718f61ce12aaefcab0f6dd6e2 100644 --- a/bigbluebutton-html5/private/locales/de.json +++ b/bigbluebutton-html5/private/locales/de.json @@ -18,9 +18,13 @@ "app.chat.label": "Chat", "app.chat.emptyLogLabel": "Chat-Log ist leer", "app.chat.clearPublicChatMessage": "Der öffentliche Chatverlauf wurde durch einen Moderator gelöscht", + "app.note.title": "Geteilte Notizen", + "app.note.label": "Notiz", + "app.note.hideNoteLabel": "Notiz verbergen", "app.userList.usersTitle": "Teilnehmer", "app.userList.participantsTitle": "Teilnehmer", "app.userList.messagesTitle": "Nachrichten", + "app.userList.notesTitle": "Notizen", "app.userList.presenter": "Präsentator", "app.userList.you": "Sie", "app.userList.locked": "Gesperrt", @@ -39,6 +43,17 @@ "app.userList.userAriaLabel": "{0} {1} {2} Status {3}", "app.userList.menu.promoteUser.label": "Zum Moderator befördern", "app.userList.menu.demoteUser.label": "Zum Zuschauer zurückstufen", + "app.userList.userOptions.manageUsersLabel": "Teilnehmer verwalten", + "app.userList.userOptions.muteAllLabel": "Alle Teilnehmer stummschalten", + "app.userList.userOptions.muteAllDesc": "Schaltet alle Teilnehmer der Konferenz stumm", + "app.userList.userOptions.clearAllLabel": "Lösche alle Statusicons", + "app.userList.userOptions.clearAllDesc": "Löscht alle Statusicons der Teilnehmer", + "app.userList.userOptions.muteAllExceptPresenterLabel": "Schalte alle Teilnehmer außer den Präsentator stumm", + "app.userList.userOptions.muteAllExceptPresenterDesc": "Schaltet alle Teilnehmer außer den Präsentator stumm", + "app.userList.userOptions.unmuteAllLabel": "Konferenz-Stummschaltung aufheben", + "app.userList.userOptions.unmuteAllDesc": "Hebt die Konferenz-Stummschaltung auf", + "app.userList.userOptions.lockViewersLabel": "Zuschauerrechte einschränken", + "app.userList.userOptions.lockViewersDesc": "Schränkt bestimmte Funktionen der Konferenzteilnehmer ein", "app.media.label": "Media", "app.media.screenshare.start": "Bildschirmfreigabe wurde gestartet", "app.media.screenshare.end": "Bildschirmfreigabe wurde gestoppt", @@ -57,6 +72,11 @@ "app.presentation.presentationToolbar.fitScreenDesc": "Gesamte Folie darstellen", "app.presentation.presentationToolbar.zoomLabel": "Vergrößerungsgrad", "app.presentation.presentationToolbar.zoomDesc": "Vergrößerungsstufe der Präsentation ändern", + "app.presentation.presentationToolbar.zoomInLabel": "Reinzoomen", + "app.presentation.presentationToolbar.zoomInDesc": "In die Präsentation hinein zoomen", + "app.presentation.presentationToolbar.zoomOutLabel": "Rauszoomen", + "app.presentation.presentationToolbar.zoomOutDesc": "Aus der Präsentation heraus zoomen", + "app.presentation.presentationToolbar.zoomIndicator": "Zoomstufe anzeigen", "app.presentation.presentationToolbar.fitToWidth": "An Breite anpassen", "app.presentation.presentationToolbar.goToSlide": "Folie {0}", "app.presentationUploder.title": "Präsentation", @@ -66,10 +86,13 @@ "app.presentationUploder.dismissLabel": "Abbrechen", "app.presentationUploder.dismissDesc": "Fenster schließen und Änderungen verwerfen", "app.presentationUploder.dropzoneLabel": "Hochzuladende Dateien hier hin ziehen", + "app.presentationUploder.dropzoneImagesLabel": "Bilder hier hinziehen, um sie hochzuladen", "app.presentationUploder.browseFilesLabel": "oder nach Dateien suchen", + "app.presentationUploder.browseImagesLabel": "oder auf der Festplatte nach Bildern suchen", "app.presentationUploder.fileToUpload": "Hochzuladende Datei...", "app.presentationUploder.currentBadge": "Aktuell", "app.presentationUploder.genericError": "Ups, irgendwas ist schief gelaufen", + "app.presentationUploder.rejectedError": "Einige der ausgewählten Dateien wurden zurückgewiesen. Bitte prüfen Sie die Datei-Mime-Typen", "app.presentationUploder.upload.progress": "Hochladen ({0}%)", "app.presentationUploder.upload.413": "Die Datei ist zu groß", "app.presentationUploder.conversion.conversionProcessingSlides": "Verarbeite Seite {0} von {1}", @@ -79,8 +102,25 @@ "app.presentationUploder.conversion.generatingSvg": "Erzeuge SVG Bilder...", "app.presentationUploder.conversion.pageCountExceeded": "Ups, die Seitenanzahl überschreitet das Limit", "app.presentationUploder.conversion.timeout": "Ups, die Konvertierung dauert zu lange", + "app.poll.pollPaneTitle": "Umfrage", + "app.poll.hidePollDesc": "Versteckt das Umfragemenü", + "app.poll.customPollInstruction": "Um selbst erstellte Umfrage zu erstellen, klicken Sie die untenstehende Schaltfläche und geben Sie Ihre Optionen ein", + "app.poll.quickPollInstruction": "Wählen Sie eine Schnellumfrageoption aus und starten Sie die Umfrage.", + "app.poll.customPollLabel": "Selbst erstellte Umfrage", + "app.poll.startCustomLabel": "Selbst erstellte Umfrage starten", + "app.poll.activePollInstruction": "Lassen Sie dieses Fenster solange geöffnet, bis alle Antworten abgegeben wurden. Wählen Sie 'Umfrage veröffentlichen' oder navigieren Sie einen Schritt zurück, um die Umfrage zu beenden.", + "app.poll.publishLabel": "Umfrage veröffentlichen", + "app.poll.backLabel": "Zurück zu den Umfrageoptionen", "app.poll.closeLabel": "Schließen", + "app.poll.customPlaceholder": "Umfrageoption hinzufügen", + "app.poll.tf": "Richtig / Falsch", + "app.poll.yn": "Ja / Nein", + "app.poll.a2": "A / B", + "app.poll.a3": "A / B / C", + "app.poll.a4": "A / B / C / D", + "app.poll.a5": "A / B / C / D / E", "app.poll.liveResult.usersTitle": "Teilnehmer", + "app.poll.liveResult.responsesTitle": "Antwort", "app.polling.pollingTitle": "Umfrage Optionen", "app.polling.pollAnswerLabel": "Umfrageantwort {0}", "app.polling.pollAnswerDesc": "Diese Option auswählen für Umfrage {0}", @@ -102,6 +142,8 @@ "app.navBar.settingsDropdown.hotkeysDesc": "Liste verfügbarer Tastaturkürzel", "app.navBar.settingsDropdown.helpLabel": "Hilfe", "app.navBar.settingsDropdown.helpDesc": "Verlinkt zu den Videoanleitungen", + "app.navBar.settingsDropdown.endMeetingDesc": "Beendet die aktuelle Konferenz", + "app.navBar.settingsDropdown.endMeetingLabel": "Beende Konferenz", "app.navBar.userListToggleBtnLabel": "Teilnehmerliste umschalten", "app.navBar.toggleUserList.ariaLabel": "Teilnehmer/Nachrichten-Umschalter", "app.navBar.toggleUserList.newMessages": "mit Benachrichtigung für neue Nachrichten", @@ -110,6 +152,8 @@ "app.navBar.recording.off": "Keine Aufnahme", "app.leaveConfirmation.confirmLabel": "Verlassen", "app.leaveConfirmation.confirmDesc": "Hiermit verlassen Sie Konferenz", + "app.endMeeting.title": "Beende Konferenz", + "app.endMeeting.description": "Sind Sie sicher, dass Sie die Konferenz beenden wollen?", "app.endMeeting.yesLabel": "Ja", "app.endMeeting.noLabel": "Nein", "app.about.title": "Versionsinfo", @@ -198,6 +242,12 @@ "app.actionsBar.actionsDropdown.initPollDesc": "Eine Umfrage starten", "app.actionsBar.actionsDropdown.desktopShareDesc": "Ihren Bildschirm mit anderen teilen", "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Bildschirmfreigabe beenden mit", + "app.actionsBar.actionsDropdown.pollBtnLabel": "Umfrage starten", + "app.actionsBar.actionsDropdown.pollBtnDesc": "Umschalten des Umfragemenüs", + "app.actionsBar.actionsDropdown.createBreakoutRoom": "Breakout-Räume erstellen", + "app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "ermöglicht die aktuelle Konferenz in mehrere Räume aufzuteilen", + "app.actionsBar.actionsDropdown.takePresenter": "Rolle des Präsentators übernehmen", + "app.actionsBar.actionsDropdown.takePresenterDesc": "Sich selber zum neuen Präsentator machen", "app.actionsBar.emojiMenu.statusTriggerLabel": "Status setzen", "app.actionsBar.emojiMenu.awayLabel": "Abwesend", "app.actionsBar.emojiMenu.awayDesc": "Ihren Status auf abwesend setzen", @@ -293,6 +343,7 @@ "app.error.404": "Nicht gefunden", "app.error.401": "Nicht erlaubt", "app.error.403": "Verboten", + "app.error.400": "Ungültige Anfrage", "app.error.leaveLabel": "Erneut einloggen", "app.guest.waiting": "Warte auf Erlaubnis zur Konferenzteilnahme", "app.toast.breakoutRoomEnded": "Breakout Raum wurde beendet. Bitte klicken Sie aufs Mikrofon-Icon um wieder der Audiokonferenz im Hauptraum beizutreten", @@ -315,13 +366,28 @@ "app.shortcut-help.closePrivateChat": "Privaten Chat schließen", "app.shortcut-help.openActions": "Aktionsmenü öffnen", "app.shortcut-help.openStatus": "Statusmenü öffnen", + "app.lock-viewers.title": "Zuschauerfunktionen einschränken", + "app.lock-viewers.description": "Diese Optionen erlauben es Ihnen, bestimmte den Zuschauern zur Verfügung stehende Funktionen einzuschränken - so kann beispielsweise der private Chat unterbunden werden (Diese Einschränkungen gelten nicht für Moderatoren)", + "app.lock-viewers.featuresLable": "Funktion", + "app.lock-viewers.lockStatusLabel": "Aktiviert", "app.lock-viewers.webcamLabel": "Webcam", + "app.lock-viewers.otherViewersWebcamLabel": "Nur Moderatoren sehen Webcams", "app.lock-viewers.microphoneLable": "Mit Mikrofon", "app.lock-viewers.PublicChatLabel": "Öffentlicher Chat", "app.lock-viewers.PrivateChatLable": "Privater Chat", "app.lock-viewers.Layout": "Layout", + "app.recording.startTitle": "Aufzeichnung starten", + "app.recording.stopTitle": "Aufzeichnung stoppen", + "app.recording.startDescription": "Klicken Sie erneut auf das Icon um die Aufzeichnung zu beenden.", + "app.recording.stopDescription": "Sind Sie sicher, dass Sie die Aufzeichnung der Konferenz beenden wollen? Sie können die Aufzeichnung jederzeit durch erneutes Klicken auf das Icon fortsetzen.", + "app.videoPreview.cameraLabel": "Kamera", "app.videoPreview.cancelLabel": "Abbrechen", "app.videoPreview.closeLabel": "Schließen", + "app.videoPreview.startSharingLabel": "Kamerafreigabe starten", + "app.videoPreview.webcamOptionLabel": "Webcam auswählen", + "app.videoPreview.webcamPreviewLabel": "Webcamvorschau", + "app.videoPreview.webcamSettingsTitle": "Webcam-Einstellungen", + "app.videoPreview.webcamNotFoundLabel": "Keine Webcam gefunden", "app.video.joinVideo": "Webcam freigeben", "app.video.leaveVideo": "Webcam stoppen", "app.video.iceCandidateError": "Fehler beim Hinzufügen vom ice candidate", @@ -332,6 +398,7 @@ "app.video.notAllowed": "Freigabeerlaubnis für die Webcam nicht erteilt, prüfen Sie Ihre Browerberechtigungen", "app.video.notSupportedError": "Webcam kann nur über sichere Verbindung freigegeben werden, stellen Sie sicher, dass das SSL-Zertifikat gültig ist", "app.video.notReadableError": "Konnte nicht auf die Webcam zugreifen. Stellen Sie sicher, dass kein anderes Programm auf die Webcam zugreift", + "app.video.mediaFlowTimeout1020": "Fehler 1020: Medienverbindung zum Server konnte nicht hergestellt werden", "app.video.swapCam": "Wechseln", "app.video.swapCamDesc": "Ausrichtung der Webcams wechseln", "app.video.videoMenu": "Videomenü", @@ -359,10 +426,12 @@ "app.sfu.mediaServerRequestTimeout2003": "Fehler 2003: Zeitüberschreitung bei Anfragen an den Mediaserver", "app.sfu.serverIceGatheringFailed2021": "Fehler 2021: Mediaserver kann ICE Kandidaten nicht erfassen", "app.sfu.serverIceGatheringFailed2022": "Fehler 2022: Mediaserver ICE Verbindung fehlgeschlagen", + "app.sfu.mediaGenericError2200": "Fehler 2200: Medienserver konnte die Anfrage nicht verarbeiten", "app.sfu.invalidSdp2202":"Fehler 2202: Client hat ungültigen SDP generiert", "app.sfu.noAvailableCodec2203": "Fehler 2203: Server konnte keinen passenden Codec finden", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.toolbar.tools": "Werkzeuge", + "app.whiteboard.toolbar.tools.hand": "Verschieben", "app.whiteboard.toolbar.tools.pencil": "Stift", "app.whiteboard.toolbar.tools.rectangle": "Rechteck", "app.whiteboard.toolbar.tools.triangle": "Dreieck", @@ -399,8 +468,37 @@ "app.videoDock.webcamFocusDesc": "Ausgewählte Webcam vergrößern", "app.videoDock.webcamUnfocusLabel": "Normalgröße", "app.videoDock.webcamUnfocusDesc": "Ausgewählte Webcam auf Normalgröße verkleinern", + "app.invitation.title": "Breakoutraum-Einladung", + "app.invitation.confirm": "Einladen", + "app.createBreakoutRoom.title": "Breakout-Räume", + "app.createBreakoutRoom.breakoutRoomLabel": "Breakout-Räume {0}", + "app.createBreakoutRoom.generatingURL": "Erzeuge URL", + "app.createBreakoutRoom.generatedURL": "Erzeugt", + "app.createBreakoutRoom.duration": "Dauer {0}", + "app.createBreakoutRoom.room": "Raum {0}", + "app.createBreakoutRoom.notAssigned": "Nicht zugewiesen ({0})", + "app.createBreakoutRoom.join": "Raum beitreten", "app.createBreakoutRoom.joinAudio": "Audio starten", - "app.externalVideo.close": "Schließen" + "app.createBreakoutRoom.returnAudio": "Return audio", + "app.createBreakoutRoom.confirm": "Erstellen", + "app.createBreakoutRoom.numberOfRooms": "Anzahl der Räume", + "app.createBreakoutRoom.durationInMinutes": "Dauer (Minuten)", + "app.createBreakoutRoom.randomlyAssign": "Zufällige Raumzuweisung", + "app.createBreakoutRoom.endAllBreakouts": "Alle Breakout-Räume beenden", + "app.createBreakoutRoom.roomName": "{0} (Raum - {1})", + "app.createBreakoutRoom.doneLabel": "Fertig", + "app.createBreakoutRoom.nextLabel": "Nächster", + "app.createBreakoutRoom.addParticipantLabel": "+ Teilnehmer hinzufügen", + "app.createBreakoutRoom.freeJoin": "Den Teilnehmern erlauben, sich selber einen Breakout-Raum auszusuchen.", + "app.createBreakoutRoom.leastOneWarnBreakout": "Jedem Breakout-Raum muss wenigstens ein Teilnehmer zugeordnet sein.", + "app.createBreakoutRoom.modalDesc": "Vervollständigen Sie die folgenden Schritte um Breakout-Räume zu erzeugen. Verteilen Sie die Teilnehmer auf die Räume.", + "app.externalVideo.start": "Neues Video teilen", + "app.externalVideo.stop": "Teilen des Videos beenden", + "app.externalVideo.title": "Youtube-Video teilen", + "app.externalVideo.input": "Youtube-Video URL", + "app.externalVideo.urlError": "Dies ist kein gültiges Youtube-Video", + "app.externalVideo.close": "Schließen", + "app.actionsBar.actionsDropdown.shareExternalVideo": "Youtube-Video teilen" } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 25487bd2f9dcec89dfb1da44cd435a2eac848a51..533031230db2f8837cb6d9cfad03c46f980ee5a8 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -43,6 +43,8 @@ "app.userList.userAriaLabel": "{0} {1} {2} Status {3}", "app.userList.menu.promoteUser.label": "Promote to moderator", "app.userList.menu.demoteUser.label": "Demote to viewer", + "app.userList.menu.unlockUser.label": "Unlock {0}", + "app.userList.menu.lockUser.label": "Lock {0}", "app.userList.userOptions.manageUsersLabel": "Manage users", "app.userList.userOptions.muteAllLabel": "Mute all users", "app.userList.userOptions.muteAllDesc": "Mutes all users in the meeting", @@ -60,6 +62,7 @@ "app.media.screenshare.safariNotSupported": "Screenshare is currently not supported by Safari. Please, use Firefox or Google Chrome.", "app.meeting.ended": "This session has ended", "app.meeting.endedMessage": "You will be forwarded back to the home screen", + "app.presentation.close": "Close presentation", "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", @@ -169,6 +172,8 @@ "app.actionsBar.camOffLabel": "Cam Off", "app.actionsBar.raiseLabel": "Raise", "app.actionsBar.label": "Actions Bar", + "app.actionsBar.actionsDropdown.restorePresentationLabel": "Restore Presentation", + "app.actionsBar.actionsDropdown.restorePresentationDesc": "Button to restore presentation after it has been closed", "app.submenu.application.applicationSectionTitle": "Application", "app.submenu.application.animationsLabel": "Animations", "app.submenu.application.audioAlertLabel": "Audio Alerts for Chat", @@ -200,6 +205,7 @@ "app.submenu.closedCaptions.fontSizeOptionLabel": "Choose Font size", "app.submenu.closedCaptions.backgroundColorLabel": "Background color", "app.submenu.closedCaptions.fontColorLabel": "Font color", + "app.submenu.closedCaptions.noLocaleSelected": "Locale is not selected", "app.submenu.participants.muteAllLabel": "Mute all except the presenter", "app.submenu.participants.lockAllLabel": "Lock all participants", "app.submenu.participants.lockItemLabel": "Participants {0}", @@ -402,6 +408,8 @@ "app.video.mediaFlowTimeout1020": "Error 1020: media could not reach the server", "app.video.swapCam": "Swap", "app.video.swapCamDesc": "swap the direction of webcams", + "app.video.videoDisabled": "Webcam is disabled", + "app.video.videoButtonDesc": "Join video button", "app.video.videoMenu": "Video menu", "app.video.videoMenuDisabled": "Video menu Webcam is disabled in Settings", "app.video.videoMenuDesc": "Open video menu dropdown", @@ -420,6 +428,7 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "Encode usage", "app.video.stats.currentDelay": "Current delay", + "app.fullscreenButton.label": "Make element fullscreen", "app.deskshare.iceConnectionStateError": "Error 1108: ICE connection failed when sharing screen", "app.sfu.mediaServerConnectionError2000": "Error 2000: Unable to connect to media server", "app.sfu.mediaServerOffline2001": "Error 2001: Media server is offline. Please try again later.", diff --git a/bigbluebutton-html5/private/locales/es.json b/bigbluebutton-html5/private/locales/es.json new file mode 100644 index 0000000000000000000000000000000000000000..35e0cfa99cc9cf9669609b9770605665b6bfb964 --- /dev/null +++ b/bigbluebutton-html5/private/locales/es.json @@ -0,0 +1,297 @@ +{ + "app.home.greeting": "¡Bienvenido {0}! Tu presentación iniciará en breve...", + "app.chat.submitLabel": "Enviar mensaje", + "app.chat.errorMinMessageLength": "El mensaje es {0} caracteres mas corto de lo esperado", + "app.chat.errorMaxMessageLength": "El mensaje es {0} caracteres mas largo de lo esperado", + "app.chat.inputLabel": "Entrada de mensaje para chat {0}", + "app.chat.inputPlaceholder": "Mensaje {0}", + "app.chat.titlePublic": "Chat público", + "app.chat.titlePrivate": "Chat privado con {0}", + "app.chat.partnerDisconnected": "{0} ha abandonado la reunión", + "app.chat.closeChatLabel": "Cerrar {0}", + "app.chat.hideChatLabel": "Ocultar {0}", + "app.chat.moreMessages": "Más mensajes abajo", + "app.chat.dropdown.options": "Opciones de chat", + "app.chat.dropdown.clear": "Limpiar", + "app.chat.dropdown.copy": "Copiar", + "app.chat.dropdown.save": "Guardar", + "app.chat.label": "Chat", + "app.chat.emptyLogLabel": "Registro de chat vacÃo", + "app.chat.clearPublicChatMessage": "El chat publico fue borrado por un moderador", + "app.note.title": "Notas compartidas", + "app.note.label": "Nota", + "app.note.hideNoteLabel": "Ocultar nota", + "app.userList.usersTitle": "Usuarios", + "app.userList.participantsTitle": "Participantes", + "app.userList.messagesTitle": "Mensajes", + "app.userList.notesTitle": "Notas", + "app.userList.presenter": "Presentador", + "app.userList.you": "Tu", + "app.userList.locked": "Bloqueado", + "app.userList.label": "Lista de usuarios", + "app.userList.toggleCompactView.label": "Cambiar a modo de vista compacta", + "app.userList.guest": "Huesped", + "app.userList.menuTitleContext": "Opciones disponibles", + "app.userList.chatListItem.unreadSingular": "{0} Nuevo Mensaje", + "app.userList.chatListItem.unreadPlural": "{0} Nuevos mensajes", + "app.userList.menu.chat.label": "Chat", + "app.userList.menu.clearStatus.label": "Limpiar estátus", + "app.userList.menu.makePresenter.label": "Hacer presentador", + "app.userList.menu.removeUser.label": "Eliminar usuario", + "app.userList.menu.muteUserAudio.label": "Silenciar usuario", + "app.userList.menu.unmuteUserAudio.label": "Activar sonido de usuario", + "app.userList.userAriaLabel": "{0} {1} {2} estado {3}", + "app.userList.menu.promoteUser.label": "Promover a moderador", + "app.userList.menu.demoteUser.label": "Degradar a espectador", + "app.userList.userOptions.manageUsersLabel": "Manejar usuarios", + "app.userList.userOptions.muteAllLabel": "Deshabilitar audio a todos los usuarios", + "app.userList.userOptions.muteAllDesc": "Deshabilitar audio a todos los usuarios en la sesión", + "app.userList.userOptions.clearAllLabel": "Borrar todos los iconos de estado", + "app.userList.userOptions.clearAllDesc": "Borrar todos los iconos de estado de usuarios", + "app.userList.userOptions.muteAllExceptPresenterLabel": "Silenciar a todos los usuarios excepto a presentador", + "app.userList.userOptions.muteAllExceptPresenterDesc": "Silenciar a todos los usuarios en la sesión excepto a presentador", + "app.userList.userOptions.unmuteAllLabel": "Desactivar función de silenciar", + "app.userList.userOptions.unmuteAllDesc": "Habilitar audio en la sesión", + "app.userList.userOptions.lockViewersLabel": "Bloquear espectadores", + "app.media.label": "Media", + "app.media.screenshare.start": "Compartir pantalla ha iniciado", + "app.media.screenshare.end": "Compartir pantalla ha finalizado", + "app.media.screenshare.safariNotSupported": "Compartir pantalla actualmente no es soportada por Safari. Por favor usilize Firefox o Google Chrome.", + "app.meeting.ended": "La sesión ha finalizado", + "app.meeting.endedMessage": "Serás enviado a la pantalla de inicio.", + "app.presentation.presentationToolbar.prevSlideLabel": "Diapositiva anterior", + "app.presentation.presentationToolbar.prevSlideDesc": "Cambiar presentación a diapositiva anterior", + "app.presentation.presentationToolbar.nextSlideLabel": "Siguiente diapositiva", + "app.presentation.presentationToolbar.nextSlideDesc": "Cambiar presentación a diapositiva siguiente", + "app.presentation.presentationToolbar.skipSlideLabel": "Brincar diapositiva", + "app.presentation.presentationToolbar.skipSlideDesc": "Cambiar presentación a diapositiva especÃfica", + "app.presentation.presentationToolbar.fitWidthLabel": "Ajustar a lo ancho", + "app.presentation.presentationToolbar.fitWidthDesc": "Mostrar diapositiva a todo lo ancho", + "app.presentation.presentationToolbar.fitScreenLabel": "Ajustar a la pantalla", + "app.presentation.presentationToolbar.fitScreenDesc": "Mostrar toda la diapositiva", + "app.presentation.presentationToolbar.zoomLabel": "Zoom", + "app.presentation.presentationToolbar.zoomDesc": "Cambiar el nivel de Zoom de la presentación", + "app.presentation.presentationToolbar.zoomInLabel": "Acercarse", + "app.presentation.presentationToolbar.zoomInDesc": "Acercarse en presentación", + "app.presentation.presentationToolbar.zoomOutLabel": "Alejarse", + "app.presentation.presentationToolbar.zoomOutDesc": "Alejarse en presentación", + "app.presentation.presentationToolbar.zoomIndicator": "Mostrar el porcentaje de acercamiento", + "app.presentation.presentationToolbar.fitToWidth": "Ajustar a lo ancho", + "app.presentation.presentationToolbar.goToSlide": "Diapositiva {0}", + "app.presentationUploder.title": "Presentación", + "app.presentationUploder.confirmLabel": "Iniciar", + "app.presentationUploder.confirmDesc": "Grardar los cambios e iniciar la presentación", + "app.presentationUploder.dismissLabel": "Cancelar", + "app.presentationUploder.dismissDesc": "Cerrar la ventana modal y descartar cambios.", + "app.presentationUploder.dropzoneLabel": "Arrastrar archivo aqui para cargarlo", + "app.presentationUploder.dropzoneImagesLabel": "Arrastrar imagenes aqui para cargarlas", + "app.presentationUploder.browseFilesLabel": "o buscar archivos", + "app.presentationUploder.browseImagesLabel": "o buscar imagenes", + "app.presentationUploder.fileToUpload": "Esperando ser cargado...", + "app.presentationUploder.currentBadge": "Acual", + "app.presentationUploder.genericError": "Ups, algo salio mal", + "app.presentationUploder.rejectedError": "Algunos de los archivos seleccionados fueron rechazados. Por favor verifique el mime type de los archivo", + "app.presentationUploder.upload.progress": "Cargando ({0}%)", + "app.presentationUploder.upload.413": "Erchivo demasiado grande", + "app.presentationUploder.conversion.conversionProcessingSlides": "Procesando página {0} de {1}", + "app.presentationUploder.conversion.genericConversionStatus": "Convirtiendo archivo...", + "app.presentationUploder.conversion.generatingThumbnail": "Generando miniaturas...", + "app.presentationUploder.conversion.generatedSlides": "Diapositivas generadas...", + "app.presentationUploder.conversion.generatingSvg": "Generando imágenes SVG...", + "app.poll.startCustomLabel": "Iniciar encuesta personalizada", + "app.poll.publishLabel": "Publicar encuesta", + "app.poll.closeLabel": "Cerrar", + "app.poll.tf": "Verdadero / Falseo", + "app.poll.yn": "Si / No", + "app.poll.a2": "A / B", + "app.poll.a3": "A / B / C", + "app.poll.a4": "A / B / C / D", + "app.poll.a5": "A / B / C / D / E", + "app.poll.liveResult.usersTitle": "Usuarios", + "app.poll.liveResult.responsesTitle": "Respuesta", + "app.polling.pollingTitle": "Respuestas de encuesta", + "app.polling.pollAnswerLabel": "Respuesta de encuesta {0}", + "app.polling.pollAnswerDesc": "Seleccione esta opcion para responder {0}", + "app.failedMessage": "Disculpas, problemas conectando al servidor.", + "app.connectingMessage": "Conectando...", + "app.waitingMessage": "Desconectado. Intentando reconectar en {0} segundos...", + "app.navBar.settingsDropdown.optionsLabel": "Opciones", + "app.navBar.settingsDropdown.fullscreenLabel": "Desplegar a pantalla completa", + "app.navBar.settingsDropdown.settingsLabel": "Abrir configuración", + "app.navBar.settingsDropdown.aboutLabel": "Acerca de", + "app.navBar.settingsDropdown.leaveSessionLabel": "Cerrar sesión", + "app.navBar.settingsDropdown.exitFullscreenLabel": "Salir de pantalla completa", + "app.navBar.settingsDropdown.fullscreenDesc": "Hacer el menú de configuración a pantalla completa", + "app.navBar.settingsDropdown.settingsDesc": "Cambiar la configuración general", + "app.navBar.settingsDropdown.aboutDesc": "Mostrar información acerca del cliente", + "app.navBar.settingsDropdown.leaveSessionDesc": "Abandonar la reunión", + "app.navBar.settingsDropdown.exitFullscreenDesc": "Salir del modo de pantalla completa", + "app.navBar.settingsDropdown.hotkeysLabel": "Acceso rápido", + "app.navBar.settingsDropdown.hotkeysDesc": "Lista de accesos rápidos", + "app.navBar.settingsDropdown.helpLabel": "Ayuda", + "app.navBar.settingsDropdown.helpDesc": "Enlaces a videos tutoriales", + "app.navBar.userListToggleBtnLabel": "Alternar lista de usuarios", + "app.navBar.toggleUserList.newMessages": "con nueva notificación de mensaje ", + "app.leaveConfirmation.confirmLabel": "Salir", + "app.leaveConfirmation.confirmDesc": "Te desconecta de la reunión", + "app.about.title": "Acerca de", + "app.about.version": "Construcción del cliente:", + "app.about.copyright": "Derechos de autor:", + "app.about.confirmLabel": "OK", + "app.about.confirmDesc": "OK", + "app.about.dismissLabel": "Cancelar", + "app.about.dismissDesc": "Cerrar información acerca del cliente", + "app.actionsBar.changeStatusLabel": "Cambiar estátus", + "app.actionsBar.muteLabel": "Silenciar", + "app.actionsBar.unmuteLabel": "De-silenciar", + "app.actionsBar.camOffLabel": "Cámara Apagada", + "app.actionsBar.raiseLabel": "Alzar", + "app.actionsBar.label": "Barra de acciones", + "app.submenu.application.applicationSectionTitle": "Aplicación", + "app.submenu.application.fontSizeControlLabel": "Tamaño de fuente", + "app.submenu.application.increaseFontBtnLabel": "Incrementar tamaño de fuente", + "app.submenu.application.decreaseFontBtnLabel": "Reducir tamaño de fuente", + "app.submenu.application.languageLabel": "Lenguaje de aplicación", + "app.submenu.application.ariaLanguageLabel": "Cambiar lenguaje de aplicación", + "app.submenu.application.languageOptionLabel": "Seleccionar lenguaje", + "app.submenu.application.noLocaleOptionLabel": "No hay locales activos", + "app.submenu.audio.micSourceLabel": "Fuente de micrófono", + "app.submenu.audio.speakerSourceLabel": "Fuente de altavoces", + "app.submenu.audio.streamVolumeLabel": "Volumen del flujo de audio", + "app.submenu.video.title": "Video", + "app.submenu.video.videoSourceLabel": "Fuente del video", + "app.submenu.video.videoOptionLabel": "Escoger ver fuente", + "app.submenu.video.videoQualityLabel": "Calidad del Video", + "app.submenu.video.qualityOptionLabel": "Escoger calidad del video", + "app.submenu.video.participantsCamLabel": "Viendo webcams de participantes", + "app.submenu.closedCaptions.closedCaptionsLabel": "SubtÃtulos", + "app.submenu.closedCaptions.takeOwnershipLabel": "Tomar el control", + "app.submenu.closedCaptions.languageLabel": "Lenguaje", + "app.submenu.closedCaptions.localeOptionLabel": "Seleccionar lenguaje", + "app.submenu.closedCaptions.noLocaleOptionLabel": "No hay locales activos", + "app.submenu.closedCaptions.fontFamilyLabel": "Familia de fuente", + "app.submenu.closedCaptions.fontFamilyOptionLabel": "Seleccionar familia de fuente", + "app.submenu.closedCaptions.fontSizeLabel": "Tamaño de fuente", + "app.submenu.closedCaptions.fontSizeOptionLabel": "Seleccionar tamaño de fuente", + "app.submenu.closedCaptions.backgroundColorLabel": "Color de fondo", + "app.submenu.closedCaptions.fontColorLabel": "Color de fuente", + "app.submenu.participants.muteAllLabel": "Silenciar a todos menos al presentador", + "app.submenu.participants.lockAllLabel": "Bloquear a todos los participantes", + "app.submenu.participants.lockItemLabel": "Participantes {0}", + "app.submenu.participants.lockMicDesc": "Deshabilita micrófono a todos los participantes bloqueados", + "app.submenu.participants.lockCamDesc": "Deshabilita el webcam a todos los participantes bloqueados", + "app.submenu.participants.lockPublicChatDesc": "Deshabilita el chat público a todos los usuarios bloqueados", + "app.submenu.participants.lockPrivateChatDesc": "Deshabilita el chat privado a todos los usuarios bloqueados", + "app.submenu.participants.lockLayoutDesc": "Bloquea el diseño a todos los usuarios bloqueados", + "app.submenu.participants.lockMicAriaLabel": "Bloquea micrófono", + "app.submenu.participants.lockCamAriaLabel": "Bloquea webcam", + "app.submenu.participants.lockPublicChatAriaLabel": "Bloquea el chat público", + "app.submenu.participants.lockPrivateChatAriaLabel": "Bloquea el chat privado", + "app.submenu.participants.lockLayoutAriaLabel": "Bloquea el diseño", + "app.submenu.participants.lockMicLabel": "Micrófono", + "app.submenu.participants.lockCamLabel": "Webcam", + "app.submenu.participants.lockPublicChatLabel": "Chat público", + "app.submenu.participants.lockPrivateChatLabel": "Chat privado", + "app.submenu.participants.lockLayoutLabel": "Diseño", + "app.settings.applicationTab.label": "Aplicación", + "app.settings.audioTab.label": "Audio", + "app.settings.videoTab.label": "Video", + "app.settings.closedcaptionTab.label": "SubtÃtulos", + "app.settings.usersTab.label": "Participantes", + "app.settings.main.label": "Configuración", + "app.settings.main.cancel.label": "Cancela", + "app.settings.main.cancel.label.description": "Deshecha los cambios y cierra el menú de configuración", + "app.settings.main.save.label": "Guardar", + "app.settings.main.save.label.description": "Gurada cambios y cierra el menú de configuración", + "app.actionsBar.actionsDropdown.actionsLabel": "Acciones", + "app.actionsBar.actionsDropdown.presentationLabel": "Subir una presentación", + "app.actionsBar.actionsDropdown.initPollLabel": "Iniciar una encuesta", + "app.actionsBar.actionsDropdown.desktopShareLabel": "Compartir tu pantalla", + "app.actionsBar.actionsDropdown.presentationDesc": "Subir tu presentación", + "app.actionsBar.actionsDropdown.initPollDesc": "Iniciar una encuesta", + "app.actionsBar.actionsDropdown.desktopShareDesc": "Compartir tu pantalla con otros", + "app.actionsBar.emojiMenu.awayLabel": "Ausente", + "app.actionsBar.emojiMenu.awayDesc": "Cambiar tu estatus a ausente", + "app.actionsBar.emojiMenu.raiseHandLabel": "Alzar", + "app.actionsBar.emojiMenu.raiseHandDesc": "Alzar la mano para preguntar", + "app.actionsBar.emojiMenu.neutralLabel": "Indeciso", + "app.actionsBar.emojiMenu.neutralDesc": "Cambiar tu estatus a indeciso", + "app.actionsBar.emojiMenu.confusedLabel": "Confundido", + "app.actionsBar.emojiMenu.confusedDesc": "Cambiar tu estatus a confundido", + "app.actionsBar.emojiMenu.sadLabel": "Triste", + "app.actionsBar.emojiMenu.sadDesc": "Cambiar tu estatus a triste", + "app.actionsBar.emojiMenu.happyLabel": "Feliz", + "app.actionsBar.emojiMenu.happyDesc": "Cambiar tu estatus a feliz", + "app.actionsBar.emojiMenu.noneLabel": "Limpiar estátus", + "app.actionsBar.emojiMenu.noneDesc": "Limpia tu estatus", + "app.actionsBar.emojiMenu.applauseLabel": "Aplausos", + "app.actionsBar.emojiMenu.applauseDesc": "Cambiar tu estatus a aplausos", + "app.actionsBar.emojiMenu.thumbsUpLabel": "Señal de aprobación", + "app.actionsBar.emojiMenu.thumbsUpDesc": "Cambiar tu estatus a señal de aprobación", + "app.actionsBar.emojiMenu.thumbsDownLabel": "Señal de desaprobación", + "app.actionsBar.emojiMenu.thumbsDownDesc": "Cambia tu estatus a señal de desaprobación", + "app.actionsBar.currentStatusDesc": "estatus actual {0}", + "app.audioNotification.audioFailedError1001": "Error 1001: WebSocket desconectado", + "app.audioNotification.audioFailedError1002": "Error 1002: No puede establecerse conexión de WebSocket", + "app.audioNotification.audioFailedError1003": "Error 1003: Versión de navegador no soportado ", + "app.audioNotification.audioFailedError1004": "Error 1004: Falla en llamada", + "app.audioNotification.audioFailedError1005": "Error 1005: La llamada terminó inesperadamente", + "app.audioNotification.audioFailedError1006": "Error 1006: Tiempo de espera de llamada agotado", + "app.audioNotification.audioFailedError1007": "Error 1007: Fallo de negociación ICE", + "app.audioNotification.audioFailedError1008": "Error 1008: Fallo de transferencia", + "app.audioNotification.audioFailedError1009": "Error 1009: No se pudo obtener información de servidor STUN/TURN", + "app.audioNotification.audioFailedError1010": "Error 1010: Se acabó tiempo de negociación ICE ", + "app.audioNotification.audioFailedError1011": "Error 1011: Se acabó tiempo recolectando ICE", + "app.audioNotification.audioFailedMessage": "Tu conexión de audio falló en conectarse", + "app.audioNotification.mediaFailedMessage": "getUserMicMedia falló, Solo orÃgenes seguros son admitidos", + "app.audioNotification.closeLabel": "Cerrar", + "app.breakoutJoinConfirmation.title": "Unirse a una sala de grupo", + "app.breakoutJoinConfirmation.message": "Quieres unirte", + "app.breakoutJoinConfirmation.confirmLabel": "Unirte", + "app.breakoutJoinConfirmation.confirmDesc": "Unirte a sala de grupo", + "app.breakoutJoinConfirmation.dismissLabel": "Cancelar", + "app.breakoutJoinConfirmation.dismissDesc": "Cierra y rechaza Unirse a Sala de Grupo", + "app.breakoutTimeRemainingMessage": "Tiempo restante en Sala de Grupo", + "app.breakoutWillCloseMessage": "Tiempo transcurrido. Sala de Grupo de cerrará en breve.", + "app.calculatingBreakoutTimeRemaining": "Calculando tiempo restante...", + "app.audioModal.microphoneLabel": "Micrófono", + "app.audioModal.listenOnlyLabel": "Solo escuchar", + "app.audioModal.audioChoiceLabel": "¿Como quieres unirte al audio?", + "app.audioModal.audioChoiceDesc": "Selecciona como unirse al audio en esta reunión", + "app.audioModal.closeLabel": "Cerrar", + "app.audio.joinAudio": "Unirse al audio", + "app.audio.leaveAudio": "Abandonar audio", + "app.audio.enterSessionLabel": "Entrar a la sesión", + "app.audio.playSoundLabel": "Reproducir sonido", + "app.audio.backLabel": "Atrás", + "app.audio.audioSettings.titleLabel": "Seleccionar tu configuración de audio", + "app.audio.audioSettings.descriptionLabel": "Ten en cuenta que aparecerá un cuadro de diálogo en tu navegador, que te pide a aceptar compartir tu micrófono.", + "app.audio.audioSettings.microphoneSourceLabel": "Fuente del micrófono", + "app.audio.audioSettings.speakerSourceLabel": "Fuente de altavoz", + "app.audio.audioSettings.microphoneStreamLabel": "Tu volúmen del flujo de audio", + "app.audio.listenOnly.backLabel": "Atrás", + "app.audio.listenOnly.closeLabel": "Cerrar", + "app.error.meeting.ended": "Haz salido de la conferencia", + "app.dropdown.close": "Cerrar", + "app.error.500": "Ups, algo salio mal", + "app.error.404": "No se encontró", + "app.error.401": "No autorizado", + "app.error.403": "Prohibido", + "app.error.leaveLabel": "Ingresa de nuevo", + "app.guest.waiting": "Esperando aprobación para unirse", + "app.shortcut-help.title": "Atajos", + "app.shortcut-help.closeLabel": "Cerrar", + "app.lock-viewers.webcamLabel": "Webcam", + "app.lock-viewers.microphoneLable": "Micrófono", + "app.lock-viewers.PublicChatLabel": "Chat público", + "app.lock-viewers.PrivateChatLable": "Chat privado", + "app.lock-viewers.Layout": "Diseño", + "app.videoPreview.cancelLabel": "Cancelar", + "app.videoPreview.closeLabel": "Cerrar", + "app.meeting.endNotification.ok.label": "OK", + "app.feedback.title": "Haz salido de la conferencia", + "app.createBreakoutRoom.joinAudio": "Unirse al audio", + "app.externalVideo.close": "Cerrar" + +} + diff --git a/bigbluebutton-html5/private/locales/pl_PL.json b/bigbluebutton-html5/private/locales/pl_PL.json new file mode 100644 index 0000000000000000000000000000000000000000..794aafd2d8b3acb224cda350d04feb1631b82f2d --- /dev/null +++ b/bigbluebutton-html5/private/locales/pl_PL.json @@ -0,0 +1,41 @@ +{ + "app.chat.submitLabel": "WyÅ›lij wiadomość", + "app.chat.titlePrivate": "Czat prywatny z {0}", + "app.chat.closeChatLabel": "Zamknij {0}", + "app.chat.hideChatLabel": "Ukryj {0}", + "app.chat.dropdown.options": "Opcje Czatu", + "app.chat.dropdown.clear": "Wyczyść", + "app.chat.dropdown.copy": "Kopiuj", + "app.chat.dropdown.save": "Zapisz", + "app.chat.label": "Czat", + "app.note.title": "UdostÄ™pnione notatki", + "app.note.label": "Notatka", + "app.note.hideNoteLabel": "Ukryj notatkÄ™", + "app.userList.usersTitle": "Użytkownicy", + "app.userList.participantsTitle": "Uczestnicy", + "app.userList.messagesTitle": "WiadomoÅ›ci", + "app.userList.notesTitle": "Notatki", + "app.userList.presenter": "Prezenter", + "app.userList.you": "Ty", + "app.userList.label": "Lista użytkowników", + "app.userList.guest": "Gość", + "app.userList.menuTitleContext": "DostÄ™pne opcje", + "app.userList.chatListItem.unreadSingular": "{0} Nowa Wiadomość", + "app.userList.menu.chat.label": "Czat", + "app.userList.menu.clearStatus.label": "Wyczyść status", + "app.userList.menu.removeUser.label": "UsuÅ„ użytkownika", + "app.poll.closeLabel": "Zamknij", + "app.poll.liveResult.usersTitle": "Użytkownicy", + "app.settings.usersTab.label": "Uczestincy", + "app.settings.main.save.label": "Zapisz", + "app.actionsBar.emojiMenu.noneLabel": "Wyczyść status", + "app.audioNotification.closeLabel": "Zamknij", + "app.audioModal.closeLabel": "Zamknij", + "app.audio.listenOnly.closeLabel": "Zamknij", + "app.dropdown.close": "Zamknij", + "app.shortcut-help.closeLabel": "Zamknij", + "app.videoPreview.closeLabel": "Zamknij", + "app.externalVideo.close": "Zamknij" + +} +