Compare commits

...

16 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
505ed15323 WIP 2020-06-10 17:10:02 +02:00
Vlad Piersec
8758c222c6 feat(branding): Add ability to customize logo & background 2020-06-10 14:58:27 +02:00
Bettenbuk Zoltan
29dc63fbcb ref: merge prejoin with lobby 2020-06-09 18:10:43 +02:00
Bettenbuk Zoltan
475a2ae596 feat: lobby feature
The lobby feature adds the possibility to lock a meeting and only allow people in after virtually knocking and going through formal approval
2020-06-09 18:10:43 +02:00
dependabot[bot]
338c960215 build(deps): bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-09 10:09:02 +02:00
Jaya Allamsetty
e6093e0706 fix(UI): Highlight the audio device when we hover over it 2020-06-08 15:34:05 -04:00
Jaya Allamsetty
d1d968997e Disable audiosettings button on Firefox as it is not supported 2020-06-08 15:34:05 -04:00
Jaya Allamsetty
45570bc0e7 fix(device-selection): Update redux when a new speaker is selected
Update userSelectedAudioOutputDeviceId and userSelectedAudioOutputDeviceLabel when a new speaker is selected from the audio settings popup menu
2020-06-08 15:34:05 -04:00
Jaya Allamsetty
f4bcad02d8 fix(device-selection): Add a workaround for a chrome bug with default mic
Pass the real deviceId to gUM instead of 'default' for Chrome to return the correct media stream
2020-06-08 15:34:05 -04:00
khajaamin
26f7951894 Marathi language (#6615)
* Lang folder removed from proxy server and now using from local dev dir

* Added Marathi 100% langulage transalation
2020-06-08 12:33:40 -05:00
Richard Février
35dabb1a27 main-fr.json : remove duplicated key introduced by #6461 2020-06-08 12:13:36 -05:00
Murat Emir Cabaroğlu
c3b79802b2 add missing and fix wrong turkish translation 2020-06-08 11:17:28 -05:00
Дамян Минков
e6dbe65193 Moderated rooms or subdomains (#6959)
* fix: Fixes using token with no user context.

* feat(moderated): Adds option to add moderated rooms and subdomains.

When a user joins such room or subdomain in order to be a moderator needs to provide a valid jwt token for that room.

* squash: Renames function.

* ref: Removes filtering jicofo setting owners.

This will be disabled on jicofo side and will greatly simplify logic.
Also check the checks to avoid jwt for main domain to access subdomains and the other way around.

* fix: Skips allowners logic for admins.
2020-06-05 07:57:49 -05:00
Deepak Verma
ff23f81dfe flags: fix comment 2020-06-05 13:53:35 +02:00
Jaya Allamsetty
bc66c9063a chore(deps): update lib-jitsi-meet 2020-06-04 10:54:06 -04:00
Emil Ivov
974ef4a382 Merge pull request #6941 from jitsi/saghul-patch-5
shared-video: use a more recent video by default
2020-06-03 06:42:41 -05:00
116 changed files with 4673 additions and 738 deletions

View File

@@ -48,6 +48,7 @@ import {
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
getDefaultDeviceId,
notifyCameraError,
notifyMicError,
setAudioOutputDeviceId,
@@ -295,12 +296,6 @@ class ConferenceConnector {
logger.error('CONFERENCE FAILED:', err, ...params);
switch (err) {
case JitsiConferenceErrors.CONNECTION_ERROR: {
const [ msg ] = params;
APP.UI.notifyConnectionFailed(msg);
break;
}
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
// let's show some auth not allowed page
@@ -335,14 +330,6 @@ class ConferenceConnector {
APP.UI.notifyGracefulShutdown();
break;
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = params;
APP.UI.hideStats();
APP.UI.notifyConferenceDestroyed(reason);
break;
}
// FIXME FOCUS_DISCONNECTED is a confusing event name.
// What really happens there is that the library is not ready yet,
// because Jicofo is not available, but it is going to give it another
@@ -2434,11 +2421,20 @@ export default {
micDeviceId => {
const audioWasMuted = this.isLocalAudioMuted();
// When the 'default' mic needs to be selected, we need to
// pass the real device id to gUM instead of 'default' in order
// to get the correct MediaStreamTrack from chrome because of the
// following bug.
// https://bugs.chromium.org/p/chromium/issues/detail?id=997689
const hasDefaultMicChanged = micDeviceId === 'default';
sendAnalytics(createDeviceChangedEvent('audio', 'input'));
createLocalTracksF({
devices: [ 'audio' ],
cameraDeviceId: null,
micDeviceId
micDeviceId: hasDefaultMicChanged
? getDefaultDeviceId(APP.store.getState(), 'audioInput')
: micDeviceId
})
.then(([ stream ]) => {
// if audio was muted before changing the device, mute
@@ -2462,6 +2458,12 @@ export default {
return this.useAudioStream(stream);
})
.then(() => {
if (hasDefaultMicChanged) {
// workaround for the default device to be shown as selected in the
// settings even when the real device id was passed to gUM because of the
// above mentioned chrome bug.
this.localAudio._realDeviceId = this.localAudio.deviceId = 'default';
}
logger.log(`switched local audio device: ${this.localAudio?.getDeviceId()}`);
this._updateAudioDeviceId();
@@ -2763,11 +2765,20 @@ export default {
checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));
}
// When the 'default' mic needs to be selected, we need to
// pass the real device id to gUM instead of 'default' in order
// to get the correct MediaStreamTrack from chrome because of the
// following bug.
// https://bugs.chromium.org/p/chromium/issues/detail?id=997689
const hasDefaultMicChanged = newDevices.audioinput === 'default';
promises.push(
mediaDeviceHelper.createLocalTracksAfterDeviceListChanged(
createLocalTracksF,
newDevices.videoinput,
newDevices.audioinput)
hasDefaultMicChanged
? getDefaultDeviceId(APP.store.getState(), 'audioInput')
: newDevices.audioinput)
.then(tracks => {
// If audio or video muted before, or we unplugged current
// device and selected new one, then mute new track.
@@ -2792,6 +2803,12 @@ export default {
// Use the new stream or null if we failed to obtain it.
return useStream(tracks.find(track => track.getType() === mediaType) || null)
.then(() => {
if (hasDefaultMicChanged) {
// workaround for the default device to be shown as selected in the
// settings even when the real device id was passed to gUM because of
// the above mentioned chrome bug.
this.localAudio._realDeviceId = this.localAudio.deviceId = 'default';
}
mediaType === 'audio'
? this._updateAudioDeviceId()
: this._updateVideoDeviceId();

View File

@@ -512,6 +512,23 @@ var config = {
// If set to true all muting operations of remote participants will be disabled.
// disableRemoteMute: true,
/**
External API url used to receive branding specific information.
If there is no url set or there are missing fields, the defaults are applied.
None of the fieds are mandatory and the response must have the shape:
{
// The hex value for the colour used as background
backgroundColor: '#fff',
// The url for the image used as background
backgroundImageUrl: 'https://example.com/background-img.png',
// The anchor url used when clicking the logo image
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png'
}
*/
// brandingDataUrl: '',
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold

View File

@@ -1,5 +1,6 @@
.audio-preview {
&-content {
background: #2A3A4B;
font-size: 15px;
line-height: 24px;
max-height: 456px;
@@ -32,7 +33,7 @@
margin-left: 48px;
&--selected {
background: rgba(28,32,37,0.5);
background: #1C2025;
cursor: initial;
margin-left: 0;
padding-left: 21px;
@@ -55,7 +56,7 @@
&:hover {
.audio-preview-entry {
background: rgba(255,255,255, 0.2);
background: #3F4E5E;
margin-left: 0;
padding-left: 48px;
@@ -80,8 +81,23 @@
&-microphone {
position: relative;
}
&:hover {
.audio-preview-entry {
background: #3F4E5E;
margin-left: 0;
padding-left: 48px;
&--selected {
padding-left: 21px;
}
}
}
.audio-preview-entry-text {
max-width: 196px;
}
}
&-icon {
border-radius: 50%;

View File

@@ -115,8 +115,9 @@ form {
.leftwatermark {
left: 32px;
top: 32px;
background-image: url($defaultWatermarkLink);
background-position: center left;
background-repeat: no-repeat;
background-size: contain;
}
.rightwatermark {

140
css/_lobby.scss Normal file
View File

@@ -0,0 +1,140 @@
#lobby-screen {
.content {
.container {
align-items: center;
display: flex;
flex-direction: column;
.spinner {
margin: 30px;
}
.joining-message {
margin: 10px;
}
}
.form {
align-items: stretch;
display: flex;
flex-direction: column;
min-width: 400px;
}
.participant-info {
align-items: center;
display: flex;
flex-direction: column;
}
}
}
#lobby-section {
display: flex;
flex-direction: column;
.control-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
label {
font-size: 14px;
font-weight: bold;
}
}
}
#knocking-participant-list {
background-color: $newToolbarBackgroundColor;
border: 1px solid rgba(255, 255, 255, .4);
border-radius: 8px;
display: flex;
flex-direction: column;
left: 0;
margin: 20px;
position: fixed;
top: 20;
transition: top 1s ease;
z-index: 100;
&.toolbox-visible {
// Same as toolbox subject position
top: 120px;
}
.title {
background-color: rgba(0, 0, 0, .2);
font-size: 1.2em;
padding: 15px
}
ul {
list-style-type: none;
padding: 0 15px 15px 15px;
li {
align-items: center;
display: flex;
flex-direction: row;
margin: 8px 0;
.details {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-evenly;
margin: 0 30px 0 10px;
}
button {
align-self: unset;
margin: 0 5px;
}
}
}
input {
align-self: stretch;
background-color: transparent;
border: 1px solid #B8C7E0;
border-radius: 4px;
color: white;
padding: 12px 8px;
&:focus {
border-color: rgb(3, 118, 218);
}
}
button {
align-self: stretch;
margin: 8px 0;
padding: 12px;
transition: .2s transform ease;
&:disabled {
opacity: .5;
}
&:hover {
transform: scale(1.05);
&:disabled {
transform: none;
}
}
&.borderLess {
background-color: transparent;
border-width: 0;
}
&.primary {
background-color: rgb(3, 118, 218);
border-width: 0;
}
}
}

View File

@@ -1,18 +1,4 @@
.prejoin {
&-full-page {
background: #1C2025;
position: absolute;
width: 100%;
height: 100%;
z-index: $toolbarZ + 1;
}
&-input-area-container {
position: absolute;
bottom: 48px;
width: 100%;
z-index: 2;
}
&-input-area {
margin: 0 auto;
@@ -27,65 +13,6 @@
margin-bottom: 16px;
}
&-btn {
border-radius: 3px;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 24px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 286px;
&--primary {
background: #0376DA;
border: 1px solid #0376DA;
}
&--secondary {
background: #2A3A4B;
border: 1px solid #5E6D7A;
}
&--text {
width: auto;
font-size: 13px;
margin: 0;
padding: 0;
}
&--disabled {
background: #5E6D7A;
border: 1px solid #5E6D7A;
color: #AFB6BC;
cursor: initial;
.prejoin-btn-icon {
& > svg {
fill: #AFB6BC;
}
}
.prejoin-btn-options {
border-left: 1px solid #AFB6BC;
}
}
}
&-btn-options {
align-items: center;
border-left: 1px solid #fff;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
width: 40px;
}
&-text-btns {
display: flex;
justify-content: space-between;
@@ -179,25 +106,6 @@
margin: 200px auto 0 auto;
}
&-btn-container {
display: flex;
justify-content: center;
margin-top: 32px;
width: 100%;
&> div {
margin: 0 12px;
}
.settings-button-small-icon {
right: -8px;
&--hovered {
right: -10px;
}
}
}
&-overlay {
height: 100%;
position: absolute;
@@ -217,22 +125,20 @@
&-status {
align-items: center;
bottom: 0;
align-self: stretch;
color: #fff;
display: flex;
font-size: 13px;
min-height: 24px;
justify-content: center;
position: absolute;
text-align: center;
width: 100%;
z-index: 1;
&--warning {
background: rgba(241, 173, 51, 0.5)
background: rgba(241, 173, 51, 0.7)
}
&--ok {
background: rgba(49, 183, 106, 0.5);
background: rgba(49, 183, 106, 0.7);
}
}
@@ -291,63 +197,3 @@
}
}
.prejoin-copy {
&-meeting {
cursor: pointer;
color: #fff;
font-size: 15px;
font-weight: 300;
line-height: 24px;
position: relative;
}
&-url {
max-width: 278px;
padding: 8px 10px;
overflow: hidden;
text-overflow: ellipsis;
}
&-badge {
border-radius: 4px;
height: 100%;
line-height: 38px;
position: absolute;
padding-left: 10px;
text-align: left;
top: 0;
width: 100%;
&--hover {
background: #1C2025;
}
&--done {
background: #31B76A;
}
}
&-icon {
position: absolute;
right: 8px;
top: 8px;
&--white {
&> svg > path {
fill: #fff
}
}
&--light {
&> svg > path {
fill: #D1DBE8;
}
}
}
&-textarea {
position: absolute;
left: -9999px;
}
}

View File

@@ -0,0 +1,194 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
align-items: stretch;
background: #1C2025;
bottom: 0;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
.content {
align-items: center;
background-image: linear-gradient(transparent, black);
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
z-index: $toolbarZ + 2;
.title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
.copy-meeting {
align-items: center;
cursor: pointer;
color: #fff;
display: flex;
flex-direction: row;
font-size: 15px;
font-weight: 300;
justify-content: center;
line-height: 24px;
.url {
display: flex;
padding: 8px 10px;
&:hover {
background: #1C2025;
border-radius: 4px;
}
&.done {
background: #31B76A;
}
.jitsi-icon {
margin-left: 10px;
}
}
&:hover {
align-self: stretch;
}
textarea {
border-width: 0;
height: 0;
opacity: 0;
padding: 0;
width: 0;
}
}
input.field {
background-color: transparent;
border: 1px solid transparent;
color: white;
outline-width: 0;
padding: 20px;
text-align: center;
&.focused {
border-bottom: 1px solid white;
}
&.error::placeholder {
color: $defaultWarningColor;
}
}
.action-btn {
border-radius: 3px;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 24px;
margin: 10px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 286px;
&.primary {
background: #0376DA;
border: 1px solid #0376DA;
}
&.secondary {
background: transparent;
border: 1px solid #5E6D7A;
}
&.text {
width: auto;
font-size: 13px;
margin: 0;
padding: 0;
}
&.disabled {
background: #5E6D7A;
border: 1px solid #5E6D7A;
color: #AFB6BC;
cursor: initial;
.icon {
& > svg {
fill: #AFB6BC;
}
}
.options {
border-left: 1px solid #AFB6BC;
}
}
.options {
align-items: center;
border-left: 1px solid #fff;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
width: 40px;
}
}
}
.media-btn-container {
display: flex;
justify-content: center;
margin: 32px 0;
width: 100%;
&> div {
margin: 0 12px;
}
.settings-button-small-icon {
right: -8px;
&--hovered {
right: -10px;
}
}
}
}
#preview {
height: 100%;
position: absolute;
width: 100%;
&.no-video {
background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
text-align: center;
}
.avatar {
background: #A4B8D1;
margin: 200px auto 0 auto;
}
video {
height: 100%;
object-fit: cover;
position: absolute;
width: 100%;
}
}

View File

@@ -101,7 +101,6 @@ $sidebarWidth: 375px;
* Misc.
*/
$borderRadius: 4px;
$defaultWatermarkLink: '../images/watermark.png';
$popoverMenuPadding: 13px;
$happySoftwareBackground: transparent;
$desktopAppDragBarHeight: 25px;
@@ -270,4 +269,3 @@ $chromeExtensionBannerTop: 80px;
$chromeExtensionBannerRight: 16px;
$chromeExtensionBannerTopInMeeting: 10px;
$chromeExtensionBannerRightInMeeeting: 10px;

View File

@@ -40,9 +40,6 @@
#remotePresenceMessage {
display: none !important;
}
#largeVideoContainer {
background-color: $defaultBackground !important;
}
/**
* Thumbnail popover menus can overlap other thumbnails. Setting an auto

View File

@@ -76,6 +76,7 @@ $flagsImagePath: "../images/";
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'labels';
@import 'lobby';
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@@ -96,5 +97,6 @@ $flagsImagePath: "../images/";
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'premeeting-screens';
/* Modules END */

View File

@@ -3,25 +3,47 @@
color: #fff;
font-size: 15px;
line-height: 24px;
&.password {
&.password-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
&-actions {
a {
cursor: pointer;
text-decoration: none;
font-size: 14px;
color: #6FB1EA;
}
.password {
align-items: center;
display: flex;
justify-content: space-between;
margin-top: 15px;
& > :first-child:not(:last-child) {
margin-right: 24px;
&-actions {
a {
cursor: pointer;
text-decoration: none;
font-size: 14px;
color: #6FB1EA;
}
&>a+a {
margin-left: 24px;
}
}
}
}
&> :first-child:not(:last-child) {
margin-right: 24px;
}
.separator-line {
margin: 24px 0 24px -20px;
padding: 0 20px;
width: 100%;
height: 1px;
background: #5E6D7A;
&:last-child {
display: none;
}
}
}
}
@@ -34,4 +56,4 @@
background: rgba(241, 173, 51, 0.7);
border: 1px solid rgba(255, 255, 255, 0.4);
}
}
}

46
docker/Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
ARG JITSI_REPO=jitsi
ARG JITSI_GIT_REPO=https://github.com/jitsi/jitsi-meet.git
ARG JITSI_GIT_REF=HEAD
FROM node:12 as builder
ARG JITSI_GIT_REPO
ARG JITSI_GIT_REF
WORKDIR /src
RUN \
git clone $JITSI_GIT_REPO && \
cd jitsi-meet && \
git reset --hard $JITSI_GIT_REF && \
npm install && \
make
FROM ${JITSI_REPO}/base
RUN \
apt-dpkg-wrap apt-get update && \
apt-dpkg-wrap apt-get install -y nginx-extras && \
apt-cleanup && \
rm -f /etc/nginx/conf.d/default.conf
COPY rootfs/ /
COPY --from=builder /src/jitsi-meet/libs /usr/share/jitsi-meet/libs
COPY --from=builder /src/jitsi-meet/static /usr/share/jitsi-meet/static
COPY --from=builder /src/jitsi-meet/sounds /usr/share/jitsi-meet/sounds
COPY --from=builder /src/jitsi-meet/fonts /usr/share/jitsi-meet/fonts
COPY --from=builder /src/jitsi-meet/images /usr/share/jitsi-meet/images
COPY --from=builder /src/jitsi-meet/lang /usr/share/jitsi-meet/lang
COPY --from=builder /src/jitsi-meet/connection_optimization /usr/share/jitsi-meet/connection_optimization
COPY --from=builder /src/jitsi-meet/css/all.css /usr/share/jitsi-meet/css/
COPY --from=builder /src/jitsi-meet/resources/*.sh /usr/share/jitsi-meet/scripts/
COPY --from=builder /src/jitsi-meet/*.html /usr/share/jitsi-meet/
COPY --from=builder /src/jitsi-meet/*.ico /usr/share/jitsi-meet/
COPY --from=builder /src/jitsi-meet/resources/robots.txt /usr/share/jitsi-meet/
ENV XMPP_BOSH_URL_BASE=https://meet.jit.si
ENV XMPP_DOMAIN=meet.jit.si
EXPOSE 8000
VOLUME ["/config"]

View File

@@ -0,0 +1,46 @@
server_name _;
client_max_body_size 0;
root /usr/share/jitsi-meet;
# ssi on with javascript for multidomain variables in config.js
ssi on;
ssi_types application/x-javascript application/javascript;
index index.html index.htm;
error_page 404 /static/404.html;
location = /config.js {
alias /config/config.js;
}
location = /interface_config.js {
alias /config/interface_config.js;
}
location = /external_api.js {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
# ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
}
# BOSH
location = /http-bind {
proxy_pass {{ .Env.XMPP_BOSH_URL_BASE }}/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host {{ .Env.XMPP_DOMAIN }};
}
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}
location @root_path {
rewrite ^/(.*)$ / break;
}

View File

@@ -0,0 +1,60 @@
user www-data;
worker_processes 4;
pid /run/nginx.pid;
daemon off;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
client_max_body_size 0;
include /etc/nginx/mime.types;
types {
# add support for wasm MIME type, that is required by specification and it is not part of default mime.types file
application/wasm wasm;
}
default_type application/octet-stream;
##
# Logging Settings
##
access_log /dev/stdout;
error_log /dev/stderr;
##
# Gzip Settings
##
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_vary on;
gzip_min_length 860;
##
## The Sever
##
server {
listen 8000 default_server;
include /config/nginx/meet.conf;
}
}

View File

@@ -0,0 +1,12 @@
#!/usr/bin/with-contenv bash
# make our folders
mkdir -p \
/config/nginx \
/run \
/var/lib/nginx/tmp/client_body \
/var/tmp/nginx
# copy config files
cp /defaults/nginx.conf /config/nginx/nginx.conf
tpl /defaults/meet.conf > /config/nginx/meet.conf

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
exec nginx -c /config/nginx/nginx.conf

View File

@@ -1,9 +1,8 @@
/* eslint-disable no-unused-vars, no-var, max-len */
var interfaceConfig = {
// TO FIX: this needs to be handled from SASS variables. There are some
// methods allowing to use variables both in css and js.
DEFAULT_BACKGROUND: '#474747',
DEFAULT_LOGO_URL: '../images/watermark.png',
/**
* Whether or not the blurred video background for large video should be

38
lang/languages-mr.json Normal file
View File

@@ -0,0 +1,38 @@
{
"en": "English",
"af": "Afrikaans",
"az": "",
"bg": "Bulgarian",
"cs": "Czech",
"de": "German",
"el": "Greek",
"eo": "Esperanto",
"es": "Spanish",
"fr": "French",
"hy": "Armenian",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"nb": "",
"oc": "Occitan",
"pl": "Polish",
"ptBR": "Portuguese (Brazil)",
"ru": "Russian",
"sk": "",
"sl": "",
"sv": "Swedish",
"tr": "Turkish",
"vi": "Vietnamese",
"zhCN": "Chinese (China)",
"zhTW": "Chinese (Taiwan)",
"nl": "Dutch",
"hu": "Hungarian",
"hr": "Croatian",
"frCA": "French (Canadian)",
"fi": "Finnish",
"et": "Estonian",
"esUS": "Spanish (Latin America)",
"enGB": "English (United Kingdom)",
"da": "Danish",
"ca": "Catalan"
}

View File

@@ -18,6 +18,7 @@
"fr": "French",
"frCA": "French (Canadian)",
"he": "Hebrew",
"mr":"Marathi",
"hr": "Croatian",
"hu": "Hungarian",
"hy": "Armenian",

View File

@@ -679,7 +679,6 @@
"videomute": "Démarrer / Arrêter la caméra",
"startvideoblur": "Flouter mon arrière plan",
"stopvideoblur": "Désactiver le flou d'arrière-plan",
"muteEveryone": "Mettre tout le monde en sourdine",
"noAudioSignalDialInDesc": "Vous pouvez également composer un numéro en utilisant :",
"noAudioSignalDialInLinkDesc": "Numéros d'appel"
},

817
lang/main-mr.json Normal file
View File

@@ -0,0 +1,817 @@
{
"addPeople": {
"add": "आमंत्रित करा",
"countryNotSupported": "आम्ही अद्याप या गंतव्याचे समर्थन करत नाही.",
"countryReminder": "यूएस बाहेर कॉल करीत आहे? कृपया आपण देशाच्या कोडसह प्रारंभ केल्याचे सुनिश्चित करा!",
"disabled": "आपण लोकांना आमंत्रित करू शकत नाही.",
"failedToAdd": "सहभागी जोडण्यात अयशस्वी",
"footerText": "डायल आउट करणे अक्षम केले आहे.",
"loading": "लोक आणि फोन नंबर शोधत आहे",
"loadingNumber": "फोन नंबर सत्यापित करीत आहे",
"loadingPeople": "लोकांना आमंत्रित करण्यासाठी शोधत आहे",
"noResults": "कोणतेही जुळणारे शोध परिणाम नाहीत",
"noValidNumbers": "कृपया एक फोन नंबर प्रविष्ट करा",
"searchNumbers": "फोन नंबर जोडा",
"searchPeople": "लोकांचा शोध घ्या",
"searchPeopleAndNumbers": "लोक शोधा किंवा त्यांचा फोन नंबर जोडा",
"telephone": "दूरध्वनी: {{number}}",
"title": "या संमेलनात लोकांना आमंत्रित करा"
},
"audioDevices": {
"bluetooth": "ब्लूटुथ",
"headphones": "हेडफोन",
"phone": "फोन",
"speaker": "स्पीकर",
"none": "कोणतेही ऑडिओ डिव्हाइस उपलब्ध नाहीत"
},
"audioOnly": {
"audioOnly": "कमी बँडविड्थ"
},
"calendarSync": {
"addMeetingURL": "मीटिंगचा दुवा जोडा",
"confirmAddLink": "आपण या कार्यक्रमास एक Jitsi दुवा जोडू इच्छिता?",
"error": {
"appConfiguration": "कॅलेंडर समाकलन योग्यरित्या कॉन्फिगर केलेले नाही.",
"generic": "त्रुटी आढळली आहे. कृपया आपल्या कॅलेंडर सेटिंग्ज तपासा किंवा कॅलेंडर रीफ्रेश करा.",
"notSignedIn": "कॅलेंडर इव्हेंट पाहण्यासाठी प्रमाणीकरण करताना त्रुटी आली. कृपया आपल्या कॅलेंडर सेटिंग्ज तपासा आणि पुन्हा लॉग इन करण्याचा प्रयत्न करा."
},
"join": "सामील व्हा",
"joinTooltip": "बैठकीत सामील व्हा",
"nextMeeting": "पुढील बैठक",
"noEvents": "कोणतेही आगामी कार्यक्रम शेड्यूल केलेले नाहीत.",
"ongoingMeeting": "चालू बैठक",
"permissionButton": "सेटिंग्ज उघडा",
"permissionMessage": "अ‍ॅपमधील आपली संमेलने पहाण्यासाठी कॅलेंडर परवानगी आवश्यक आहे.",
"refresh": "रीफ्रेश कॅलेंडर",
"today": "आज"
},
"chat": {
"error": "Error: तुमचा संदेश पाठविला गेला नाही. कारणः {{error}}",
"fieldPlaceHolder": "आपला संदेश येथे टाइप करा",
"messagebox": "एक संदेश टाइप करा",
"messageTo": "यांना खाजगी संदेश {{recipient}}",
"noMessagesMessage": "अद्याप मीटिंगमध्ये कोणतेही संदेश नाहीत. येथे संभाषण सुरू करा!",
"nickname": {
"popover": "टोपणनाव निवडा",
"title": "चॅट वापरण्यासाठी टोपणनाव प्रविष्ट करा"
},
"privateNotice": "यांना खाजगी संदेश{{recipient}}",
"title": "गप्पा",
"you": "आपण"
},
"chromeExtensionBanner": {
"installExtensionText": "Google कॅलेंडर आणि ऑफिस 365 एकत्रिकरणासाठी विस्तार स्थापित करा",
"buttonText": "Chrome विस्तार स्थापित करा",
"dontShowAgain": "मला हे पुन्हा दर्शवू नका"
},
"connectingOverlay": {
"joiningRoom": "आपल्याला आपल्या संमेलनात कनेक्ट करीत आहे ..."
},
"connection": {
"ATTACHED": "जोडले",
"AUTHENTICATING": "प्रमाणीकरण करीत आहे",
"AUTHFAIL": "प्रमाणीकरण अयशस्वी",
"CONNECTED": "जोडलेले",
"CONNECTING": "कनेक्ट करीत आहे",
"CONNFAIL": "संपर्क खंडित",
"DISCONNECTED": "डिस्कनेक्ट केलेले",
"DISCONNECTING": "डिस्कनेक्ट करत आहे",
"ERROR": "त्रुटी",
"FETCH_SESSION_ID": "सत्र आयडी प्राप्त करीत आहे ...",
"GET_SESSION_ID_ERROR": "सत्र-आयडी त्रुटी मिळवा:{{code}}",
"GOT_SESSION_ID": "सत्र-आयडी मिळवित आहे ... पूर्ण झाले",
"LOW_BANDWIDTH": "बँडविड्थ जतन करण्यासाठी {{displayName}}चा व्हिडिओ बंद केला गेला आहे"
},
"connectionindicator": {
"address": "पत्ता:",
"bandwidth": "अंदाजे बँडविड्थ:",
"bitrate": "बिटरेट:",
"bridgeCount": "सर्व्हर संख्या:",
"connectedTo": "यांना जोडलेले:",
"e2e_rtt": "E2E आरटीटी:",
"framerate": "फ्रेम दर:",
"less": "कमी दाखवा",
"localaddress": "स्थानिक पत्ता:",
"localaddress_plural": "स्थानिक पत्ते:",
"localport": "स्थानिक बंदर:",
"localport_plural": "स्थानिक बंदरे:",
"more": "अजून दाखवा",
"packetloss": "पॅकेट तोटा",
"quality": {
"good": "चांगले",
"inactive": "निष्क्रिय",
"lost": "हरवले",
"nonoptimal": "नॉनओप्टिमल",
"poor": "गरीब"
},
"remoteaddress": "दूरस्थ पत्ता:",
"remoteaddress_plural": "दूरस्थ पत्ते:",
"remoteport": "रिमोट पोर्ट:",
"remoteport_plural": "दूरस्थ बंदरे:",
"resolution": "ठराव:",
"status": "कनेक्शन:",
"transport": "वाहतूक:",
"transport_plural": "परिवहन:"
},
"dateUtils": {
"earlier": "यापूर्वी",
"today": "आज",
"yesterday": "काल"
},
"deepLinking": {
"appNotInstalled": "आपल्या फोनवर या संमेलनात सामील होण्यासाठी आपल्यास {{app}}मोबाइल अॅप आवश्यक आहे.",
"description": "काहीच घडलं नाही? आम्ही आपली बैठक {{app}} डेस्कटॉप अॅपमध्ये लाँच करण्याचा प्रयत्न केला. पुन्हा प्रयत्न करा किंवा {{app}} वेब अ‍ॅपमध्ये लाँच करा",
"descriptionWithoutWeb": "काहीच घडलं नाही? आम्ही आपली बैठक {{app}} डेस्कटॉप अॅपमध्ये लाँच करण्याचा प्रयत्न केला.",
"downloadApp": "अ‍ॅप डाउनलोड करा",
"launchWebButton": "वेबमध्ये लाँच करा",
"openApp": "अ‍ॅप वर सुरू ठेवा",
"title": " {{app}}मध्ये आपली बैठक सुरू करत आहे",
"tryAgainButton": "डेस्कटॉपवर पुन्हा प्रयत्न करा"
},
"defaultLink": "उदा. {{url}}",
"defaultNickname": " उदा. जेन गुलाबी",
"deviceError": {
"cameraError": "आपल्या कॅमेर्‍यावर प्रवेश करण्यात अयशस्वी",
"cameraPermission": "कॅमेर्‍याची परवानगी घेण्यात त्रुटी",
"microphoneError": "आपल्या मायक्रोफोनवर प्रवेश करण्यात अयशस्वी",
"microphonePermission": "मायक्रोफोन परवानगी प्राप्त करताना त्रुटी"
},
"deviceSelection": {
"noPermission": "परवानगी दिली नाही",
"previewUnavailable": "पूर्वावलोकन अनुपलब्ध",
"selectADevice": "एक डिव्हाइस निवडा",
"testAudio": "चाचणी आवाज प्ले करा"
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": "थेट प्रसारण"
},
"allow": "परवानगी द्या",
"alreadySharedVideoMsg": "दुसरा सहभागी आधीपासूनच व्हिडिओ सामायिक करीत आहे. ही परिषद एका वेळी फक्त एकच सामायिक व्हिडिओ परवानगी देते.",
"alreadySharedVideoTitle": "एकावेळी फक्त सामायिक केलेला व्हिडिओ अनुमत आहे",
"applicationWindow": "अनुप्रयोग विंडो",
"Back": "Back",
"cameraConstraintFailedError": "आपला कॅमेरा काही आवश्यक मर्यादा पूर्ण करीत नाही.",
"cameraNotFoundError": "कॅमेरा आढळला नाही.",
"cameraNotSendingData": "आम्ही आपल्या कॅमेर्‍यावर प्रवेश करण्यात अक्षम आहोत. कृपया एखादा दुसरा अनुप्रयोग हे डिव्हाइस वापरत आहे की नाही ते तपासा, सेटिंग्ज मेनूमधून दुसरे डिव्हाइस निवडा किंवा अनुप्रयोग रीलोड करण्याचा प्रयत्न करा.",
"cameraNotSendingDataTitle": "कॅमेर्‍यावर प्रवेश करण्यात अक्षम",
"cameraPermissionDeniedError": "आपल्याला आपला कॅमेरा वापरण्याची परवानगी नाही. आपण अद्याप परिषदेत सामील होऊ शकता परंतु इतर आपल्याला पाहणार नाहीत. हे निश्चित करण्यासाठी अ‍ॅड्रेस बारमधील कॅमेरा बटण वापरा.",
"cameraUnknownError": "अज्ञात कारणासाठी कॅमेरा वापरू शकत नाही.",
"cameraUnsupportedResolutionError": "आपला कॅमेरा आवश्यक व्हिडिओ रिझोल्यूशनला समर्थन देत नाही.",
"Cancel": "रद्द करा",
"close": "बंद",
"conferenceDisconnectMsg": "आपण आपले नेटवर्क कनेक्शन तपासू शकता. सेकंदात पुन्हा कनेक्ट करत आहे {{seconds}}..",
"conferenceDisconnectTitle": "आपण डिस्कनेक्ट झाला आहात.",
"conferenceReloadMsg": "आम्ही हे निश्चित करण्याचा प्रयत्न करीत आहोत. पुन्हा कनेक्ट करत आहे. {{seconds}} sec...",
"conferenceReloadTitle": "दुर्दैवाने, काहीतरी चूक झाली",
"confirm": "पुष्टी",
"confirmNo": "नाही",
"confirmYes": "होय",
"connectError": "अरेरे! काहीतरी चूक झाली आणि आम्ही परिषदेत कनेक्ट होऊ शकलो नाही.",
"connectErrorWithMsg": "अरेरे! काहीतरी चूक झाली आणि आम्ही परिषदेत कनेक्ट होऊ शकलो नाही:{{msg}}",
"connecting": "कनेक्ट करीत आहे",
"contactSupport": "समर्थन संपर्क",
"copy": "Copy",
"dismiss": "Dismiss",
"displayNameRequired": "हाय! तुझे नाव काय आहे?",
"done": "पूर्ण झाले",
"e2eeDescription": "<p>एंड-टू-एंड एनक्रिप्शन सध्या आहे <strong>प्रायोगिक</strong>. कृपया पहा <a href='https://jitsi.org/blog/e2ee/' target='_blank'>this post</a>तपशीलांसाठी.</p><br/><p>कृपया लक्षात ठेवा की एंड-टू-एंड एन्क्रिप्शन चालू केल्याने सर्व्हर-साइड प्रदान सेवा प्रभावीपणे अक्षम होईल: रेकॉर्डिंग, थेट प्रवाह आणि फोन सहभाग. हे देखील लक्षात ठेवा की मीटिंग केवळ समाविष्ट करण्यायोग्य प्रवाहांसाठी समर्थन असलेल्या ब्राउझरमधून सामील झालेल्या लोकांसाठीच कार्य करेल.</p>",
"e2eeLabel": "Key",
"e2eeTitle": "एंड-टू-एंड एनक्रिप्शन",
"e2eeWarning": "<br /><p><strong>चेतावणी:</strong>या बैठकीतील सर्व सहभागींना एंड-टू-एंड एनक्रिप्शनसाठी समर्थन असल्याचे दिसत नाही. आपण सक्षम केल्यास ते आपल्याला पाहण्यास किंवा ऐकण्यास सक्षम राहणार नाहीत.</p>",
"enterDisplayName": "कृपया आपले नाव येथे प्रविष्ट करा",
"error": "त्रुटी",
"externalInstallationMsg": "आपल्याला आमचा डेस्कटॉप सामायिकरण विस्तार स्थापित करणे आवश्यक आहे.",
"externalInstallationTitle": "विस्तार आवश्यक",
"goToStore": "वेब स्टोअरवर जा",
"gracefulShutdown": "आमची सेवा सध्या देखभालीसाठी बंद आहे. कृपया पुन्हा प्रयत्न करा.",
"IamHost": "मी यजमान आहे",
"incorrectRoomLockPassword": "चुकीचा संकेतशब्द",
"incorrectPassword": "वापरकर्त्याचे नाव अथवा पासवर्ड चुकीचा",
"inlineInstallationMsg": "आपल्याला आमचा डेस्कटॉप सामायिकरण विस्तार स्थापित करणे आवश्यक आहे.",
"inlineInstallExtension": "स्थापित करा",
"internalError": "अरेरे! काहीतरी चूक झाली. पुढील त्रुटी आली:{{error}}",
"internalErrorTitle": "अंतर्गत त्रुटी",
"kickMessage": "अधिक तपशीलांसाठी आपण {{participantDisplayName}} वर संपर्क साधू शकता.",
"kickParticipantButton": "लाथ मारा",
"kickParticipantDialog": "आपणास खात्री आहे की आपण या सहभागीस लाथ मारायची आहे?",
"kickParticipantTitle": "या सहभागीला बाहेर करा?",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"liveStreaming": "थेट प्रवाह",
"liveStreamingDisabledForGuestTooltip": "अतिथी थेट प्रवाह सुरू करू शकत नाहीत.",
"liveStreamingDisabledTooltip": "थेट प्रवाह अक्षम करा.",
"lockMessage": "परिषद लॉक करण्यात अयशस्वी.",
"lockRoom": "मीटिंग जोडा $t(lockRoomPasswordUppercase)",
"lockTitle": "लॉक अयशस्वी",
"logoutQuestion": "आपणास खात्री आहे की आपण लॉगआउट आणि परिषद थांबवू इच्छिता?",
"logoutTitle": "बाहेर पडणे",
"maxUsersLimitReached": "जास्तीत जास्त सहभागी होण्याची मर्यादा गाठली आहे. परिषद भरली आहे. कृपया मीटिंग मालकाशी संपर्क साधा किंवा नंतर पुन्हा प्रयत्न करा!",
"maxUsersLimitReachedTitle": "जास्तीत जास्त सहभागींची मर्यादा गाठली",
"micConstraintFailedError": "आपला मायक्रोफोन आवश्यक असलेल्या काही मर्यादा पूर्ण करीत नाही.",
"micNotFoundError": "मायक्रोफोन सापडला नाही.",
"micNotSendingData": "आपला माईक सशब्द करण्यासाठी आणि त्याचा स्तर समायोजित करण्यासाठी आपल्या संगणकाच्या सेटिंग्जवर जा",
"micNotSendingDataTitle": "आपले माइक आपल्या सिस्टम सेटिंग्जद्वारे निःशब्द केले आहे",
"micPermissionDeniedError": "आपल्याला आपला मायक्रोफोन वापरण्याची परवानगी नाही. आपण अद्याप परिषदेत सामील होऊ शकता परंतु इतर आपले ऐकणार नाहीत. हे निश्चित करण्यासाठी अ‍ॅड्रेस बारमधील कॅमेरा बटण वापरा.",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"muteEveryoneElseDialog": "एकदा नि: शब्द झाल्यास आपण त्यांना ध्वनीमुद्रित करण्यास सक्षम राहणार नाही परंतु ते कधीही स्वत: ला सशब्द करू शकतात.",
"muteEveryoneElseTitle": "सोडून सर्वांना नि: शब्द करा{{whom}}?",
"muteEveryoneDialog": "आपली खात्री आहे की आपण प्रत्येकाला निःशब्द करू इच्छिता? आपण त्यांना सशब्द करण्यास सक्षम राहणार नाही परंतु ते कधीही स्वत: ला सशब्द करू शकतात.",
"muteEveryoneTitle": "सर्वांना नि: शब्द करा?",
"muteEveryoneSelf": "तू स्वतः",
"muteEveryoneStartMuted": "आतापासून प्रत्येकजण निःशब्द होऊ लागतो",
"muteParticipantBody": "आपण त्यांना सशब्द करण्यास सक्षम राहणार नाही परंतु ते कधीही स्वत: ला सशब्द करू शकतात.",
"muteParticipantButton": "नि: शब्द करा",
"muteParticipantDialog": "आपली खात्री आहे की आपण या सहभागीस नि: शब्द करू इच्छिता? आपण त्यांना सशब्द करण्यास सक्षम राहणार नाही परंतु ते कधीही स्वत: ला सशब्द करू शकतात.",
"muteParticipantTitle": "हा सहभागी नि: शब्द करायचा?",
"Ok": "Ok",
"passwordLabel": "संमेलनास एका सहभागीने लॉक केले आहे. कृपया सामील होण्यासाठी $t(lockRoomPassword) प्रविष्ट करा.",
"passwordNotSupported": "मीटिंग सेट करणे $t(lockRoomPassword) समर्थित नाही..",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) समर्थित नाही",
"passwordRequired": "$t(lockRoomPasswordUppercase)आवश्यक",
"popupError": "आपला ब्राउझर या साइटवरील पॉप-अप विंडोज अवरोधित करत आहे. कृपया आपल्या ब्राउझरच्या सुरक्षा सेटिंग्जमध्ये पॉप-अप सक्षम करा आणि पुन्हा प्रयत्न करा.",
"popupErrorTitle": "पॉप-अप अवरोधित",
"recording": "मुद्रित करणे",
"recordingDisabledForGuestTooltip": "अतिथी रेकॉर्डिंग प्रारंभ करू शकत नाहीत.",
"recordingDisabledTooltip": "रेकॉर्डिंग प्रारंभ अक्षम.",
"rejoinNow": "आता पुन्हा सामील व्हा",
"remoteControlAllowedMessage": "{{user}} आपली रिमोट कंट्रोल विनंती मान्य केली!",
"remoteControlDeniedMessage": "{{user}} आपली रिमोट कंट्रोल विनंती नाकारली!",
"remoteControlErrorMessage": "वरून रिमोट कंट्रोल परवानग्यांची विनंती करण्याचा प्रयत्न करताना एक त्रुटी आली {{user}}!",
"remoteControlRequestMessage": "आपण{{user}} ला डेस्कटॉप दूरस्थपणे नियंत्रित करण्यास अनुमती द्याल?",
"remoteControlShareScreenWarning": "लक्षात ठेवा की आपण \"Allow\" दाबल्यास आपण आपली स्क्रीन सामायिक कराल!",
"remoteControlStopMessage": "रिमोट कंट्रोल सत्र संपले!",
"remoteControlTitle": "दूरस्थ डेस्कटॉप नियंत्रण",
"Remove": "काढा",
"removePassword": "काढा $t(lockRoomPassword)",
"removeSharedVideoMsg": " आपली खात्री आहे की आपण आपला सामायिक केलेला व्हिडिओ काढू इच्छिता?",
"removeSharedVideoTitle": "सामायिक केलेला व्हिडिओ काढा",
"reservationError": "आरक्षण प्रणाली त्रुटी",
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "पुन्हा प्रयत्न करा",
"screenSharingAudio": "ऑडिओ सामायिक करा",
"screenSharingFailedToInstall": "अरेरे! आपला स्क्रीन सामायिकरण विस्तार स्थापित करण्यात अयशस्वी.",
"screenSharingFailedToInstallTitle": "स्क्रीन सामायिकरण विस्तार स्थापित करण्यात अयशस्वी",
"screenSharingFirefoxPermissionDeniedError": "आम्ही आपली स्क्रीन सामायिक करण्याचा प्रयत्न करीत असताना काहीतरी चुकीचे झाले. कृपया याची खात्री करा की आपण आम्हाला तसे करण्यास परवानगी दिली आहे.",
"screenSharingFirefoxPermissionDeniedTitle": "अरेरे! आम्ही स्क्रीन सामायिकरण प्रारंभ करण्यास सक्षम नाही!",
"screenSharingPermissionDeniedError": "अरेरे! आपल्या स्क्रीन सामायिकरण विस्तार परवानग्यांसह काहीतरी चूक झाली. कृपया रीलोड करा आणि पुन्हा प्रयत्न करा.",
"sendPrivateMessage": "आपल्याला अलीकडे एक खाजगी संदेश प्राप्त झाला आहे. त्यास खाजगीरित्या उत्तर देण्याचा आपला हेतू होता की आपण आपला संदेश गटाला पाठवू इच्छिता?",
"sendPrivateMessageCancel": "गटाला पाठवा",
"sendPrivateMessageOk": "खाजगी पाठवा",
"sendPrivateMessageTitle": "खाजगी पाठवायचे?",
"serviceUnavailable": "सेवा अनुपलब्ध",
"sessTerminated": "कॉल संपुष्टात आला",
"Share": "सामायिक करा",
"shareVideoLinkError": "कृपया योग्य YouTube दुवा प्रदान करा.",
"shareVideoTitle": "एक व्हिडिओ सामायिक करा",
"shareYourScreen": "आपली स्क्रीन सामायिक करा",
"shareYourScreenDisabled": "स्क्रीन सामायिकरण अक्षम केले.",
"shareYourScreenDisabledForGuest": "अतिथी स्क्रीन सामायिकरण करू शकत नाहीत.",
"startLiveStreaming": "थेट प्रवाह सुरू करा",
"startRecording": "रेकॉर्डिंग प्रारंभ करा",
"startRemoteControlErrorMessage": "रिमोट कंट्रोल सत्र सुरू करण्याचा प्रयत्न करताना एक त्रुटी आली!",
"stopLiveStreaming": "थेट प्रवाह थांबवा",
"stopRecording": "रेकॉर्डिंग थांबवा",
"stopRecordingWarning": "आपली खात्री आहे की आपण रेकॉर्डिंग थांबवू इच्छिता?",
"stopStreamingWarning": "आपणास खात्री आहे की आपण थेट प्रवाह थांबवू इच्छिता?",
"streamKey": "थेट प्रवाह की",
"Submit": "प्रस्तुत करणे",
"thankYou": "वापरल्याबद्दल धन्यवाद {{appName}}!",
"token": "टोकन",
"tokenAuthFailed": "क्षमस्व, आपणास या कॉलमध्ये सामील होण्याची परवानगी नाही.",
"tokenAuthFailedTitle": "प्रमाणीकरण अयशस्वी",
"transcribing": "लिप्यंतरण",
"unlockRoom": "मीटिंग काढा $t(lockRoomPassword)",
"userPassword": "user password",
"WaitForHostMsg": "परिषद <b>{{room}}</b>अद्याप सुरू झाले नाही. आपण होस्ट असल्यास कृपया अधिकृत करा. अन्यथा, कृपया होस्ट येण्याची प्रतीक्षा करा.",
"WaitForHostMsgWOk": "परिषद <b>{{room}}</b> अद्याप सुरू झाले नाही. आपण होस्ट असल्यास कृपया प्रमाणीकरणासाठी ओके दाबा. अन्यथा, कृपया होस्ट येण्याची प्रतीक्षा करा.",
"WaitingForHost": " होस्टची प्रतीक्षा करीत आहे ...",
"Yes": "होय",
"yourEntireScreen": "आपली संपूर्ण स्क्रीन"
},
"dialOut": {
"statusMessage": "आता आहे {{status}}"
},
"documentSharing": {
"title": "सामायिक दस्तऐवज"
},
"e2ee": {
"labelToolTip": "या बैठकीतील सर्व सहभागींनी एंड-टू-एंड एनक्रिप्शन सक्षम केले आहे"
},
"feedback": {
"average": "सरासरी",
"bad": "वाईट",
"detailsLabel": " त्याबद्दल आम्हाला सांगा.",
"good": "चांगले",
"rateExperience": "आपल्या भेटीचा अनुभव रेट करा",
"veryBad": "फार वाईट",
"veryGood": "खुप छान"
},
"incomingCall": {
"answer": "उत्तर",
"audioCallTitle": " कॉल येत आहे",
"decline": "काढून टाकणे",
"productLabel": "Jitsi Meet पासून",
"videoCallTitle": "येणारा व्हिडिओ कॉल"
},
"info": {
"accessibilityLabel": "माहिती दर्शवा",
"addPassword": "जोडा $t(lockRoomPassword)",
"cancelPassword": " रद्द करा $t(lockRoomPassword)",
"conferenceURL": "दुवा:",
"country": "देश",
"dialANumber": "आपल्या संमेलनात सामील होण्यासाठी, यापैकी एक क्रमांक डायल करा आणि नंतर पिन प्रविष्ट करा.",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "क्षमस्व, सध्या डायल करणे समर्थित नाही.",
"dialInNumber": "डायल-इन:",
"dialInSummaryError": "आता डायल-इन माहिती आणताना त्रुटी. कृपया पुन्हा प्रयत्न करा.",
"dialInTollFree": "कर मुक्त",
"genericError": "अरेरे, काहीतरी चूक झाली.",
"inviteLiveStream": "या सभेचा थेट प्रवाह पाहण्यासाठी, या दुव्यावर क्लिक करा: {{url}}",
"invitePhone": "त्याऐवजी फोनद्वारे सामील होण्यासाठी, हे टॅप करा:{{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "वेगळा डायल-इन नंबर शोधत आहात? \nमीटिंग डायल-इन नंबर पहा: {{url}}\n\n\nIf also dialing-in through a room phone, join without connecting to audio: {{silentUrl}}",
"inviteURLFirstPartGeneral": " आपल्याला बैठकीत सामील होण्यासाठी आमंत्रित केले आहे.",
"inviteURLFirstPartPersonal": "{{name}} आपल्याला मीटिंगसाठी आमंत्रित करीत आहे.\n",
"inviteURLSecondPart": "\nसभेमध्ये सामील व्हा:\n{{url}}\n",
"liveStreamURL": "थेट प्रसारण:",
"moreNumbers": "अधिक संख्या",
"noNumbers": "डायल-इन नंबर नाहीत.",
"noPassword": "काहीही नाही",
"noRoom": "डायल-इन करण्यासाठी कोणतीही खोली निर्दिष्ट केलेली नाही.",
"numbers": "डायल-इन क्रमांक",
"password": "$t(lockRoomPasswordUppercase):",
"title": "सामायिक करा",
"tooltip": "या संमेलनासाठी दुवा आणि डायल-इन माहिती सामायिक करा",
"label": "संमेलनाची माहिती"
},
"inviteDialog": {
"alertText": "काही सहभागींना आमंत्रित करण्यात अयशस्वी.",
"header": "आमंत्रित करा",
"searchCallOnlyPlaceholder": " फोन नंबर प्रविष्ट करा",
"searchPeopleOnlyPlaceholder": "सहभागींचा शोध घ्या",
"searchPlaceholder": "सहभागी किंवा फोन नंबर",
"send": "पाठवा"
},
"inlineDialogFailure": {
"msg": "आम्ही जरा अडखळलो.",
"retry": "पुन्हा प्रयत्न करा",
"support": "आधार",
"supportMsg": "हे असेच होत राहिल्यास संपर्क साधा"
},
"keyboardShortcuts": {
"focusLocal": "आपल्या व्हिडिओवर लक्ष द्या",
"focusRemote": "दुसर्‍या व्यक्तीच्या व्हिडिओवर लक्ष द्या",
"fullScreen": "पूर्ण स्क्रीन पहा किंवा बाहेर पडा",
"keyboardShortcuts": "कीबोर्ड शॉर्टकट",
"localRecording": "स्थानिक रेकॉर्डिंग नियंत्रणे दर्शवा किंवा लपवा",
"mute": "आपला मायक्रोफोन नि: शब्द करा किंवा सशब्द करा",
"pushToTalk": "बोलण्यासाठी दाबा",
"raiseHand": "आपला हात वर करा किंवा कमी करा",
"showSpeakerStats": "स्पीकरची आकडेवारी दर्शवा",
"toggleChat": "गप्पा उघडा किंवा बंद करा",
"toggleFilmstrip": "व्हिडिओ लघुप्रतिमा दर्शवा किंवा लपवा",
"toggleScreensharing": "कॅमेरा आणि स्क्रीन सामायिकरण दरम्यान स्विच करा",
"toggleShortcuts": "कीबोर्ड शॉर्टकट दर्शवा किंवा लपवा",
"videoMute": "आपला कॅमेरा प्रारंभ करा किंवा थांबवा",
"videoQuality": "कॉल गुणवत्ता व्यवस्थापित करा"
},
"liveStreaming": {
"busy": " आम्ही प्रवाह स्त्रोत मुक्त करण्याचे कार्य करीत आहोत. कृपया काही मिनिटांत पुन्हा प्रयत्न करा.",
"busyTitle": "सर्व स्ट्रीमर सध्या व्यस्त आहेत",
"changeSignIn": "खाती स्विच करा.",
"choose": "थेट प्रवाह निवडा",
"chooseCTA": "प्रवाह पर्याय निवडा. आपण सध्या म्हणून लॉग इन आहात {{email}}.",
"enterStreamKey": " येथे आपली YouTube थेट प्रवाह की प्रविष्ट करा.",
"error": "थेट प्रवाह अयशस्वी. कृपया पुन्हा प्रयत्न करा.",
"errorAPI": "आपल्या YouTube प्रसारणामध्ये प्रवेश करताना त्रुटी आली. कृपया पुन्हा लॉग इन करून पहा.",
"errorLiveStreamNotEnabled": "{{email}}. वर थेट प्रवाह सक्षम केलेले नाही. कृपया थेट प्रवाह सक्षम करा किंवा थेट प्रवाह सक्षम केलेल्या खात्यात लॉग इन करा",
"expandedOff": "थेट प्रवाह थांबला आहे",
"expandedOn": "या संमेलनाचे सध्या युट्यूबवर प्रसारण केले जात आहे.",
"expandedPending": "थेट प्रवाह सुरू केला जात आहे ",
"failedToStart": "थेट प्रवाह सुरू करण्यात अयशस्वी",
"getStreamKeyManually": "आम्ही कोणतेही थेट प्रवाह आणण्यात सक्षम नाही. YouTube वरून आपली थेट प्रवाह की मिळवण्याचा प्रयत्न करा.",
"invalidStreamKey": "थेट प्रवाह की चुकीची असू शकते.",
"off": "थेट प्रवाह थांबला",
"offBy": "{{name}} थेट प्रवाह थांबविला",
"on": "थेट प्रवाह",
"onBy": "{{name}} थेट प्रवाह सुरू केला",
"pending": "थेट प्रवाह सुरू करत आहे ...",
"serviceName": "थेट प्रवाह सेवा",
"signedInAs": "आपण सध्या म्हणून साइन इन केले आहे:",
"signIn": "Google सह साइन इन करा",
"signInCTA": "YouTube वरून साइन इन करा किंवा आपली थेट प्रवाह की प्रविष्ट करा.",
"signOut": "साइन आउट करा",
"start": "थेट प्रवाह सुरू करा",
"streamIdHelp": "हे काय आहे?",
"unavailableTitle": "थेट प्रवाह अनुपलब्ध",
"youtubeTerms": "YouTube सेवा अटी",
"googlePrivacyPolicy": "Google गोपनीयता धोरण"
},
"localRecording": {
"clientState": {
"off": "बंद",
"on": "चालू",
"unknown": "अज्ञात"
},
"dialogTitle": "स्थानिक रेकॉर्डिंग नियंत्रणे",
"duration": "कालावधी",
"durationNA": "N/A",
"encoding": "एन्कोडिंग",
"label": "LOR",
"labelToolTip": "स्थानिक रेकॉर्डिंग गुंतलेली आहे",
"localRecording": "स्थानिक रेकॉर्डिंग",
"me": "Me",
"messages": {
"engaged": "स्थानिक रेकॉर्डिंग व्यस्त",
"finished": " रेकॉर्डिंग सत्र {{token}} . समाप्त. कृपया रेकॉर्ड केलेली फाईल नियंत्रकावर पाठवा.",
"finishedModerator": "रेकॉर्डिंग सत्र {{token}}. समाप्त. लोकल ट्रॅकचे रेकॉर्डिंग सेव्ह केले गेले आहे. कृपया इतर सहभागींना त्यांचे रेकॉर्डिंग सबमिट करण्यास सांगा.",
"notModerator": "आपण नियंत्रक नाही. आपण स्थानिक रेकॉर्डिंग प्रारंभ करू किंवा थांबवू शकत नाही."
},
"moderator": "नियंत्रक",
"no": "No",
"participant": "नियंत्रक",
"participantStats": "सहभागी आकडेवारी",
"sessionToken": "सत्र टोकन",
"start": "रेकॉर्डिंग प्रारंभ करा",
"stop": "रेकॉर्डिंग थांबवा",
"yes": "होय"
},
"lockRoomPassword": "संकेतशब्द",
"lockRoomPasswordUppercase": "संकेतशब्द",
"me": "मी",
"notify": {
"connectedOneMember": "{{name}} बैठकीत सामील झाले",
"connectedThreePlusMembers": "{{name}} आणि {{count}} इतर बैठकीत सामील झाले",
"connectedTwoMembers": "{{first}} आणि {{second}} बैठकीत सामील झाले",
"disconnected": "डिस्कनेक्ट झाले",
"focus": "परिषद लक्ष",
"focusFail": "{{component}} उपलब्ध नाही - पुन्हा प्रयत्न करा {{ms}} सेकंद",
"grantedTo": "नियंत्रक अधिकार यांना दिले {{to}}!",
"invitedOneMember": "{{name}}आमंत्रित केले गेले आहे",
"invitedThreePlusMembers": "{{name}} आणि {{count}} इतरांना आमंत्रित केले गेले आहे",
"invitedTwoMembers": "{{first}} आणि {{second}} आमंत्रित केले गेले आहे",
"kickParticipant": "{{kicked}} was kicked by {{kicker}}",
"me": "Me",
"moderator": "नियंत्रक अधिकार मंजूर!",
"muted": "आपण संभाषण निःशब्द केले आहे.",
"mutedTitle": "आपण निःशब्द आहात!",
"mutedRemotelyTitle": "आपण द्वारे निःशब्द केले गेले आहे {{participantDisplayName}}!",
"mutedRemotelyDescription": "आपण बोलण्यास तयार असता तेव्हा आपण नेहमी सशब्द करू शकता. आपण संमेलनापासून आवाज दूर ठेवण्यासाठी पूर्ण झाल्यावर परत नि: शब्द करा.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) दुसर्‍या सहभागीने काढले",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) दुसर्‍या सहभागीने सेट केलेले",
"raisedHand": "{{name}} बोलायला आवडेल.",
"somebody": "कुणीतरी",
"startSilentTitle": "आपण ऑडिओ आउटपुटसह सामील झालात!",
"startSilentDescription": "ऑडिओ सक्षम करण्यासाठी संमेलनात पुन्हा सामील व्हा",
"suboptimalBrowserWarning": " आम्हाला भीती वाटते की येथे आपल्या भेटीचा अनुभव इतका उत्कृष्ट होणार नाही. आम्ही यामध्ये सुधारणा करण्याचे मार्ग शोधत आहोत, परंतु तोपर्यंत कृपया त्यापैकी एक वापरून पहा <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
"suboptimalExperienceTitle": "ब्राउझर चेतावणी",
"unmute": "सशब्द करा",
"newDeviceCameraTitle": "नवीन कॅमेरा आढळला",
"newDeviceAudioTitle": "नवीन ऑडिओ डिव्हाइस आढळले",
"newDeviceAction": "वापरा",
"OldElectronAPPTitle": "सुरक्षा असुरक्षा!",
"oldElectronClientDescription1": "आपण सुरक्षा असुरक्षा ज्ञात असलेल्या जितसी मीट क्लायंटची जुनी आवृत्ती वापरत असल्याचे दिसून येत आहे. कृपया आमच्याकडे आपण अद्यतनित असल्याची खात्री करा",
"oldElectronClientDescription2": "नवीनतम बिल्ड",
"oldElectronClientDescription3": " आता!"
},
"passwordSetRemotely": "दुसर्‍या सहभागीने सेट केलेले",
"passwordDigitsOnly": " पर्यंत {{number}} अंक",
"poweredby": "द्वारा समर्थित",
"prejoin": {
"audioAndVideoError": "ऑडिओ आणि व्हिडिओ त्रुटी:",
"audioOnlyError": "ऑडिओ त्रुटी:",
"audioTrackError": "ऑडिओ ट्रॅक तयार करू शकलो नाही.",
"callMe": "मला कॉल करा",
"callMeAtNumber": "मला या नंबरवर कॉल करा:",
"configuringDevices": "डिव्हाइस कॉन्फिगर करीत आहे ...",
"connectedWithAudioQ": "आपण ऑडिओशी कनेक्ट आहात?",
"copyAndShare": "मीटिंगचा दुवा कॉपी आणि सामायिक करा",
"dialInMeeting": "बैठकीत डायल करा",
"dialInPin": "संमेलनात डायल करा आणि पिन कोड प्रविष्ट करा:",
"dialing": "डायल करत आहे",
"iWantToDialIn": "मला डायल करायचे आहे",
"joinAudioByPhone": "फोन ऑडिओसह सामील व्हा",
"joinMeeting": "बैठकीत सामील व्हा",
"joinWithoutAudio": "ऑडिओशिवाय सामील व्हा",
"initiated": "कॉल सुरू झाला",
"linkCopied": "क्लिपबोर्डवर दुवा कॉपी केला",
"lookGood": "स्पीकर आणि मायक्रोफोन चांगले दिसतात",
"or": "किंवा",
"calling": "कॉल करीत आहे",
"startWithPhone": "फोन ऑडिओसह प्रारंभ करा",
"screenSharingError": "स्क्रीन सामायिकरण त्रुटी:",
"videoOnlyError": "व्हिडिओ त्रुटी:",
"videoTrackError": "व्हिडिओ ट्रॅक तयार करू शकलो नाही.",
"viewAllNumbers": "सर्व संख्या पहा"
},
"presenceStatus": {
"busy": "व्यस्त",
"calling": "कॉल करीत आहे ...",
"connected": "जोडलेले",
"connecting": "कनेक्ट करीत आहे ...",
"connecting2": "कनेक्ट करीत आहे ...",
"disconnected": "डिस्कनेक्ट केलेले",
"expired": "कालबाह्य",
"ignored": "दुर्लक्षित",
"initializingCall": "कॉल प्रारंभ करीत आहे ...",
"invited": "आमंत्रित केले",
"rejected": "नाकारले",
"ringing": "रिंग होत आहे ..."
},
"profile": {
"setDisplayNameLabel": " आपले प्रदर्शन नाव सेट करा",
"setEmailInput": "ई-मेल प्रविष्ट करा",
"setEmailLabel": "आपला गुरुतर ईमेल सेट करा",
"title": "प्रोफाइल"
},
"raisedHand": "बोलायला आवडेल",
"recording": {
"authDropboxText": " ड्रॉपबॉक्सवर अपलोड करा",
"availableSpace": "उपलब्ध जागा: {{spaceLeft}} MB (approximately {{duration}} रेकॉर्डिंग मिनिटे)",
"beta": "BETA",
"busy": "आम्ही रेकॉर्डिंग संसाधने मुक्त करण्यावर कार्य करीत आहोत. कृपया काही मिनिटांत पुन्हा प्रयत्न करा.",
"busyTitle": "सर्व रेकॉर्डर सध्या व्यस्त आहेत",
"error": "रेकॉर्डिंग अयशस्वी. कृपया पुन्हा प्रयत्न करा.",
"expandedOff": "रेकॉर्डिंग थांबले आहे",
"expandedOn": "सभेची नोंद सध्या घेतली जात आहे.",
"expandedPending": "रेकॉर्डिंग सुरू केले जात आहे ...",
"failedToStart": "रेकॉर्डिंग सुरू करण्यात अयशस्वी",
"fileSharingdescription": "मीटिंगमधील सहभागींसह रेकॉर्डिंग सामायिक करा",
"live": "LIVE",
"loggedIn": "म्हणून लॉग इन केले {{userName}}",
"off": "रेकॉर्डिंग थांबले",
"offBy": "{{name}} रेकॉर्डिंग थांबविले",
"on": "Recording",
"onBy": "{{name}} रेकॉर्डिंग सुरू केले",
"pending": "मीटिंग रेकॉर्ड करण्याची तयारी करत आहे ...",
"rec": "REC",
"serviceDescription": "आपले रेकॉर्डिंग रेकॉर्डिंग सेवेद्वारे जतन केले जाईल",
"serviceName": "रेकॉर्डिंग सेवा",
"signIn": "साइन इन करा",
"signOut": "साइन आउट करा",
"unavailable": " अरेरे! {{serviceName}} currently सध्या अनुपलब्ध आहे. आम्ही या समस्येचे निराकरण करण्याचे काम करीत आहोत. कृपया पुन्हा प्रयत्न करा.",
"unavailableTitle": "रेकॉर्डिंग अनुपलब्ध"
},
"sectionList": {
"pullToRefresh": "रीफ्रेश करण्यासाठी खेचा"
},
"settings": {
"calendar": {
"about": " {{appName}} कॅलेंडर समाकलन सुरक्षितपणे आपल्या कॅलेंडरमध्ये प्रवेश करण्यासाठी वापरले जाते जेणेकरून ते आगामी कार्यक्रम वाचू शकतील.",
"disconnect": "डिस्कनेक्ट करा",
"microsoftSignIn": "मायक्रोसॉफ्ट सह साइन इन करा",
"signedIn": "सध्या {{email}} साठी कॅलेंडर इव्हेंटमध्ये प्रवेश करत आहे. कॅलेंडर इव्हेंटमध्ये प्रवेश करणे थांबविण्यासाठी खालील डिस्कनेक्ट बटणावर क्लिक करा",
"title": "कॅलेंडर"
},
"devices": "उपकरणे",
"followMe": "प्रत्येकजण माझ्या मागे येतो",
"language": "भाषा",
"loggedIn": "{{name}} म्हणून लॉग इन केले",
"microphones": "मायक्रोफोन",
"moderator": "नियंत्रक",
"more": "अधिक",
"name": "नाव",
"noDevice": "काहीही नाही",
"selectAudioOutput": "ऑडिओ आउटपुट",
"selectCamera": "कॅमेरा",
"selectMic": "मायक्रोफोन",
"speakers": "स्पीकर्स",
"startAudioMuted": " प्रत्येकजण निःशब्द होतो",
"startVideoMuted": "प्रत्येकजण दडलेला सुरू होतो",
"title": "सेटिंग्ज"
},
"settingsView": {
"advanced": "प्रगत",
"alertOk": "ठीक आहे",
"alertTitle": "चेतावणी",
"alertURLText": "प्रविष्ट केलेली सर्व्हर URL अवैध आहे",
"buildInfoSection": "बिल्ड माहिती",
"conferenceSection": "परिषद",
"disableCallIntegration": "नेटिव्ह कॉल एकत्रीकरण अक्षम करा",
"disableP2P": " पीअर-टू-पीअर मोड अक्षम करा",
"displayName": " नाव प्रदर्शन",
"email": "ईमेल",
"header": "सेटिंग्ज",
"profileSection": "प्रोफाइल",
"serverURL": "सर्व्हर URL",
"showAdvanced":"प्रगत सेटिंग्ज दर्शवा",
"startWithAudioMuted": "ऑडिओ नि: शब्द सह प्रारंभ करा",
"startWithVideoMuted": "निःशब्द व्हिडिओसह प्रारंभ करा",
"version": "आवृत्ती"
},
"share": {
"dialInfoText": "\n\n=====\n\nफक्त आपल्या फोनवर डायल करू इच्छिता?\n\n{{defaultDialInNumber}} या संमेलनासाठी फोन नंबर डायल पाहण्यासाठी या दुव्यावर क्लिक करा\n{{dialInfoPageUrl}}",
"mainText": "संमेलनात सामील होण्यासाठी खालील दुव्यावर क्लिक करा:\n{{roomUrl}}"
},
"speaker": "स्पीकर",
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "नाव",
"seconds": "{{count}}s",
"speakerStats": "स्पीकर आकडेवारी",
"speakerTime": "स्पीकर वेळ"
},
"startupoverlay": {
"policyText": " ",
"title": "{{app}} आपला मायक्रोफोन आणि कॅमेरा वापरण्याची आवश्यकता आहे."
},
"suspendedoverlay": {
"rejoinKeyTitle": "पुन्हा जॉइन करा",
"text": " पुन्हा कनेक्ट करण्यासाठी <i>Rejoin</i> बटण दाबा.",
"title": "आपला व्हिडिओ कॉल व्यत्यय आला कारण हा संगणक झोपायला गेला."
},
"toolbar": {
"accessibilityLabel": {
},
"addPeople": "आपल्या कॉलमध्ये लोकांना जोडा",
"audioOnlyOff": "कमी बँडविड्थ मोड अक्षम करा",
"audioOnlyOn": "कमी बँडविड्थ मोड सक्षम करा",
"audioRoute": "ध्वनी यंत्र निवडा",
"authenticate": "प्रमाणित करा",
"callQuality": "व्हिडिओ गुणवत्ता व्यवस्थापित करा",
"chat": "गप्पा / बंद करा उघडा",
"closeChat": "गप्पा बंद करा",
"documentClose": "सामायिक दस्तऐवज बंद करा",
"documentOpen": "सामायिक दस्तऐवज उघडा",
"download": "आमचे अ‍ॅप्स डाउनलोड करा",
"e2ee": "एंड-टू-एंड एनक्रिप्शन",
"enterFullScreen": "पूर्ण स्क्रीन पहा",
"enterTileView": "टाइल दृश्य प्रविष्ट करा",
"exitFullScreen": "पूर्ण स्क्रीनमधून बाहेर पडा",
"exitTileView": "बाहेर पडा टाइल दृश्य",
"feedback": "अभिप्राय द्या",
"hangup": "सोडा",
"help": "मदत करा",
"invite": "लोकांना आमंत्रित करा",
"login": "लॉगिन",
"logout": "बाहेर पडणे",
"lowerYourHand": "बाहेर पडणे",
"moreActions": "अधिक क्रिया",
"moreOptions": "अधिक पर्याय",
"mute": "नि: शब्द / सशब्द करा",
"muteEveryone": "सर्वांना नि: शब्द करा",
"noAudioSignalTitle": "आपल्या माइकवरून कोणतेही इनपुट येत नाही!",
"noAudioSignalDesc": "आपण सिस्टम सेटिंग्ज किंवा हार्डवेअरवरून हेतुपुरस्सर नि: शब्द न केल्यास, डिव्हाइस बदलण्याचा विचार करा.",
"noAudioSignalDescSuggestion": "आपण सिस्टम सेटिंग्ज किंवा हार्डवेअरवरून हेतुपुरस्सर नि: शब्द न केल्यास, सूचित डिव्हाइसवर स्विच करण्याचा विचार करा.",
"noAudioSignalDialInDesc": "आपण हे वापरून डायल-इन देखील करू शकता:",
"noAudioSignalDialInLinkDesc": "डायल-इन क्रमांक",
"noisyAudioInputTitle": "आपला मायक्रोफोन गोंगाट करणारा दिसत आहे!",
"noisyAudioInputDesc": "आपला मायक्रोफोन आवाज देत असल्यासारखे दिसत आहे, कृपया डिव्हाइस नि: शब्द करणे किंवा बदलणे याचा विचार करा.",
"openChat": "खुली गप्पा",
"pip": "पिक्चर-इन-पिक्चर मोड प्रविष्ट करा",
"privateMessage": "खाजगी संदेश पाठवा",
"profile": "आपले प्रोफाइल संपादित करा",
"raiseHand": "हात वर करा / कमी करा",
"raiseYourHand": "तुझा हात वर कर",
"Settings": "सेटिंग्ज",
"sharedvideo": "एक YouTube व्हिडिओ सामायिक करा",
"shareRoom": "एखाद्यास आमंत्रित करा",
"shortcuts": "शॉर्टकट पहा",
"speakerStats": "स्पीकर आकडेवारी",
"startScreenSharing": "स्क्रीन सामायिकरण प्रारंभ करा",
"startSubtitles": "उपशीर्षके प्रारंभ करा",
"stopScreenSharing": "स्क्रीन सामायिकरण थांबवा",
"stopSubtitles": "उपशीर्षके थांबवा",
"stopSharedVideo": "YouTube व्हिडिओ थांबवा",
"talkWhileMutedPopup": "बोलण्याचा प्रयत्न करीत आहात? आपण निःशब्द आहात.",
"tileViewToggle": "टाइल दृश्य टॉगल करा",
"toggleCamera": "टॉगल कॅमेरा",
"videomute": "कॅमेरा प्रारंभ / थांबवा",
"startvideoblur": "माझी पार्श्वभूमी अस्पष्ट करा",
"stopvideoblur": "पार्श्वभूमी डाग अक्षम करा"
},
"transcribing": {
"ccButtonTooltip": "उपशीर्षके प्रारंभ / थांबवा",
"error": "लिप्यंतरण अयशस्वी. कृपया पुन्हा प्रयत्न करा.",
"expandedLabel": "लिप्यंतरण चालू आहे",
"failedToStart": "लिप्यंतरण सुरू करण्यात अयशस्वी",
"labelToolTip": "सभेचे प्रतिलेखन केले जात आहे",
"off": "लिप्यंतरण थांबविले",
"pending": "संमेलनाची नक्कल करण्याची तयारी करत आहे ...",
"start": "उपशीर्षके दर्शविणे प्रारंभ करा",
"stop": "उपशीर्षके दर्शविणे थांबवा",
"tr": "टीआर"
},
"userMedia": {
"androidGrantPermissions": "निवडा <b><i>परवानगी द्या</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"chromeGrantPermissions": "निवडा <b><i>परवानगी द्या</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"edgeGrantPermissions": "निवडा <b><i> होय</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"electronGrantPermissions": "कृपया आपला कॅमेरा आणि मायक्रोफोन वापरण्यास परवानगी द्या",
"firefoxGrantPermissions": "Select <b><i>निवडलेले डिव्हाइस सामायिक करा</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"iexplorerGrantPermissions": "निवडा <b><i>ठीक आहे</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"nwjsGrantPermissions": "कृपया आपला कॅमेरा आणि मायक्रोफोन वापरण्यास परवानगी द्या",
"operaGrantPermissions": "निवडा <b><i>परवानगी द्या</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"react-nativeGrantPermissions": "निवडा <b><i>परवानगी द्या</i></b>जेव्हा आपला ब्राउझर परवानग्या विचारतो.",
"safariGrantPermissions": "निवडा <b><i>ठीक आहे</i></b> जेव्हा आपला ब्राउझर परवानग्या विचारतो."
},
"videoSIPGW": {
"busy": "आम्ही स्त्रोत मुक्त करण्याचे काम करत आहोत. कृपया काही मिनिटांत पुन्हा प्रयत्न करा.",
"busyTitle": " परिषद खोलीत सेवा सध्या व्यस्त आहे",
"errorAlreadyInvited": " {{displayName}} आधीच आमंत्रित आहे",
"errorInvite": "परिषद अद्याप स्थापन केलेली नाही. कृपया पुन्हा प्रयत्न करा.",
"errorInviteFailed": "आम्ही या समस्येचे निराकरण करण्याचे काम करीत आहोत. कृपया पुन्हा प्रयत्न करा.",
"errorInviteFailedTitle": "{{displayName}} आमंत्रित करणे अयशस्वी",
"errorInviteTitle": " परिषद खोलीत आमंत्रित करताना त्रुटी",
"pending": "{{displayName}} आमंत्रित केले गेले आहे"
},
"videoStatus": {
"audioOnly": "एडीडी",
"audioOnlyExpanded": "आपण कमी बँडविड्थ मोडमध्ये आहात. या मोडमध्ये आपल्याला केवळ ऑडिओ आणि स्क्रीन सामायिकरण प्राप्त होईल.",
"callQuality": "व्हिडिओ गुणवत्ता",
"hd": "एचडी",
"hdTooltip": "उच्च परिभाषा व्हिडिओ पहात आहे",
"highDefinition": "उंच - व्याख्या",
"labelTooiltipNoVideo": "व्हिडिओ नाही",
"labelTooltipAudioOnly": "कमी बँडविड्थ मोड सक्षम",
"ld": "एलडी",
"ldTooltip": "निम्न परिभाषा व्हिडिओ पहात आहे",
"lowDefinition": "कमी व्याख्या",
"onlyAudioAvailable": "केवळ ऑडिओ उपलब्ध आहे",
"onlyAudioSupported": "आम्ही या ब्राउझरमध्ये केवळ ऑडिओचे समर्थन करतो.",
"p2pEnabled": "सरदार ते सरदार सक्षम",
"p2pVideoQualityDescription": "पीअर टू पीअर मोडमध्ये, प्राप्त व्हिडिओ गुणवत्ता केवळ उच्च आणि ऑडिओ दरम्यानच टॉगल केली जाऊ शकते. पीअर टू पीअर बाहेर येईपर्यंत इतर सेटिंग्जचा आदर केला जाणार नाही.",
"recHighDefinitionOnly": "उच्च परिभाषा पसंत करेल.",
"sd": "एसडी",
"sdTooltip": "मानक परिभाषा व्हिडिओ पहात आहे",
"standardDefinition": "मानक व्याख्या"
},
"videothumbnail": {
"domute": "नि: शब्द करा",
"domuteOthers": "इतर सर्वांना नि: शब्द करा",
"flip": "फ्लिप",
"kick": "लाथा मारून बाहेर काढ",
"moderator": "नियंत्रक",
"mute": "सहभागी निःशब्द आहे",
"muted": "नि: शब्द केलेले",
"remoteControl": "रिमोट कंट्रोल प्रारंभ / थांबवा.",
"show": "रंगमंचावर दाखवा",
"videomute": "सहभागीने कॅमेरा थांबविला आहे"
},
"welcomepage": {
"accessibilityLabel": {
"join": "सामील होण्यासाठी टॅप करा",
"roomname": "खोलीचे नाव प्रविष्ट करा"
},
"appDescription": " पुढे जा, संपूर्ण टीमसह व्हिडिओ चॅट करा. खरं तर, आपल्या ओळखीच्या प्रत्येकास आमंत्रित करा. {{app}} एक संपूर्ण एनक्रिप्टेड, 100% मुक्त स्रोत व्हिडिओ कॉन्फरन्सिंग समाधान आहे जे आपण दिवसभर, दररोज विनामूल्य वापरु शकता - कोणतेही खाते आवश्यक नाही.",
"audioVideoSwitch": {
"audio": "आवाज",
"video": "व्हिडिओ"
},
"calendar": "कॅलेंडर",
"connectCalendarButton": "आपले कॅलेंडर कनेक्ट करा",
"connectCalendarText": "आपली सर्व सभा {{app}} in मध्ये पाहण्यासाठी कॅलेंडर कनेक्ट करा. तसेच, आपल्या कॅलेंडरमध्ये {{provider}} संमेलने जोडा आणि एका क्लिकने त्या प्रारंभ करा.",
"enterRoomTitle": "नवीन बैठक सुरू करा",
"getHelp": "Get help",
"roomNameAllowedChars": "संमेलनाच्या नावात यापैकी कोणतेही वर्ण नसावेत: ?, &, :, ', \", %, #.",
"go": "GO",
"goSmall": "GO",
"join": "तयार करा / सामील व्हा",
"info": "माहिती",
"privacy": "गोपनीयता",
"recentList": "अलीकडील",
"recentListDelete": "हटवा",
"recentListEmpty": "आपली अलीकडील यादी सध्या रिक्त आहे. आपल्या कार्यसंघाशी गप्पा मारा आणि आपल्याला आपल्या सर्व अलीकडील संमेलने येथे आढळतील.",
"reducedUIText": "{{App}} वर आपले स्वागत आहे!",
"roomname": "Enter room name",
"roomnameHint": "आपण सामील होऊ इच्छित असलेल्या खोलीचे नाव किंवा URL प्रविष्ट करा. आपण नाव लिहू शकता, आपण ज्यांना भेटत आहात त्या लोकांना हे कळू द्या जेणेकरुन ते समान नाव प्रविष्ट करा.",
"sendFeedback": "अभिप्राय पाठवा",
"terms": "अटी",
"title": "सुरक्षित, पूर्णपणे वैशिष्ट्यीकृत आणि पूर्णपणे विनामूल्य व्हिडिओ कॉन्फरन्सिंग"
},
"lonelyMeetingExperience": {
"button": "इतरांना आमंत्रित करा",
"youAreAlone": "आपण सभेत एकटाच आहात"
},
"helpView": {
"header": "मदत केंद्र"
}
}

View File

@@ -227,7 +227,7 @@
"remoteControlStopMessage": "Uzaktan kontrol oturumu sona erdi!",
"remoteControlTitle": "Uzak masaüstü kontrolü",
"Remove": "Kaldır",
"removePassword": "Şifreyi kaldır",
"removePassword": "Parolayı kaldır",
"removeSharedVideoMsg": "Paylaşılan videonuzu kaldırmak istediğinizden emin misiniz?",
"removeSharedVideoTitle": "Paylaşılan videoyu kaldır",
"reservationError": "Rezervasyon sistemi hatası",
@@ -289,8 +289,8 @@
},
"info": {
"accessibilityLabel": "Bilgiyi göster",
"addPassword": "Şifre ekle",
"cancelPassword": "Şifreyi iptal et",
"addPassword": "Parola ekle",
"cancelPassword": "Parolayı iptal et",
"conferenceURL": "Bağlantı:",
"country": "Ülke",
"dialANumber": "Toplantınıza katılmak için bu numaralardan birini çevirin ve ardından kodu girin.",
@@ -312,7 +312,7 @@
"noPassword": "Yok",
"noRoom": "Aranacak oda belirtilmedi.",
"numbers": "Arama Numaraları",
"password": "Şifre:",
"password": "Parola:",
"title": "Paylaş",
"tooltip": "Bu toplantı için bağlantıyı ve arama bilgilerini paylaşın",
"label": "Toplantı bilgileri"
@@ -424,16 +424,16 @@
"muted": "Görüşmeye sesiniz kapalı olarak başladınız.",
"mutedTitle": "Sesiniz kapalı!",
"mutedRemotelyTitle": "{{participantDisplayName}} tarafından sessize alındınız!",
"mutedRemotelyDescription": "",
"passwordRemovedRemotely": "",
"passwordSetRemotely": "",
"mutedRemotelyDescription": "Konuşmaya hazır olduğun zaman, Kendi mikrofonunu açabilirsin. Görüşmeden gürültüyü uzak tutmak için kendini tekrar sessize almalısın.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) başka bir katılımcı tarafından kaldırıldı",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) başka bir katılımcı tarafından ayarlandı",
"raisedHand": "{{name}} konuşmak istiyor.",
"somebody": "Birisi",
"startSilentTitle": "",
"startSilentDescription": "",
"suboptimalExperienceDescription": "Mmm... {{appName}} ile olan deneyiminizin burada çok iyi olmayacağından korkuyoruz. Bunu iyileştirmenin yollarını arıyoruz, ancak o zamana kadar lütfen şunlardan birini deneyin: <a href='{{recommendedBrowserPageLink}}' target='_blank'>fully supported browsers</a>.",
"startSilentTitle": "Ses çıkışı olmadan bağlandınız",
"startSilentDescription": "Ses çıkışını açtıktan sonra tekrar bağlanın",
"suboptimalExperienceDescription": "Mmm... {{appName}} ile olan deneyiminizin burada çok iyi olmayacağından korkuyoruz. Bunu iyileştirmenin yollarını arıyoruz, ancak o zamana kadar lütfen şunlardan birini deneyin: <a href='{{recommendedBrowserPageLink}}' target='_blank'>desteklenen tarayıcılar</a>.",
"suboptimalExperienceTitle": "Tarayıcı Uyarısı",
"unmute": "",
"unmute": "Sessizden çıkar",
"newDeviceCameraTitle": "Yeni kamera algılandı",
"newDeviceAudioTitle": "Yeni ses aygıtı algılandı",
"newDeviceAction": "Kullan"
@@ -577,7 +577,7 @@
"shareRoom": "Birini davet et",
"shareYourScreen": "Ekran paylaşımını aç/kapat",
"shortcuts": "Kısayolları aç/kapat",
"show": "",
"show": "Sahnede göster",
"speakerStats": "Konuşmacı istatistiklerini aç/kapat",
"tileView": "Döşeme görünümünü aç/kapat",
"toggleCamera": "Kamerayı aç/kapat",

View File

@@ -675,6 +675,7 @@
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password",
"moreActions": "Toggle more actions menu",
@@ -722,6 +723,8 @@
"hangup": "Leave",
"help": "Help",
"invite": "Invite people",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Login",
"logout": "Logout",
"lowerYourHand": "Lower your hand",
@@ -861,5 +864,32 @@
},
"helpView": {
"header": "Help center"
},
"lobby": {
"allow": "Allow",
"backToKnockModeButton": "No password, ask to join instead",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
"emailField": "Enter your email address",
"enableDialogPasswordField": "Set password (optional)",
"enableDialogSubmit": "Enable",
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "Enter meeting password",
"enterPasswordTitle": "Enter password to join meeting",
"invalidPassword": "Invalid password",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
"joinWithPasswordMessage": "Trying to join with password, please wait...",
"joinRejectedMessage": "Your join request was rejected by a moderator.",
"joinTitle": "Join Meeting",
"joiningTitle": "Asking to join meeting...",
"joiningWithPasswordTitle": "Joining with password...",
"knockButton": "Ask to Join",
"knockTitle": "Someone wants to join the meeting",
"nameField": "Enter your name",
"passwordField": "Enter meeting password",
"passwordJoinButton": "Join",
"reject": "Reject",
"toggleLabel": "Enable lobby"
}
}

View File

@@ -98,19 +98,6 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Notify user that conference was destroyed.
* @param reason {string} the reason text
*/
UI.notifyConferenceDestroyed = function(reason) {
// FIXME: use Session Terminated from translation, but
// 'reason' text comes from XMPP packet and is not translated
messageHandler.showError({
description: reason,
titleKey: 'dialog.sessTerminated'
});
};
/**
* Change nickname for the user.
* @param {string} id user id

View File

@@ -498,9 +498,6 @@ export class VideoContainer extends LargeContainer {
});
this._updateBackground();
// Reset the large video background depending on the stream.
this.setLargeVideoBackground(this.avatarDisplayed);
}
/**
@@ -533,14 +530,6 @@ export class VideoContainer extends LargeContainer {
* @param {boolean} show
*/
showAvatar(show) {
// TO FIX: Video background need to be black, so that we don't have a
// flickering effect when scrolling between videos and have the screen
// move to grey before going back to video. Avatars though can have the
// default background set.
// In order to fix this code we need to introduce video background or
// find a workaround for the video flickering.
this.setLargeVideoBackground(show);
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
this.avatarDisplayed = show;
@@ -596,21 +585,6 @@ export class VideoContainer extends LargeContainer {
return false;
}
/**
* Sets the large video container background depending on the container
* type and the parameter indicating if an avatar is currently shown on
* large.
*
* @param {boolean} isAvatar - Indicates if the avatar is currently shown
* on the large video.
* @returns {void}
*/
setLargeVideoBackground(isAvatar) {
$('#largeVideoContainer').css('background',
this.videoType === VIDEO_CONTAINER_TYPE && !isAvatar
? '#000' : interfaceConfig.DEFAULT_BACKGROUND);
}
/**
* Callback invoked when the video element changes dimensions.
*

10
package-lock.json generated
View File

@@ -10966,8 +10966,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#cfbb511bcec24df44879a1fc271498e2d80e8477",
"from": "github:jitsi/lib-jitsi-meet#cfbb511bcec24df44879a1fc271498e2d80e8477",
"version": "github:jitsi/lib-jitsi-meet#6af8eee57d1ebdc0881c8c2875d4346e02d01549",
"from": "github:jitsi/lib-jitsi-meet#6af8eee57d1ebdc0881c8c2875d4346e02d01549",
"requires": {
"@jitsi/sdp-interop": "1.0.2",
"@jitsi/sdp-simulcast": "0.3.0",
@@ -19759,9 +19759,9 @@
}
},
"websocket-extensions": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"whatwg-fetch": {

View File

@@ -56,7 +56,7 @@
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#cfbb511bcec24df44879a1fc271498e2d80e8477",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6af8eee57d1ebdc0881c8c2875d4346e02d01549",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",

View File

@@ -23,7 +23,7 @@ import {
parseURIString,
toURLString
} from '../base/util';
import { showNotification } from '../notifications';
import { clearNotifications, showNotification } from '../notifications';
import { setFatalError } from '../overlay';
import {
@@ -79,6 +79,10 @@ export function appNavigate(uri: ?string) {
dispatch(disconnect());
}
// There are notifications now that gets displayed after we technically left
// the conference, but we're still on the conference screen.
dispatch(clearNotifications());
dispatch(configWillLoad(locationURL, room));
let protocol = location.protocol.toLowerCase();

View File

@@ -7,6 +7,7 @@ import '../../base/lastn'; // Register lastN middleware
import { toURLString } from '../../base/util';
import '../../follow-me';
import { OverlayContainer } from '../../overlay';
import '../../lobby'; // Import lobby function
import '../../rejoin'; // Enable rejoin analytics
import { appNavigate } from '../actions';
import { getDefaultURL } from '../functions';

View File

@@ -70,7 +70,7 @@ export default {
initialsText: (size: number = DEFAULT_SIZE) => {
return {
color: 'rgba(255, 255, 255, 0.6)',
color: 'white',
fontSize: size * 0.45,
fontWeight: '100'
};

View File

@@ -249,6 +249,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
* @param {JitsiConference} conference - The JitsiConference that has failed.
* @param {string} error - The error describing/detailing the cause of the
* failure.
* @param {any} params - Rest of the params that we receive together with the event.
* @returns {{
* type: CONFERENCE_FAILED,
* conference: JitsiConference,
@@ -256,7 +257,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
* }}
* @public
*/
export function conferenceFailed(conference: Object, error: string) {
export function conferenceFailed(conference: Object, error: string, ...params: any) {
return {
type: CONFERENCE_FAILED,
conference,
@@ -265,6 +266,7 @@ export function conferenceFailed(conference: Object, error: string) {
// jitsi-meet needs it).
error: {
name: error,
params,
recoverable: undefined
}
};
@@ -650,28 +652,23 @@ export function setPassword(
case conference.join: {
let state = getState()['features/base/conference'];
// Make sure that the action will set a password for a conference
// that the application wants joined.
if (state.passwordRequired === conference) {
dispatch({
type: SET_PASSWORD,
conference,
method,
password
});
dispatch({
type: SET_PASSWORD,
conference,
method,
password
});
// Join the conference with the newly-set password.
// Join the conference with the newly-set password.
// Make sure that the action did set the password.
state = getState()['features/base/conference'];
if (state.password === password
&& !state.passwordRequired
// Make sure that the action did set the password.
state = getState()['features/base/conference'];
if (state.password === password
// Make sure that the application still wants the
// conference joined.
&& !state.conference) {
method.call(conference, password);
}
// Make sure that the application still wants the
// conference joined.
&& !state.conference) {
method.call(conference, password);
}
break;
}

View File

@@ -203,7 +203,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
* @returns {JitsiConference|undefined}
*/
export function getCurrentConference(stateful: Function | Object) {
const { conference, joining, leaving, passwordRequired }
const { conference, joining, leaving, membersOnly, passwordRequired }
= toState(stateful)['features/base/conference'];
// There is a precendence
@@ -211,7 +211,7 @@ export function getCurrentConference(stateful: Function | Object) {
return conference === leaving ? undefined : conference;
}
return joining || passwordRequired;
return joining || passwordRequired || membersOnly;
}
/**

View File

@@ -8,7 +8,8 @@ import {
sendAnalytics
} from '../../analytics';
import { openDisplayNamePrompt } from '../../display-name';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
import { showErrorNotification } from '../../notifications';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import {
@@ -140,13 +141,40 @@ StateListenerRegistry.register(
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceFailed(store, next, action) {
function _conferenceFailed({ dispatch, getState }, next, action) {
const result = next(action);
const { conference, error } = action;
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
// Handle specific failure reasons.
switch (error.name) {
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = error.params;
dispatch(showErrorNotification({
description: reason,
titleKey: 'dialog.sessTerminated'
}));
if (typeof APP !== 'undefined') {
APP.UI.hideStats();
}
break;
}
case JitsiConferenceErrors.CONNECTION_ERROR: {
const [ msg ] = error.params;
dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
dispatch(showErrorNotification({
descriptionArguments: { msg },
descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
titleKey: 'connection.CONNFAIL'
}));
break;
}
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
sendAnalytics(createOfferAnswerFailedEvent());
break;
}
// FIXME: Workaround for the web version. Currently, the creation of the

View File

@@ -36,6 +36,7 @@ const DEFAULT_STATE = {
leaving: undefined,
locked: undefined,
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
membersOnly: undefined,
password: undefined,
passwordRequired: undefined,
preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
@@ -161,6 +162,7 @@ function _conferenceFailed(state, { conference, error }) {
}
let authRequired;
let membersOnly;
let passwordRequired;
switch (error.name) {
@@ -168,6 +170,11 @@ function _conferenceFailed(state, { conference, error }) {
authRequired = conference;
break;
case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR:
membersOnly = conference;
break;
case JitsiConferenceErrors.PASSWORD_REQUIRED:
passwordRequired = conference;
break;
@@ -189,6 +196,7 @@ function _conferenceFailed(state, { conference, error }) {
* @type {string}
*/
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
membersOnly,
password: undefined,
/**
@@ -232,6 +240,7 @@ function _conferenceJoined(state, { conference }) {
e2eeSupported: conference.isE2EESupported(),
joining: undefined,
membersOnly: undefined,
leaving: undefined,
/**
@@ -378,34 +387,30 @@ function _setDesktopSharingEnabled(state, action) {
function _setPassword(state, { conference, method, password }) {
switch (method) {
case conference.join:
if (state.passwordRequired === conference) {
return assign(state, {
// XXX 1. The JitsiConference which transitions away from
// passwordRequired MUST remain in the redux state
// features/base/conference until it transitions into
// conference; otherwise, there is a span of time during which
// the redux state does not even know that there is a
// JitsiConference whatsoever.
//
// 2. The redux action setPassword will attempt to join the
// JitsiConference so joining is an appropriate transitional
// redux state.
//
// 3. The redux action setPassword will perform the same check
// before it proceeds with the re-join.
joining: state.conference ? state.joining : conference,
locked: LOCKED_REMOTELY,
return assign(state, {
// 1. The JitsiConference which transitions away from
// passwordRequired MUST remain in the redux state
// features/base/conference until it transitions into
// conference; otherwise, there is a span of time during which
// the redux state does not even know that there is a
// JitsiConference whatsoever.
//
// 2. The redux action setPassword will attempt to join the
// JitsiConference so joining is an appropriate transitional
// redux state.
//
// 3. The redux action setPassword will perform the same check
// before it proceeds with the re-join.
joining: state.conference ? state.joining : conference,
locked: LOCKED_REMOTELY,
/**
* The password with which the conference is to be joined.
*
* @type {string}
*/
password,
passwordRequired: undefined
});
}
break;
/**
* The password with which the conference is to be joined.
*
* @type {string}
*/
password
});
case conference.lock:
return assign(state, {

View File

@@ -18,6 +18,7 @@ export default [
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
'CONNECTION_INDICATOR_DISABLED',
'DEFAULT_BACKGROUND',
'DEFAULT_LOGO_URL',
'DISABLE_PRESENCE_STATUS',
'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
'DEFAULT_LOCAL_DISPLAY_NAME',

View File

@@ -5,8 +5,7 @@
*
* {
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection,
* message: string
* connection: JitsiConnection
* }
*/
export const CONNECTION_DISCONNECTED = 'CONNECTION_DISCONNECTED';

View File

@@ -113,13 +113,12 @@ export function connect(id: ?string, password: ?string) {
* Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
* disconnected.
*
* @param {string} message - Disconnect reason.
* @private
* @returns {void}
*/
function _onConnectionDisconnected(message: string) {
function _onConnectionDisconnected() {
unsubscribe();
dispatch(_connectionDisconnected(connection, message));
dispatch(connectionDisconnected(connection));
}
/**
@@ -187,19 +186,16 @@ export function connect(id: ?string, password: ?string) {
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which
* disconnected.
* @param {string} message - Error message.
* @private
* @returns {{
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection,
* message: string
* connection: JitsiConnection
* }}
*/
function _connectionDisconnected(connection: Object, message: string) {
export function connectionDisconnected(connection: Object) {
return {
type: CONNECTION_DISCONNECTED,
connection,
message
connection
};
}

View File

@@ -9,6 +9,7 @@ import { configureInitialDevices } from '../devices';
import { getBackendSafeRoomName } from '../util';
export {
connectionDisconnected,
connectionEstablished,
connectionFailed,
setLocationURL

View File

@@ -243,8 +243,10 @@ export function setAudioInputDeviceAndUpdateSettings(deviceId) {
* @returns {Function}
*/
export function setAudioOutputDevice(deviceId) {
return function(dispatch) {
return setAudioOutputDeviceId(deviceId, dispatch);
return function(dispatch, getState) {
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioOutput');
return setAudioOutputDeviceId(deviceId, dispatch, true, deviceLabel);
};
}

View File

@@ -8,6 +8,12 @@ import logger from './logger';
declare var APP: Object;
const webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
audiooutput: 'audioOutput',
videoinput: 'videoInput'
};
/**
* Detects the use case when the labels are not available if the A/V permissions
* are not yet granted.
@@ -41,6 +47,29 @@ export function getAudioOutputDeviceId() {
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
}
/**
* Finds the real device id of the default device of the given type.
*
* @param {Object} state - The redux state.
* @param {*} kind - The type of the device. One of "audioInput",
* "audioOutput", and "videoInput". Also supported is all lowercase versions
* of the preceding types.
* @returns {string|undefined}
*/
export function getDefaultDeviceId(state: Object, kind: string) {
const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind;
const defaultDevice = (state['features/base/devices'].availableDevices[kindToSearch] || [])
.find(d => d.deviceId === 'default');
// Find the device with a matching group id.
const matchingDevice = (state['features/base/devices'].availableDevices[kindToSearch] || [])
.find(d => d.deviceId !== 'default' && d.groupId === defaultDevice.groupId);
if (matchingDevice) {
return matchingDevice.deviceId;
}
}
/**
* Finds a device with a label that matches the passed label and returns its id.
*
@@ -52,12 +81,6 @@ export function getAudioOutputDeviceId() {
* @returns {string|undefined}
*/
export function getDeviceIdByLabel(state: Object, label: string, kind: string) {
const webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
audiooutput: 'audioOutput',
videoinput: 'videoInput'
};
const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind;
const device
@@ -80,12 +103,6 @@ export function getDeviceIdByLabel(state: Object, label: string, kind: string) {
* @returns {string|undefined}
*/
export function getDeviceLabelById(state: Object, id: string, kind: string) {
const webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
audiooutput: 'audioOutput',
videoinput: 'videoInput'
};
const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind;
const device

View File

@@ -30,14 +30,17 @@ export function hideDialog(component: ?Object) {
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @param {boolean} rawDialog - True if the dialog is a raw dialog.
* (Doesn't inherit behavior from other common frameworks).
* @returns {{
* type: OPEN_DIALOG,
* component: React.Component,
* componentProps: (Object | undefined)
* }}
*/
export function openDialog(component: Object, componentProps: ?Object) {
export function openDialog(component: Object, componentProps: ?Object, rawDialog?: boolean) {
return {
rawDialog,
type: OPEN_DIALOG,
component,
componentProps

View File

@@ -17,6 +17,11 @@ type Props = {
*/
_componentProps: Object,
/**
* True if the dialog is a raw dialog (doesn't inherit behavior from other common frameworks, such as atlaskit).
*/
_rawDialog: boolean,
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
@@ -52,19 +57,16 @@ export default class AbstractDialogContainer extends Component<Props> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _component: React.Component,
* _componentProps: Object,
* _reducedUI: boolean
* }}
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object) {
export function abstractMapStateToProps(state: Object): $Shape<Props> {
const stateFeaturesBaseDialog = state['features/base/dialog'];
const { reducedUI } = state['features/base/responsive-ui'];
return {
_component: stateFeaturesBaseDialog.component,
_componentProps: stateFeaturesBaseDialog.componentProps,
_rawDialog: stateFeaturesBaseDialog.rawDialog,
_reducedUI: reducedUI
};
}

View File

@@ -57,14 +57,13 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
<KeyboardAvoidingView
behavior = 'height'
style = { [
styles.overlay,
style
styles.overlay
] }>
<View
pointerEvents = 'box-none'
style = { [
_dialogStyles.dialog,
this.props.style
style
] }>
<TouchableOpacity
onPress = { this._onCancel }

View File

@@ -34,7 +34,7 @@ class BaseSubmitDialog<P: Props, S: *> extends BaseDialog<P, S> {
* @returns {string}
*/
_getSubmitButtonKey() {
return 'dialog.Ok';
return this.props.okKey || 'dialog.Ok';
}
/**

View File

@@ -13,6 +13,11 @@ import StatelessDialog from './StatelessDialog';
*/
type Props = AbstractDialogProps & {
/**
* True if listening for the Enter key should be disabled.
*/
disableEnter: boolean,
/**
* Whether the dialog is modal. This means clicking on the blanket will
* leave the dialog open. No cancel button.

View File

@@ -20,6 +20,10 @@ class DialogContainer extends AbstractDialogContainer {
* @returns {ReactElement}
*/
render() {
if (this.props._rawDialog) {
return this._renderDialogContent();
}
return (
<ModalTransition>
{ this._renderDialogContent() }

View File

@@ -33,6 +33,11 @@ type Props = {
*/
customHeader?: React$Element<any> | Function,
/*
* True if listening for the Enter key should be disabled.
*/
disableEnter: boolean,
/**
* Disables dismissing the dialog when the blanket is clicked. Enabled
* by default.
@@ -313,7 +318,7 @@ class StatelessDialog extends Component<Props> {
return;
}
if (event.key === 'Enter') {
if (event.key === 'Enter' && !this.props.disableEnter) {
event.preventDefault();
event.stopPropagation();

View File

@@ -21,7 +21,8 @@ ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
if (typeof component === 'undefined' || state.component === component) {
return assign(state, {
component: undefined,
componentProps: undefined
componentProps: undefined,
rawDialog: false
});
}
break;
@@ -30,7 +31,8 @@ ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
case OPEN_DIALOG:
return assign(state, {
component: action.component,
componentProps: action.componentProps
componentProps: action.componentProps,
rawDialog: action.rawDialog
});
}

View File

@@ -20,7 +20,7 @@ export const CALENDAR_ENABLED = 'calendar.enabled';
export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
/**
* Flag indicating if chat should be enabled.
* Flag indicating if close captions should be enabled.
* Default: enabled (true).
*/
export const CLOSE_CAPTIONS_ENABLED = 'close-captions.enabled';

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z" fill="#5E6D7A"/>
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 374 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -31,6 +31,7 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconDragHandle } from './drag-handle.svg';
export { default as IconE2EE } from './e2ee.svg';
export { default as IconEdit } from './edit.svg';
export { default as IconEmail } from './envelope.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExclamation } from './exclamation.svg';
@@ -46,6 +47,8 @@ export { default as IconInviteMore } from './user-plus.svg';
export { default as IconKick } from './kick.svg';
export { default as IconLiveStreaming } from './public.svg';
export { default as IconLockPassword } from './lock.svg';
export { default as IconMeetingLocked } from './meeting-locked.svg';
export { default as IconMeetingUnlocked } from './meeting-unlocked.svg';
export { default as IconMenu } from './menu.svg';
export { default as IconMenuDown } from './menu-down.svg';
export { default as IconMenuThumb } from './thumb-menu.svg';

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M11 11h-1v2h2v-1l9.73 9.73L20.46 23 14 16.54V21H3v-2h2V7.54l-4-4 1.27-1.27L11 11zm3 .49L5.51 3H14v1h5v12.49l-2-2V6h-3v5.49z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M14 6v15H3v-2h2V3h9v1h5v15h2v2h-4V6h-3zm-4 5v2h2v-2h-2z"/></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -141,7 +141,7 @@ function _setJWT(store, next, action) {
action.jwt = jwt;
action.issuer = iss;
if (context) {
const user = _user2participant(context.user);
const user = _user2participant(context.user || {});
action.callee = context.callee;
action.group = context.group;

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -0,0 +1,77 @@
// @flow
import React from 'react';
import { Icon, IconArrowDown } from '../../../icons';
type Props = {
/**
* Text of the button.
*/
children: React$Node,
/**
* Text css class of the button.
*/
className?: string,
/**
* If the button is disabled or not.
*/
disabled?: boolean,
/**
* If the button has options.
*/
hasOptions?: boolean,
/**
* The type of th button: primary, secondary, text.
*/
type: string,
/**
* OnClick button handler.
*/
onClick: Function,
/**
* Click handler for options.
*/
onOptionsClick?: Function
};
/**
* Button used for pre meeting actions.
*
* @returns {ReactElement}
*/
function ActionButton({
children,
className = '',
disabled,
hasOptions,
type = 'primary',
onClick,
onOptionsClick
}: Props) {
return (
<div
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
onClick = { disabled ? undefined : onClick }>
{children}
{hasOptions && <div
className = 'options'
onClick = { disabled ? undefined : onOptionsClick }>
<Icon
className = 'icon'
size = { 14 }
src = { IconArrowDown } />
</div>
}
</div>
);
}
export default ActionButton;

View File

@@ -2,10 +2,10 @@
import React, { Component } from 'react';
import { getCurrentConferenceUrl } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { Icon, IconCopy, IconCheck } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n';
import { Icon, IconCopy, IconCheck } from '../../../icons';
import { connect } from '../../../redux';
import logger from '../../logger';
type Props = {
@@ -108,7 +108,8 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
_hideCopyLink() {
this.setState({
showCopyLink: false
showCopyLink: false,
showLinkCopied: false
});
}
@@ -122,7 +123,8 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
_showCopyLink() {
this.setState({
showCopyLink: true
showCopyLink: true,
showLinkCopied: false
});
}
@@ -152,35 +154,30 @@ class CopyMeetingUrl extends Component<Props, State> {
const { url, t } = this.props;
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
const src = showLinkCopied ? IconCheck : IconCopy;
const iconCls = showCopyLink || showCopyLink ? 'prejoin-copy-icon--white' : 'prejoin-copy-icon--light';
return (
<div
className = 'prejoin-copy-meeting'
className = 'copy-meeting'
onMouseEnter = { _showCopyLink }
onMouseLeave = { _hideCopyLink }>
<div className = 'prejoin-copy-url'>{url}</div>
{showCopyLink && <div
className = 'prejoin-copy-badge prejoin-copy-badge--hover'
onClick = { _copyUrl }>
{t('prejoin.copyAndShare')}
</div>}
{showLinkCopied && <div
className = 'prejoin-copy-badge prejoin-copy-badge--done'>
{t('prejoin.linkCopied')}
</div>}
<Icon
className = { `prejoin-copy-icon ${iconCls}` }
onClick = { _copyUrl }
size = { 24 }
src = { src } />
<div
className = { `url ${showLinkCopied ? 'done' : ''}` }
onClick = { _copyUrl } >
{ !showCopyLink && !showLinkCopied && url }
{ showCopyLink && t('prejoin.copyAndShare') }
{ showLinkCopied && t('prejoin.linkCopied') }
<Icon
onClick = { _copyUrl }
size = { 24 }
src = { src } />
</div>
<textarea
className = 'prejoin-copy-textarea'
readOnly = { true }
ref = { this.textarea }
tabIndex = '-1'
value = { url } />
</div>);
</div>
);
}
}

View File

@@ -0,0 +1,175 @@
// @flow
import React, { PureComponent } from 'react';
import { getFieldValue } from '../../../react';
type Props = {
/**
* Class name to be appended to the default class list.
*/
className?: string,
/**
* Callback for the onChange event of the field.
*/
onChange: Function,
/**
* Callback to be used when the user hits Enter in the field.
*/
onSubmit?: Function,
/**
* Placeholder text for the field.
*/
placeHolder: string,
/**
* The field type (e.g. text, password...etc).
*/
type: string,
/**
* Externally provided value.
*/
value?: string
};
type State = {
/**
* True if the field is focused, false otherwise.
*/
focused: boolean,
/**
* The current value of the field.
*/
value: string
}
/**
* Implements a pre-styled input field to be used on pre-meeting screens.
*/
export default class InputField extends PureComponent<Props, State> {
static defaultProps: {
className: '',
type: 'text'
};
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
focused: false,
value: props.value || ''
};
this._onBlur = this._onBlur.bind(this);
this._onChange = this._onChange.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
}
/**
* Implements {@code PureComponent.getDerivedStateFromProps}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, state: State) {
const { value } = props;
if (state.value !== value) {
return {
...state,
value
};
}
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
return (
<input
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
onBlur = { this._onBlur }
onChange = { this._onChange }
onFocus = { this._onFocus }
onKeyDown = { this._onKeyDown }
placeholder = { this.props.placeHolder }
type = { this.props.type }
value = { this.state.value } />
);
}
_onBlur: () => void;
/**
* Callback for the onBlur event of the field.
*
* @returns {void}
*/
_onBlur() {
this.setState({
focused: false
});
}
_onChange: Object => void;
/**
* Callback for the onChange event of the field.
*
* @param {Object} evt - The static event.
* @returns {void}
*/
_onChange(evt) {
const value = getFieldValue(evt);
this.setState({
value
});
const { onChange } = this.props;
onChange && onChange(value);
}
_onFocus: () => void;
/**
* Callback for the onFocus event of the field.
*
* @returns {void}
*/
_onFocus() {
this.setState({
focused: true
});
}
_onKeyDown: Object => void;
/**
* Joins the conference on 'Enter'.
*
* @param {Event} event - Key down event object.
* @returns {void}
*/
_onKeyDown(event) {
const { onSubmit } = this.props;
onSubmit && event.key === 'Enter' && onSubmit();
}
}

View File

@@ -0,0 +1,73 @@
// @flow
import React, { PureComponent } from 'react';
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox';
import CopyMeetingUrl from './CopyMeetingUrl';
import Preview from './Preview';
type Props = {
/**
* Children component(s) to be rendered on the screen.
*/
children: React$Node,
/**
* Footer to be rendered for the page (if any).
*/
footer?: React$Node,
/**
* Title of the screen.
*/
title: string,
/**
* True if the preview overlay should be muted, false otherwise.
*/
videoMuted?: boolean,
/**
* The video track to render as preview (if omitted, the default local track will be rendered).
*/
videoTrack?: Object
}
/**
* Implements a pre-meeting screen that can be used at various pre-meeting phases, for example
* on the prejoin screen (pre-connection) or lobby (post-connection).
*/
export default class PreMeetingScreen extends PureComponent<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { title, videoMuted, videoTrack } = this.props;
return (
<div
className = 'premeeting-screen'
id = 'lobby-screen'>
<Preview
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
<div className = 'content'>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
{ this.props.children }
<div className = 'media-btn-container'>
<AudioSettingsButton visible = { true } />
<VideoSettingsButton visible = { true } />
</div>
{ this.props.footer }
</div>
</div>
);
}
}

View File

@@ -0,0 +1,73 @@
// @flow
import React from 'react';
import { Avatar } from '../../../avatar';
import { Video } from '../../../media';
import { connect } from '../../../redux';
import { getLocalVideoTrack } from '../../../tracks';
export type Props = {
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Flag signaling the visibility of camera preview.
*/
videoMuted: boolean,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
};
/**
* Component showing the video preview and device status.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { name, videoMuted, videoTrack } = props;
if (!videoMuted && videoTrack) {
return (
<div id = 'preview'>
<Video
className = 'flipVideoX'
videoTrack = {{ jitsiTrack: videoTrack }} />
</div>
);
}
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
size = { 200 } />
</div>
);
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
return {
videoMuted: ownProps.videoTrack ? ownProps.videoMuted : state['features/base/media'].video.muted,
videoTrack: ownProps.videoTrack || (getLocalVideoTrack(state['features/base/tracks']) || {}).jitsiTrack
};
}
export default connect(_mapStateToProps)(Preview);

View File

@@ -0,0 +1,5 @@
// @flow
export { default as ActionButton } from './ActionButton';
export { default as InputField } from './InputField';
export { default as PreMeetingScreen } from './PreMeetingScreen';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './components';

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/premeeting');

View File

@@ -21,11 +21,27 @@ const _RIGHT_WATERMARK_STYLE = {
*/
type Props = {
/**
* The user selected url used to navigate to on logo click.
*/
_customLogoLink: string,
/**
* The url of the user selected logo.
*/
_customLogoUrl: string,
/**
* Whether or not the current user is logged in through a JWT.
*/
_isGuest: boolean,
/**
* Flag used to signal that the logo can be displayed.
* It becomes true after the user customization options are fetched.
*/
_readyToDisplayJitsiWatermark: boolean,
/**
* Invoked to obtain translated strings.
*/
@@ -133,6 +149,26 @@ class Watermarks extends Component<Props, State> {
);
}
/**
* Returns true if the watermark is ready to be displayed.
*
* @private
* @returns {boolean}
*/
_canDisplayJitsiWatermark() {
const {
showJitsiWatermark,
showJitsiWatermarkForGuests
} = this.state;
const {
_isGuest,
_readyToDisplayJitsiWatermark
} = this.props;
return _readyToDisplayJitsiWatermark
&& (showJitsiWatermark || (_isGuest && showJitsiWatermarkForGuests));
}
/**
* Renders a brand watermark if it is enabled.
*
@@ -173,18 +209,27 @@ class Watermarks extends Component<Props, State> {
*/
_renderJitsiWatermark() {
let reactElement = null;
const {
_customLogoUrl,
_customLogoLink
} = this.props;
if (this.state.showJitsiWatermark
|| (this.props._isGuest
&& this.state.showJitsiWatermarkForGuests)) {
reactElement = <div className = 'watermark leftwatermark' />;
if (this._canDisplayJitsiWatermark()) {
const link = _customLogoLink || this.state.jitsiWatermarkLink;
const style = {
backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
maxWidth: 140,
maxHeight: 70
};
const { jitsiWatermarkLink } = this.state;
reactElement = (<div
className = 'watermark leftwatermark'
style = { style } />);
if (jitsiWatermarkLink) {
if (link) {
reactElement = (
<a
href = { jitsiWatermarkLink }
href = { link }
target = '_new'>
{ reactElement }
</a>
@@ -223,12 +268,11 @@ class Watermarks extends Component<Props, State> {
* Maps parts of Redux store to component prop types.
*
* @param {Object} state - Snapshot of Redux store.
* @returns {{
* _isGuest: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const { isGuest } = state['features/base/jwt'];
const { customizationReady, logoClickUrl, logoImageUrl } = state['features/dynamic-branding'];
return {
/**
@@ -238,7 +282,10 @@ function _mapStateToProps(state) {
* @private
* @type {boolean}
*/
_isGuest: isGuest
_customLogoLink: logoClickUrl,
_customLogoUrl: logoImageUrl,
_isGuest: isGuest,
_readyToDisplayJitsiWatermark: customizationReady
};
}

View File

@@ -0,0 +1,11 @@
// @flow
/**
* Returns the field value in a platform generic way.
*
* @param {Object | string} fieldParameter - The parameter passed through the change event function.
* @returns {string}
*/
export function getFieldValue(fieldParameter: Object | string) {
return typeof fieldParameter === 'string' ? fieldParameter : fieldParameter?.target?.value;
}

View File

@@ -1,3 +1,5 @@
export * from './components';
export * from './functions';
export { default as Platform } from './Platform';
export * from './Types';

View File

@@ -22,6 +22,7 @@ import {
} from '../../../filmstrip';
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { Captions } from '../../../subtitles';
import { isToolboxVisible, setToolboxVisible, Toolbox } from '../../../toolbox';
@@ -320,6 +321,7 @@ class Conference extends AbstractConference<Props, *> {
style = { styles.navBarSafeView }>
<NavigationBar />
{ this._renderNotificationsContainer() }
<KnockingParticipantList />
</SafeAreaView>
<TestConnectionInfo />
@@ -414,6 +416,7 @@ function _mapStateToProps(state) {
const {
conference,
joining,
membersOnly,
leaving
} = state['features/base/conference'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
@@ -428,7 +431,7 @@ function _mapStateToProps(state) {
// - the XMPP connection is connected and we have no conference yet, nor we
// are leaving one.
const connecting_
= connecting || (connection && (joining || (!conference && !leaving)));
= connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))));
return {
...abstractMapStateToProps(state),

View File

@@ -12,6 +12,7 @@ import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import {
Toolbox,
@@ -198,8 +199,8 @@ class Conference extends AbstractConference<Props, *> {
<InviteMore />
<div id = 'videospace'>
<LargeVideo />
{ hideLabels
|| <Labels /> }
<KnockingParticipantList />
{ hideLabels || <Labels /> }
<Filmstrip filmstripOnly = { filmstripOnly } />
</div>
@@ -208,9 +209,9 @@ class Conference extends AbstractConference<Props, *> {
{ this.renderNotificationsContainer() }
{ !filmstripOnly && _showPrejoin && <Prejoin />}
<CalleeInfoContainer />
{ !filmstripOnly && _showPrejoin && <Prejoin />}
</div>
);
}

View File

@@ -66,7 +66,7 @@ MiddlewareRegistry.register(store => next => action => {
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch, getState }, prevConference) => {
const { authRequired, passwordRequired }
const { authRequired, membersOnly, passwordRequired }
= getState()['features/base/conference'];
if (conference !== prevConference) {
@@ -80,6 +80,7 @@ StateListenerRegistry.register(
// and explicitly check.
if (typeof authRequired === 'undefined'
&& typeof passwordRequired === 'undefined'
&& typeof membersOnly === 'undefined'
&& !isDialogOpen(getState(), FeedbackDialog)) {
// Conference changed, left or failed... and there is no
// pending authentication, nor feedback request, so close any

View File

@@ -0,0 +1,9 @@
/**
* Action used to set custom user properties.
*/
export const SET_DYNAMIC_BRANDING_DATA = 'SET_DYNAMIC_BRANDING_DATA';
/**
* Action used to signal the branding elements are ready to be displayed
*/
export const SET_DYNAMIC_BRANDING_READY = 'SET_DYNAMIC_BRANDING_READY';

View File

@@ -0,0 +1,66 @@
// @flow
import { getLogger } from 'jitsi-meet-logger';
import { doGetJSON } from '../base/util';
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
import { extractFqnFromPath } from './functions';
const logger = getLogger(__filename);
/**
* Fetches custom branding data.
* If there is no data or the request fails, sets the `customizationReady` flag
* so the defaults can be displayed.
*
* @returns {Function}
*/
export function fetchCustomBrandingData() {
return async function(dispatch: Function, getState: Function) {
const state = getState();
const baseUrl = state['features/base/config'].brandingDataUrl;
const { customizationReady } = state['features/dynamic-branding'];
if (!customizationReady) {
const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
if (baseUrl && fqn) {
try {
const res = await doGetJSON(`${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`);
return dispatch(setDynamicBrandingData(res));
} catch (err) {
logger.error('Error fetching branding data', err);
}
}
dispatch(setDynamicBrandingReady());
}
};
}
/**
* Action used to set the user customizations.
*
* @param {Object} value - The custom data to be set.
* @returns {Object}
*/
function setDynamicBrandingData(value) {
return {
type: SET_DYNAMIC_BRANDING_DATA,
value
};
}
/**
* Action used to signal the branding elements are ready to be displayed.
*
* @returns {Object}
*/
function setDynamicBrandingReady() {
return {
type: SET_DYNAMIC_BRANDING_READY
};
}

View File

@@ -0,0 +1,15 @@
// @flow
/**
* Extracts the fqn part from a path, where fqn represents
* tenant/roomName.
*
* @param {string} path - The URL path.
* @returns {string}
*/
export function extractFqnFromPath(path: string) {
const parts = path.split('/');
const len = parts.length;
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
}

View File

@@ -0,0 +1,4 @@
export * from './actions';
export * from './functions';
import './reducer';

View File

@@ -0,0 +1,46 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
/**
* The name of the redux store/state property which is the root of the redux
* state of the feature {@code dynamic-branding}.
*/
const STORE_NAME = 'features/dynamic-branding';
const DEFAULT_STATE = {
backgroundColor: '',
backgroundImageUrl: '',
customizationReady: false,
logoClickUrl: '',
logoImageUrl: ''
};
/**
* Reduces redux actions for the purposes of the feature {@code dynamic-branding}.
*/
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_DYNAMIC_BRANDING_DATA: {
const { backgroundColor, backgroundImageUrl, logoClickUrl, logoImageUrl } = action.value;
return {
backgroundColor,
backgroundImageUrl,
logoClickUrl,
logoImageUrl,
customizationReady: true
};
}
case SET_DYNAMIC_BRANDING_READY:
return {
...state,
customizationReady: true
};
}
return state;
});

View File

@@ -4,12 +4,28 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { fetchCustomBrandingData } from '../../dynamic-branding';
import { Captions } from '../../subtitles/';
declare var interfaceConfig: Object;
type Props = {
/**
* The user selected background color.
*/
_customBackgroundColor: string,
/**
* The user selected background image url.
*/
_customBackgroundImageUrl: string,
/**
* Fetches the branding data.
*/
_fetchCustomBrandingData: Function,
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
@@ -24,6 +40,15 @@ type Props = {
* @extends Component
*/
class LargeVideo extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this.props._fetchCustomBrandingData();
}
/**
* Implements React's {@link Component#render()}.
*
@@ -31,10 +56,13 @@ class LargeVideo extends Component<Props> {
* @returns {React$Element}
*/
render() {
const style = this._getCustomSyles();
return (
<div
className = 'videocontainer'
id = 'largeVideoContainer'>
id = 'largeVideoContainer'
style = { style }>
<div id = 'sharedVideo'>
<div id = 'sharedVideoIFrame' />
</div>
@@ -72,6 +100,26 @@ class LargeVideo extends Component<Props> {
</div>
);
}
/**
* Creates the custom styles object.
*
* @private
* @returns {Object}
*/
_getCustomSyles() {
const styles = {};
const { _customBackgroundColor, _customBackgroundImageUrl } = this.props;
styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
if (_customBackgroundImageUrl) {
styles.backgroundImage = `url(${_customBackgroundImageUrl})`;
styles.backgroundSize = 'cover';
}
return styles;
}
}
@@ -80,17 +128,21 @@ class LargeVideo extends Component<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _noAutoPlayVideo: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const testingConfig = state['features/base/config'].testing;
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
return {
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
};
}
const _mapDispatchToProps = {
_fetchCustomBrandingData: fetchCustomBrandingData
};
export default connect(_mapStateToProps)(LargeVideo);
export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);

View File

@@ -0,0 +1,26 @@
// @flow
/**
* Action type to signal the arriving or updating of a knocking participant.
*/
export const KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED = 'KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED';
/**
* Action type to signal the leave of a knocking participant.
*/
export const KNOCKING_PARTICIPANT_LEFT = 'KNOCKING_PARTICIPANT_LEFT';
/**
* Action type to set the new state of the lobby mode.
*/
export const SET_LOBBY_MODE_ENABLED = 'SET_LOBBY_MODE_ENABLED';
/**
* Action type to set the knocking state of the participant.
*/
export const SET_KNOCKING_STATE = 'SET_KNOCKING_STATE';
/**
* Action type to set the password join failed status.
*/
export const SET_PASSWORD_JOIN_FAILED = 'SET_PASSWORD_JOIN_FAILED';

View File

@@ -0,0 +1,25 @@
// @flow
import { openDialog } from '../base/dialog';
import { DisableLobbyModeDialog, EnableLobbyModeDialog } from './components/native';
export * from './actions.web';
/**
* Action to show the dialog to disable lobby mode.
*
* @returns {showNotification}
*/
export function showDisableLobbyModeDialog() {
return openDialog(DisableLobbyModeDialog);
}
/**
* Action to show the dialog to enable lobby mode.
*
* @returns {showNotification}
*/
export function showEnableLobbyModeDialog() {
return openDialog(EnableLobbyModeDialog);
}

View File

@@ -0,0 +1,207 @@
// @flow
import { type Dispatch } from 'redux';
import { appNavigate, maybeRedirectToWelcomePage } from '../app';
import { conferenceWillJoin, getCurrentConference, setPassword } from '../base/conference';
import { hideDialog, openDialog } from '../base/dialog';
import { getLocalParticipant } from '../base/participants';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
import { LobbyScreen } from './components';
declare var APP: Object;
/**
* Cancels the ongoing knocking and abandones the join flow.
*
* @returns {Function}
*/
export function cancelKnocking() {
return async (dispatch: Dispatch<any>) => {
if (typeof APP !== 'undefined') {
// when we are redirecting the library should handle any
// unload and clean of the connection.
APP.API.notifyReadyToClose();
dispatch(maybeRedirectToWelcomePage());
return;
}
dispatch(appNavigate(undefined));
};
}
/**
* Action to hide the lobby screen.
*
* @returns {hideDialog}
*/
export function hideLobbyScreen() {
return hideDialog(LobbyScreen);
}
/**
* Tries to join with a preset password.
*
* @param {string} password - The password to join with.
* @returns {Function}
*/
export function joinWithPassword(password: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
dispatch(setPassword(conference, conference.join, password));
};
}
/**
* Action to be dispatched when a knocking poarticipant leaves before any response.
*
* @param {string} id - The ID of the participant.
* @returns {{
* id: string,
* type: KNOCKING_PARTICIPANT_LEFT
* }}
*/
export function knockingParticipantLeft(id: string) {
return {
id,
type: KNOCKING_PARTICIPANT_LEFT
};
}
/**
* Action to open the lobby screen.
*
* @returns {openDialog}
*/
export function openLobbyScreen() {
return openDialog(LobbyScreen, {}, true);
}
/**
* Action to be executed when a participant starts knocking or an already knocking participant gets updated.
*
* @param {Object} participant - The knocking participant.
* @returns {{
* participant: Object,
* type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
* }}
*/
export function participantIsKnockingOrUpdated(participant: Object) {
return {
participant,
type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
};
}
/**
* Approves (lets in) or rejects a knocking participant.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approved - True if the participant is approved, false otherwise.
* @returns {Function}
*/
export function setKnockingParticipantApproval(id: string, approved: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (conference) {
if (approved) {
conference.lobbyApproveAccess(id);
} else {
conference.lobbyDenyAccess(id);
}
}
};
}
/**
* Action to set the knocking state of the participant.
*
* @param {boolean} knocking - The new state.
* @returns {{
* state: boolean,
* type: SET_KNOCKING_STATE
* }}
*/
export function setKnockingState(knocking: boolean) {
return {
knocking,
type: SET_KNOCKING_STATE
};
}
/**
* Action to set the new state of the lobby mode.
*
* @param {boolean} enabled - The new state to set.
* @returns {{
* enabled: boolean,
* type: SET_LOBBY_MODE_ENABLED
* }}
*/
export function setLobbyModeEnabled(enabled: boolean) {
return {
enabled,
type: SET_LOBBY_MODE_ENABLED
};
}
/**
* Action to be dispatched when we failed to join with a password.
*
* @param {boolean} failed - True of recent password join failed.
* @returns {{
* failed: boolean,
* type: SET_PASSWORD_JOIN_FAILED
* }}
*/
export function setPasswordJoinFailed(failed: boolean) {
return {
failed,
type: SET_PASSWORD_JOIN_FAILED
};
}
/**
* Starts knocking and waiting for approval.
*
* @returns {Function}
*/
export function startKnocking() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
dispatch(conferenceWillJoin(membersOnly));
membersOnly.joinLobby(localParticipant.name, localParticipant.email);
dispatch(setKnockingState(true));
};
}
/**
* Action to toggle lobby mode on or off.
*
* @param {boolean} enabled - The desired (new) state of the lobby mode.
* @returns {Function}
*/
export function toggleLobbyMode(enabled: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (enabled) {
conference.enableLobby();
} else {
conference.disableLobby();
}
};
}

View File

@@ -0,0 +1,75 @@
// @flow
import { PureComponent } from 'react';
import { isLocalParticipantModerator } from '../../base/participants';
import { setKnockingParticipantApproval } from '../actions';
export type Props = {
/**
* The list of participants.
*/
_participants: Array<Object>,
/**
* True if the list should be rendered.
*/
_visible: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Abstract class to encapsulate the platform common code of the {@code KnockingParticipantList}.
*/
export default class AbstractKnockingParticipantList<P: Props = Props> extends PureComponent<P> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
}
_onRespondToParticipant: (string, boolean) => Function;
/**
* Function that constructs a callback for the response handler button.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approve - The response for the knocking.
* @returns {Function}
*/
_onRespondToParticipant(id, approve) {
return () => {
this.props.dispatch(setKnockingParticipantApproval(id, approve));
};
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
export function mapStateToProps(state: Object): $Shape<Props> {
const { knockingParticipants, lobbyEnabled } = state['features/lobby'];
return {
_participants: knockingParticipants,
_visible: lobbyEnabled && isLocalParticipantModerator(state) && Boolean(knockingParticipants.length)
};
}

View File

@@ -0,0 +1,375 @@
// @flow
// eslint-disable-next-line no-unused-vars
import React, { PureComponent } from 'react';
import { getConferenceName } from '../../base/conference';
import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
import { updateSettings } from '../../base/settings';
import { cancelKnocking, joinWithPassword, setPasswordJoinFailed, startKnocking } from '../actions';
export const SCREEN_STATES = {
EDIT: 1,
PASSWORD: 2,
VIEW: 3
};
export type Props = {
/**
* True if knocking is already happening, so we're waiting for a response.
*/
_knocking: boolean,
/**
* The name of the meeting we're about to join.
*/
_meetingName: string,
/**
* The email of the participant about to knock/join.
*/
_participantEmail: string,
/**
* The id of the participant about to knock/join. This is the participant ID in the lobby room, at this point.
*/
_participantId: string,
/**
* The name of the participant about to knock/join.
*/
_participantName: string;
/**
* True if a recent attempt to join with password failed.
*/
_passwordJoinFailed: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
/**
* The display name value entered into the field.
*/
displayName: string,
/**
* The email value entered into the field.
*/
email: string,
/**
* The password value entered into the field.
*/
password: string,
/**
* True if a recent attempt to join with password failed.
*/
passwordJoinFailed: boolean,
/**
* The state of the screen. One of {@code SCREEN_STATES[*]}
*/
screenState: number
}
/**
* Abstract class to encapsulate the platform common code of the {@code LobbyScreen}.
*/
export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent<P, State> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this.state = {
displayName: props._participantName || '',
email: props._participantEmail || '',
password: '',
passwordJoinFailed: false,
screenState: props._participantName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
};
this._onAskToJoin = this._onAskToJoin.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
this._onChangeEmail = this._onChangeEmail.bind(this);
this._onChangePassword = this._onChangePassword.bind(this);
this._onEnableEdit = this._onEnableEdit.bind(this);
this._onJoinWithPassword = this._onJoinWithPassword.bind(this);
this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
}
/**
* Implements {@code PureComponent.getDerivedStateFromProps}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, state: State) {
if (props._passwordJoinFailed && !state.passwordJoinFailed) {
return {
password: '',
passwordJoinFailed: true
};
}
}
/**
* Returns the screen title.
*
* @returns {string}
*/
_getScreenTitleKey() {
const { screenState } = this.state;
const passwordPrompt = screenState === SCREEN_STATES.PASSWORD;
return !passwordPrompt && this.props._knocking
? 'lobby.joiningTitle'
: passwordPrompt ? 'lobby.enterPasswordTitle' : 'lobby.joinTitle';
}
_onAskToJoin: () => void;
/**
* Callback to be invoked when the user submits the joining request.
*
* @returns {void}
*/
_onAskToJoin() {
this.setState({
password: ''
});
this.props.dispatch(startKnocking());
return false;
}
_onCancel: () => boolean;
/**
* Callback to be invoked when the user cancels the dialog.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(cancelKnocking());
return true;
}
_onChangeDisplayName: Object => void;
/**
* Callback to be invoked when the user changes its display name.
*
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
* @returns {void}
*/
_onChangeDisplayName(event) {
const displayName = getFieldValue(event);
this.setState({
displayName
}, () => {
this.props.dispatch(updateSettings({
displayName
}));
});
}
_onChangeEmail: Object => void;
/**
* Callback to be invoked when the user changes its email.
*
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
* @returns {void}
*/
_onChangeEmail(event) {
const email = getFieldValue(event);
this.setState({
email
}, () => {
this.props.dispatch(updateSettings({
email
}));
});
}
_onChangePassword: Object => void;
/**
* Callback to be invoked when the user changes the password.
*
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
* @returns {void}
*/
_onChangePassword(event) {
this.setState({
password: getFieldValue(event)
});
}
_onEnableEdit: () => void;
/**
* Callback to be invoked for the edit button.
*
* @returns {void}
*/
_onEnableEdit() {
this.setState({
screenState: SCREEN_STATES.EDIT
});
}
_onJoinWithPassword: () => void;
/**
* Callback to be invoked when the user tries to join using a preset password.
*
* @returns {void}
*/
_onJoinWithPassword() {
this.setState({
passwordJoinFailed: false
});
this.props.dispatch(joinWithPassword(this.state.password));
}
_onSwitchToKnockMode: () => void;
/**
* Callback to be invoked for the enter (go back to) knocking mode button.
*
* @returns {void}
*/
_onSwitchToKnockMode() {
this.setState({
password: '',
screenState: this.state.displayName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
});
this.props.dispatch(setPasswordJoinFailed(false));
}
_onSwitchToPasswordMode: () => void;
/**
* Callback to be invoked for the enter password button.
*
* @returns {void}
*/
_onSwitchToPasswordMode() {
this.setState({
screenState: SCREEN_STATES.PASSWORD
});
}
/**
* Renders the content of the dialog.
*
* @returns {React$Element}
*/
_renderContent() {
const { _knocking } = this.props;
const { screenState } = this.state;
if (screenState !== SCREEN_STATES.PASSWORD && _knocking) {
return this._renderJoining();
}
return (
<>
{ screenState === SCREEN_STATES.VIEW && this._renderParticipantInfo() }
{ screenState === SCREEN_STATES.EDIT && this._renderParticipantForm() }
{ screenState === SCREEN_STATES.PASSWORD && this._renderPasswordForm() }
{ (screenState === SCREEN_STATES.VIEW || screenState === SCREEN_STATES.EDIT)
&& this._renderStandardButtons() }
{ screenState === SCREEN_STATES.PASSWORD && this._renderPasswordJoinButtons() }
</>
);
}
/**
* Renders the joining (waiting) fragment of the screen.
*
* @returns {React$Element}
*/
_renderJoining: () => React$Element<*>;
/**
* Renders the participant form to let the knocking participant enter its details.
*
* @returns {React$Element}
*/
_renderParticipantForm: () => React$Element<*>;
/**
* Renders the participant info fragment when we have all the required details of the user.
*
* @returns {React$Element}
*/
_renderParticipantInfo: () => React$Element<*>;
/**
* Renders the password form to let the participant join by using a password instead of knocking.
*
* @returns {React$Element}
*/
_renderPasswordForm: () => React$Element<*>;
/**
* Renders the password join button (set).
*
* @returns {React$Element}
*/
_renderPasswordJoinButtons: () => React$Element<*>;
/**
* Renders the standard (pre-knocking) button set.
*
* @returns {React$Element}
*/
_renderStandardButtons: () => React$Element<*>;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const localParticipant = getLocalParticipant(state);
const participantId = localParticipant?.id;
const { knocking, passwordJoinFailed } = state['features/lobby'];
return {
_knocking: knocking,
_meetingName: getConferenceName(state),
_participantEmail: localParticipant?.email,
_participantId: participantId,
_participantName: localParticipant?.name,
_passwordJoinFailed: passwordJoinFailed
};
}

View File

@@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -0,0 +1,60 @@
// @flow
import React, { PureComponent } from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { toggleLobbyMode } from '../../actions';
export type Props = {
/**
* The Redux Dispatch function.
*/
dispatch: Function
};
/**
* Implements a dialog that lets the user disable the lobby mode.
*/
class DisableLobbyModeDialog extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this._onDisableLobbyMode = this._onDisableLobbyMode.bind(this);
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
return (
<ConfirmDialog
contentKey = 'lobby.disableDialogContent'
onSubmit = { this._onDisableLobbyMode } />
);
}
_onDisableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode disable flow.
*
* @returns {void}
*/
_onDisableLobbyMode() {
this.props.dispatch(toggleLobbyMode(false));
return true;
}
}
export default translate(connect()(DisableLobbyModeDialog));

View File

@@ -0,0 +1,88 @@
// @flow
import React, { PureComponent } from 'react';
import { Text, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { CustomSubmitDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { toggleLobbyMode } from '../../actions';
import styles from './styles';
type Props = {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Implements a dialog that lets the user enable the lobby mode.
*/
class EnableLobbyModeDialog extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onEnableLobbyMode = this._onEnableLobbyMode.bind(this);
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
return (
<CustomSubmitDialog
okKey = 'lobby.enableDialogSubmit'
onSubmit = { this._onEnableLobbyMode }
titleKey = 'lobby.dialogTitle'>
<View style = { styles.formWrapper }>
<Text>
{ this.props.t('lobby.enableDialogText') }
</Text>
</View>
</CustomSubmitDialog>
);
}
_onEnableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode enable flow.
*
* @returns {void}
*/
_onEnableLobbyMode() {
this.props.dispatch(toggleLobbyMode(true));
return true;
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): Object {
return {
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
};
}
export default translate(connect(_mapStateToProps)(EnableLobbyModeDialog));

View File

@@ -0,0 +1,99 @@
// @flow
import React from 'react';
import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props
} from '../AbstractKnockingParticipantList';
import styles from './styles';
/**
* Component to render a list for the actively knocking participants.
*/
class KnockingParticipantList extends AbstractKnockingParticipantList {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _participants, _visible, t } = this.props;
if (!_visible) {
return null;
}
return (
<ScrollView
style = { styles.knockingParticipantList }>
{ _participants.map(p => (
<View
key = { p.id }
style = { styles.knockingParticipantListEntry }>
<Avatar
displayName = { p.name }
size = { 48 }
url = { p.loadableAvatarUrl } />
<View style = { styles.knockingParticipantListDetails }>
<Text style = { styles.knockingParticipantListText }>
{ p.name }
</Text>
{ p.email && (
<Text style = { styles.knockingParticipantListText }>
{ p.email }
</Text>
) }
</View>
<TouchableOpacity
onPress = { this._onRespondToParticipant(p.id, true) }
style = { [
styles.knockingParticipantListButton,
styles.knockingParticipantListPrimaryButton
] }>
<Text style = { styles.knockingParticipantListText }>
{ t('lobby.allow') }
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { this._onRespondToParticipant(p.id, false) }
style = { [
styles.knockingParticipantListButton,
styles.knockingParticipantListSecondaryButton
] }>
<Text style = { styles.knockingParticipantListText }>
{ t('lobby.reject') }
</Text>
</TouchableOpacity>
</View>
)) }
</ScrollView>
);
}
_onRespondToParticipant: (string, boolean) => Function;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
const abstractProps = abstractMapStateToProps(state);
return {
...abstractProps,
// On mobile we only show a portion of the list for screen real estate reasons
_participants: abstractProps._participants.slice(0, 2)
};
}
export default translate(connect(_mapStateToProps)(KnockingParticipantList));

View File

@@ -0,0 +1,77 @@
// @flow
import { getCurrentConference } from '../../../base/conference';
import { translate } from '../../../base/i18n';
import { IconMeetingUnlocked, IconMeetingLocked } from '../../../base/icons';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractButton, { type Props as AbstractProps } from '../../../base/toolbox/components/AbstractButton';
import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../../actions.native';
type Props = AbstractProps & {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* True if the lobby mode is currently enabled for this conference.
*/
lobbyEnabled: boolean
};
/**
* Component to render the lobby mode initiator button.
*/
class LobbyModeButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.lobbyButton';
icon = IconMeetingUnlocked;
label = 'toolbar.lobbyButtonEnable';
toggledLabel = 'toolbar.lobbyButtonDisable'
toggledIcon = IconMeetingLocked;
/**
* Callback for the click event of the button.
*
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
if (this._isToggled()) {
dispatch(showDisableLobbyModeDialog());
} else {
dispatch(showEnableLobbyModeDialog());
}
}
/**
* Function to define the button state.
*
* @returns {boolean}
*/
_isToggled() {
return this.props.lobbyEnabled;
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const conference = getCurrentConference(state);
const { lobbyEnabled } = state['features/lobby'];
const lobbySupported = conference && conference.isLobbySupported();
return {
lobbyEnabled,
visible: lobbySupported && isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(LobbyModeButton));

View File

@@ -0,0 +1,241 @@
// @flow
import React from 'react';
import { Text, View, TouchableOpacity, TextInput } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { CustomDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { Icon, IconEdit } from '../../../base/icons';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
import styles from './styles';
/**
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _meetingName, t } = this.props;
return (
<CustomDialog
onCancel = { this._onCancel }
style = { styles.contentWrapper }>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey()) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent() }
</CustomDialog>
);
}
_getScreenTitleKey: () => string;
_onAskToJoin: () => void;
_onCancel: () => boolean;
_onChangeDisplayName: Object => void;
_onChangeEmail: Object => void;
_onChangePassword: Object => void;
_onEnableEdit: () => void;
_onJoinWithPassword: () => void;
_onSwitchToKnockMode: () => void;
_onSwitchToPasswordMode: () => void;
_renderContent: () => React$Element<*>;
/**
* Renders the joining (waiting) fragment of the screen.
*
* @inheritdoc
*/
_renderJoining() {
return (
<>
<LoadingIndicator
color = 'black'
style = { styles.loadingIndicator } />
<Text style = { styles.joiningMessage }>
{ this.props.t('lobby.joiningMessage') }
</Text>
{ this._renderStandardButtons() }
</>
);
}
/**
* Renders the participant form to let the knocking participant enter its details.
*
* @inheritdoc
*/
_renderParticipantForm() {
const { t } = this.props;
const { displayName, email } = this.state;
return (
<View style = { styles.formWrapper }>
<Text style = { styles.fieldLabel }>
{ t('lobby.nameField') }
</Text>
<TextInput
onChangeText = { this._onChangeDisplayName }
style = { styles.field }
value = { displayName } />
<Text style = { styles.fieldLabel }>
{ t('lobby.emailField') }
</Text>
<TextInput
onChangeText = { this._onChangeEmail }
style = { styles.field }
value = { email } />
</View>
);
}
/**
* Renders the participant info fragment when we have all the required details of the user.
*
* @inheritdoc
*/
_renderParticipantInfo() {
const { displayName, email } = this.state;
return (
<View style = { styles.participantBox }>
<TouchableOpacity
onPress = { this._onEnableEdit }
style = { styles.editButton }>
<Icon
src = { IconEdit }
style = { styles.editIcon } />
</TouchableOpacity>
<Avatar
participantId = { this.props._participantId }
size = { 64 } />
<Text style = { styles.displayNameText }>
{ displayName }
</Text>
{ Boolean(email) && <Text style = { styles.secondaryText }>
{ email }
</Text> }
</View>
);
}
/**
* Renders the password form to let the participant join by using a password instead of knocking.
*
* @inheritdoc
*/
_renderPasswordForm() {
const { _passwordJoinFailed, t } = this.props;
return (
<View style = { styles.formWrapper }>
<Text style = { styles.fieldLabel }>
{ this.props.t('lobby.passwordField') }
</Text>
<TextInput
autoCapitalize = 'none'
autoCompleteType = 'off'
onChangeText = { this._onChangePassword }
secureTextEntry = { true }
style = { styles.field }
value = { this.state.password } />
{ _passwordJoinFailed && <Text style = { styles.fieldError }>
{ t('lobby.invalidPassword') }
</Text> }
</View>
);
}
/**
* Renders the password join button (set).
*
* @inheritdoc
*/
_renderPasswordJoinButtons() {
const { t } = this.props;
return (
<>
<TouchableOpacity
disabled = { !this.state.password }
onPress = { this._onJoinWithPassword }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.passwordJoinButton') }
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { this._onSwitchToKnockMode }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text>
{ t('lobby.backToKnockModeButton') }
</Text>
</TouchableOpacity>
</>
);
}
/**
* Renders the standard button set.
*
* @inheritdoc
*/
_renderStandardButtons() {
const { _knocking, t } = this.props;
return (
<>
{ _knocking || <TouchableOpacity
disabled = { !this.state.displayName }
onPress = { this._onAskToJoin }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity> }
<TouchableOpacity
onPress = { this._onSwitchToPasswordMode }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text>
{ t('lobby.enterPasswordButton') }
</Text>
</TouchableOpacity>
</>
);
}
}
export default translate(connect(_mapStateToProps)(LobbyScreen));

View File

@@ -0,0 +1,7 @@
// @flow
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyModeButton } from './LobbyModeButton';
export { default as LobbyScreen } from './LobbyScreen';

View File

@@ -0,0 +1,142 @@
// @flow
import { ColorPalette } from '../../../base/styles';
const SECONDARY_COLOR = '#B8C7E0';
export default {
button: {
alignItems: 'center',
borderRadius: 4,
marginVertical: 8,
paddingVertical: 10
},
contentWrapper: {
alignItems: 'center',
flexDirection: 'column',
padding: 32
},
dialogTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10
},
displayNameText: {
fontWeight: 'bold',
marginVertical: 10
},
editButton: {
alignSelf: 'flex-end',
paddingHorizontal: 10
},
editIcon: {
color: 'black',
fontSize: 16
},
field: {
borderColor: SECONDARY_COLOR,
borderRadius: 4,
borderWidth: 1,
marginVertical: 8,
padding: 8
},
fieldError: {
color: ColorPalette.warning,
fontSize: 10
},
fieldRow: {
paddingTop: 16
},
fieldLabel: {
textAlign: 'center'
},
formWrapper: {
alignItems: 'stretch',
alignSelf: 'stretch',
paddingVertical: 16
},
joiningMessage: {
textAlign: 'center'
},
loadingIndicator: {
marginVertical: 36
},
participantBox: {
alignItems: 'center',
alignSelf: 'stretch',
borderColor: SECONDARY_COLOR,
borderRadius: 4,
borderWidth: 1,
marginVertical: 18,
paddingVertical: 12
},
primaryButton: {
alignSelf: 'stretch',
backgroundColor: 'rgb(3, 118, 218)'
},
primaryButtonText: {
color: 'white'
},
secondaryButton: {
alignSelf: 'stretch',
backgroundColor: 'transparent'
},
secondaryText: {
color: 'rgba(0, 0, 0, .7)'
},
// KnockingParticipantList
knockingParticipantList: {
alignSelf: 'stretch',
backgroundColor: 'rgba(22, 38, 55, 0.8)',
flexDirection: 'column'
},
knockingParticipantListButton: {
borderRadius: 4,
marginHorizontal: 3,
paddingHorizontal: 10,
paddingVertical: 5
},
knockingParticipantListDetails: {
flex: 1,
marginLeft: 10
},
knockingParticipantListEntry: {
alignItems: 'center',
flexDirection: 'row',
padding: 10
},
knockingParticipantListPrimaryButton: {
backgroundColor: 'rgb(3, 118, 218)'
},
knockingParticipantListSecondaryButton: {
backgroundColor: 'transparent'
},
knockingParticipantListText: {
color: 'white'
}
};

View File

@@ -0,0 +1,97 @@
// @flow
import React from 'react';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { isToolboxVisible } from '../../../toolbox';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractKnockingParticipantList';
type Props = AbstractProps & {
/**
* True if the toolbox is visible, so we need to adjust the position.
*/
_toolboxVisible: boolean,
};
/**
* Component to render a list for the actively knocking participants.
*/
class KnockingParticipantList extends AbstractKnockingParticipantList<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _participants, _toolboxVisible, _visible, t } = this.props;
if (!_visible) {
return null;
}
return (
<div
className = { _toolboxVisible ? 'toolbox-visible' : '' }
id = 'knocking-participant-list'>
<span className = 'title'>
Knocking participant list
</span>
<ul>
{ _participants.map(p => (
<li key = { p.id }>
<Avatar
displayName = { p.name }
size = { 48 }
url = { p.loadableAvatarUrl } />
<div className = 'details'>
<span>
{ p.name }
</span>
{ p.email && (
<span>
{ p.email }
</span>
) }
</div>
<button
className = 'primary'
onClick = { this._onRespondToParticipant(p.id, true) }
type = 'button'>
{ t('lobby.allow') }
</button>
<button
className = 'borderLess'
onClick = { this._onRespondToParticipant(p.id, false) }
type = 'button'>
{ t('lobby.reject') }
</button>
</li>
)) }
</ul>
</div>
);
}
_onRespondToParticipant: (string, boolean) => Function;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
return {
...abstractMapStateToProps(state),
_toolboxVisible: isToolboxVisible(state)
};
}
export default translate(connect(_mapStateToProps)(KnockingParticipantList));

View File

@@ -0,0 +1,170 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { ActionButton, InputField, PreMeetingScreen } from '../../../base/premeeting';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractLobbyScreen, {
_mapStateToProps
} from '../AbstractLobbyScreen';
/**
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
return (
<PreMeetingScreen title = { this.props.t(this._getScreenTitleKey()) }>
{ this._renderContent() }
</PreMeetingScreen>
);
}
_getScreenTitleKey: () => string;
_onAskToJoin: () => boolean;
_onCancel: () => boolean;
_onChangeDisplayName: Object => void;
_onChangeEmail: Object => void;
_onChangePassword: Object => void;
_onEnableEdit: () => void;
_onJoinWithPassword: () => void;
_onSubmit: () => boolean;
_onSwitchToKnockMode: () => void;
_onSwitchToPasswordMode: () => void;
_renderContent: () => React$Element<*>;
/**
* Renders the joining (waiting) fragment of the screen.
*
* @inheritdoc
*/
_renderJoining() {
return (
<div className = 'container'>
<div className = 'spinner'>
<LoadingIndicator size = 'large' />
</div>
<span className = 'joining-message'>
{ this.props.t('lobby.joiningMessage') }
</span>
{ this._renderStandardButtons() }
</div>
);
}
/**
* Renders the participant info fragment when we have all the required details of the user.
*
* @inheritdoc
*/
_renderParticipantInfo() {
const { displayName, email } = this.state;
const { t } = this.props;
return (
<div className = 'participant-info'>
<div className = 'form'>
<InputField
onChange = { this._onChangeDisplayName }
placeHolder = { t('lobby.nameField') }
value = { displayName } />
<InputField
onChange = { this._onChangeEmail }
placeHolder = { t('lobby.emailField') }
value = { email } />
</div>
</div>
);
}
/**
* Renders the password form to let the participant join by using a password instead of knocking.
*
* @inheritdoc
*/
_renderPasswordForm() {
const { _passwordJoinFailed, t } = this.props;
return (
<div className = 'form'>
<InputField
className = { _passwordJoinFailed ? 'error' : '' }
onChange = { this._onChangePassword }
placeHolder = { _passwordJoinFailed ? t('lobby.invalidPassword') : t('lobby.passwordField') }
type = 'password'
value = { this.state.password } />
</div>
);
}
/**
* Renders the password join button (set).
*
* @inheritdoc
*/
_renderPasswordJoinButtons() {
const { t } = this.props;
return (
<>
<ActionButton
disabled = { !this.state.password }
onClick = { this._onJoinWithPassword }
type = 'primary'>
{ t('lobby.passwordJoinButton') }
</ActionButton>
<ActionButton
onClick = { this._onSwitchToKnockMode }
type = 'secondary'>
{ t('lobby.backToKnockModeButton') }
</ActionButton>
</>
);
}
/**
* Renders the standard button set.
*
* @inheritdoc
*/
_renderStandardButtons() {
const { _knocking, t } = this.props;
return (
<>
{ _knocking || <ActionButton
disabled = { !this.state.displayName }
onClick = { this._onAskToJoin }
type = 'primary'>
{ t('lobby.knockButton') }
</ActionButton> }
<ActionButton
onClick = { this._onSwitchToPasswordMode }
type = 'secondary'>
{ t('lobby.enterPasswordButton') }
</ActionButton>
</>
);
}
}
export default translate(connect(_mapStateToProps)(LobbyScreen));

View File

@@ -0,0 +1,136 @@
// @flow
import React, { PureComponent } from 'react';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { Switch } from '../../../base/react';
import { connect } from '../../../base/redux';
import { toggleLobbyMode } from '../../actions';
type Props = {
/**
* True if lobby is currently enabled in the conference.
*/
_lobbyEnabled: boolean,
/**
* True if the section should be visible.
*/
_visible: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
/**
* True if the lobby switch is toggled on.
*/
lobbyEnabled: boolean
}
/**
* Implements a security feature section to control lobby mode.
*/
class LobbySection extends PureComponent<Props, State> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
lobbyEnabled: props._lobbyEnabled
};
this._onToggleLobby = this._onToggleLobby.bind(this);
}
/**
* Implements {@code PureComponent#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps, prevState) {
if (this.props._lobbyEnabled !== prevProps._lobbyEnabled
&& this.state.lobbyEnabled !== prevState.lobbyEnabled) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lobbyEnabled: this.props._lobbyEnabled
});
}
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _visible, t } = this.props;
if (!_visible) {
return null;
}
return (
<div id = 'lobby-section'>
{ t('lobby.enableDialogText') }
<div className = 'control-row'>
<label>
{ t('lobby.toggleLabel') }
</label>
<Switch
onValueChange = { this._onToggleLobby }
value = { this.state.lobbyEnabled } />
</div>
</div>
);
}
_onToggleLobby: () => void;
/**
* Callback to be invoked when the user toggles the lobby feature on or off.
*
* @returns {void}
*/
_onToggleLobby() {
const newValue = !this.state.lobbyEnabled;
this.setState({
lobbyEnabled: newValue
});
this.props.dispatch(toggleLobbyMode(newValue));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state: Object): $Shape<Props> {
const { conference } = state['features/base/conference'];
return {
_lobbyEnabled: state['features/lobby'].lobbyEnabled,
_visible: conference && conference.isLobbySupported() && isLocalParticipantModerator(state)
};
}
export default translate(connect(mapStateToProps)(LobbySection));

View File

@@ -0,0 +1,5 @@
// @flow
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbySection } from './LobbySection';
export { default as LobbyScreen } from './LobbyScreen';

View File

@@ -0,0 +1,23 @@
// @flow
import { getCurrentConference } from '../base/conference';
/**
* Approves (lets in) or rejects a knocking participant.
*
* @param {Function} getState - Function to get the Redux state.
* @param {string} id - The id of the knocking participant.
* @param {boolean} approved - True if the participant is approved, false otherwise.
* @returns {Function}
*/
export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
const conference = getCurrentConference(getState());
if (conference) {
if (approved) {
conference.lobbyApproveAccess(id);
} else {
conference.lobbyDenyAccess(id);
}
}
}

View File

@@ -0,0 +1,6 @@
// @flow
import './middleware';
import './reducer';
export * from './components';

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/lobby');

View File

@@ -0,0 +1,153 @@
// @flow
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getFirstLoadableAvatarUrl } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
import { isPrejoinPageEnabled } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
import {
hideLobbyScreen,
knockingParticipantLeft,
openLobbyScreen,
participantIsKnockingOrUpdated,
setLobbyModeEnabled,
startKnocking,
setPasswordJoinFailed
} from './actions';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
// We need the full update result to be in the store already
const result = next(action);
_findLoadableAvatarForKnockingParticipant(store, action.participant);
return result;
}
}
return next(action);
});
/**
* Registers a change handler for state['features/base/conference'].conference to
* set the event listeners needed for the lobby feature to operate.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
dispatch(setLobbyModeEnabled(enabled));
});
conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
dispatch(participantIsKnockingOrUpdated({
id,
name
}));
});
conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {
dispatch(participantIsKnockingOrUpdated({
...participant,
id
}));
});
conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
dispatch(knockingParticipantLeft(id));
});
}
});
/**
* Function to handle the conference failed event and navigate the user to the lobby screen
* based on the failure reason.
*
* @param {Object} store - The Redux store.
* @param {Function} next - The Redux next function.
* @param {Object} action - The Redux action.
* @returns {Object}
*/
function _conferenceFailed({ dispatch, getState }, next, action) {
const { error } = action;
const state = getState();
const nonFirstFailure = Boolean(state['features/base/conference'].membersOnly);
if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
const result = next(action);
dispatch(openLobbyScreen());
if (isPrejoinPageEnabled(state) && !state['features/lobby'].knocking) {
// prejoin is enabled, so we knock automatically
dispatch(startKnocking());
}
dispatch(setPasswordJoinFailed(nonFirstFailure));
return result;
}
dispatch(hideLobbyScreen());
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
dispatch(showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
titleKey: 'lobby.joinRejectedMessage'
}));
}
return next(action);
}
/**
* Handles cleanup of lobby state when a conference is joined.
*
* @param {Object} store - The Redux store.
* @param {Function} next - The Redux next function.
* @param {Object} action - The Redux action.
* @returns {Object}
*/
function _conferenceJoined({ dispatch }, next, action) {
dispatch(hideLobbyScreen());
return next(action);
}
/**
* Finds the loadable avatar URL and updates the participant accordingly.
*
* @param {Object} store - The Redux store.
* @param {Object} participant - The knocking participant.
* @returns {void}
*/
function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id }) {
const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
const { disableThirdPartyRequests } = getState()['features/base/config'];
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
getFirstLoadableAvatarUrl(updatedParticipant).then(loadableAvatarUrl => {
if (loadableAvatarUrl) {
dispatch(participantIsKnockingOrUpdated({
loadableAvatarUrl,
id
}));
}
});
}
}

View File

@@ -0,0 +1,93 @@
// @flow
import { CONFERENCE_JOINED, CONFERENCE_LEFT, SET_PASSWORD } from '../base/conference';
import { ReducerRegistry } from '../base/redux';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
const DEFAULT_STATE = {
knocking: false,
knockingParticipants: [],
lobbyEnabled: false,
passwordJoinFailed: false
};
/**
* Reduces redux actions which affect the display of notifications.
*
* @param {Object} state - The current redux state.
* @param {Object} action - The redux action to reduce.
* @returns {Object} The next redux state which is the result of reducing the
* specified {@code action}.
*/
ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
return {
...state,
knocking: false,
passwordJoinFailed: false
};
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
return _knockingParticipantArrivedOrUpdated(action.participant, state);
case KNOCKING_PARTICIPANT_LEFT:
return {
...state,
knockingParticipants: state.knockingParticipants.filter(p => p.id !== action.id)
};
case SET_KNOCKING_STATE:
return {
...state,
knocking: action.knocking,
passwordJoinFailed: false
};
case SET_LOBBY_MODE_ENABLED:
return {
...state,
lobbyEnabled: action.enabled
};
case SET_PASSWORD:
return {
...state,
passwordJoinFailed: false
};
case SET_PASSWORD_JOIN_FAILED:
return {
...state,
passwordJoinFailed: action.failed
};
}
return state;
});
/**
* Stores or updates a knocking participant.
*
* @param {Object} participant - The arrived or updated knocking participant.
* @param {Object} state - The current Redux state of the feature.
* @returns {Object}
*/
function _knockingParticipantArrivedOrUpdated(participant, state) {
let existingParticipant = state.knockingParticipants.find(p => p.id === participant.id);
existingParticipant = {
...existingParticipant,
...participant
};
return {
...state,
knockingParticipants: [
...state.knockingParticipants.filter(p => p.id !== participant.id),
existingParticipant
]
};
}

Some files were not shown because too many files have changed in this diff Show More