mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-05 05:20:18 +00:00
Compare commits
16 Commits
saghul-pat
...
dockerisms
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505ed15323 | ||
|
|
8758c222c6 | ||
|
|
29dc63fbcb | ||
|
|
475a2ae596 | ||
|
|
338c960215 | ||
|
|
e6093e0706 | ||
|
|
d1d968997e | ||
|
|
45570bc0e7 | ||
|
|
f4bcad02d8 | ||
|
|
26f7951894 | ||
|
|
35dabb1a27 | ||
|
|
c3b79802b2 | ||
|
|
e6dbe65193 | ||
|
|
ff23f81dfe | ||
|
|
bc66c9063a | ||
|
|
974ef4a382 |
@@ -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();
|
||||
|
||||
17
config.js
17
config.js
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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
140
css/_lobby.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
194
css/_premeeting-screens.scss
Normal file
194
css/_premeeting-screens.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@
|
||||
#remotePresenceMessage {
|
||||
display: none !important;
|
||||
}
|
||||
#largeVideoContainer {
|
||||
background-color: $defaultBackground !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thumbnail popover menus can overlap other thumbnails. Setting an auto
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
46
docker/Dockerfile
Normal 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"]
|
||||
46
docker/rootfs/defaults/meet.conf
Normal file
46
docker/rootfs/defaults/meet.conf
Normal 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;
|
||||
}
|
||||
60
docker/rootfs/defaults/nginx.conf
Normal file
60
docker/rootfs/defaults/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
12
docker/rootfs/etc/cont-init.d/10-config
Normal file
12
docker/rootfs/etc/cont-init.d/10-config
Normal 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
|
||||
3
docker/rootfs/etc/services.d/nginx/run
Normal file
3
docker/rootfs/etc/services.d/nginx/run
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
exec nginx -c /config/nginx/nginx.conf
|
||||
@@ -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
38
lang/languages-mr.json
Normal 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"
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
"fr": "French",
|
||||
"frCA": "French (Canadian)",
|
||||
"he": "Hebrew",
|
||||
"mr":"Marathi",
|
||||
"hr": "Croatian",
|
||||
"hu": "Hungarian",
|
||||
"hy": "Armenian",
|
||||
|
||||
@@ -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
817
lang/main-mr.json
Normal 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": "मदत केंद्र"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
10
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*
|
||||
* {
|
||||
* type: CONNECTION_DISCONNECTED,
|
||||
* connection: JitsiConnection,
|
||||
* message: string
|
||||
* connection: JitsiConnection
|
||||
* }
|
||||
*/
|
||||
export const CONNECTION_DISCONNECTED = 'CONNECTION_DISCONNECTED';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { configureInitialDevices } from '../devices';
|
||||
import { getBackendSafeRoomName } from '../util';
|
||||
|
||||
export {
|
||||
connectionDisconnected,
|
||||
connectionEstablished,
|
||||
connectionFailed,
|
||||
setLocationURL
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,6 +20,10 @@ class DialogContainer extends AbstractDialogContainer {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
if (this.props._rawDialog) {
|
||||
return this._renderDialogContent();
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalTransition>
|
||||
{ this._renderDialogContent() }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 |
1
react/features/base/icons/svg/edit.svg
Normal file
1
react/features/base/icons/svg/edit.svg
Normal 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 |
@@ -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';
|
||||
|
||||
1
react/features/base/icons/svg/meeting-locked.svg
Normal file
1
react/features/base/icons/svg/meeting-locked.svg
Normal 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 |
1
react/features/base/icons/svg/meeting-unlocked.svg
Normal file
1
react/features/base/icons/svg/meeting-unlocked.svg
Normal 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 |
@@ -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;
|
||||
|
||||
3
react/features/base/premeeting/components/index.web.js
Normal file
3
react/features/base/premeeting/components/index.web.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
175
react/features/base/premeeting/components/web/InputField.js
Normal file
175
react/features/base/premeeting/components/web/InputField.js
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
73
react/features/base/premeeting/components/web/Preview.js
Normal file
73
react/features/base/premeeting/components/web/Preview.js
Normal 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);
|
||||
5
react/features/base/premeeting/components/web/index.js
Normal file
5
react/features/base/premeeting/components/web/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as PreMeetingScreen } from './PreMeetingScreen';
|
||||
3
react/features/base/premeeting/index.js
Normal file
3
react/features/base/premeeting/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './components';
|
||||
5
react/features/base/premeeting/logger.js
Normal file
5
react/features/base/premeeting/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/premeeting');
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
11
react/features/base/react/functions.js
Normal file
11
react/features/base/react/functions.js
Normal 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;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
export { default as Platform } from './Platform';
|
||||
export * from './Types';
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
9
react/features/dynamic-branding/actionTypes.js
Normal file
9
react/features/dynamic-branding/actionTypes.js
Normal 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';
|
||||
66
react/features/dynamic-branding/actions.js
Normal file
66
react/features/dynamic-branding/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
15
react/features/dynamic-branding/functions.js
Normal file
15
react/features/dynamic-branding/functions.js
Normal 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]}` : '';
|
||||
}
|
||||
4
react/features/dynamic-branding/index.js
Normal file
4
react/features/dynamic-branding/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
46
react/features/dynamic-branding/reducer.js
Normal file
46
react/features/dynamic-branding/reducer.js
Normal 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;
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
26
react/features/lobby/actionTypes.js
Normal file
26
react/features/lobby/actionTypes.js
Normal 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';
|
||||
25
react/features/lobby/actions.native.js
Normal file
25
react/features/lobby/actions.native.js
Normal 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);
|
||||
}
|
||||
207
react/features/lobby/actions.web.js
Normal file
207
react/features/lobby/actions.web.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
375
react/features/lobby/components/AbstractLobbyScreen.js
Normal file
375
react/features/lobby/components/AbstractLobbyScreen.js
Normal 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
|
||||
};
|
||||
}
|
||||
3
react/features/lobby/components/index.native.js
Normal file
3
react/features/lobby/components/index.native.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
3
react/features/lobby/components/index.web.js
Normal file
3
react/features/lobby/components/index.web.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
77
react/features/lobby/components/native/LobbyModeButton.js
Normal file
77
react/features/lobby/components/native/LobbyModeButton.js
Normal 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));
|
||||
241
react/features/lobby/components/native/LobbyScreen.js
Normal file
241
react/features/lobby/components/native/LobbyScreen.js
Normal 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));
|
||||
7
react/features/lobby/components/native/index.js
Normal file
7
react/features/lobby/components/native/index.js
Normal 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';
|
||||
142
react/features/lobby/components/native/styles.js
Normal file
142
react/features/lobby/components/native/styles.js
Normal 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'
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
170
react/features/lobby/components/web/LobbyScreen.js
Normal file
170
react/features/lobby/components/web/LobbyScreen.js
Normal 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));
|
||||
136
react/features/lobby/components/web/LobbySection.js
Normal file
136
react/features/lobby/components/web/LobbySection.js
Normal 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));
|
||||
5
react/features/lobby/components/web/index.js
Normal file
5
react/features/lobby/components/web/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
export { default as KnockingParticipantList } from './KnockingParticipantList';
|
||||
export { default as LobbySection } from './LobbySection';
|
||||
export { default as LobbyScreen } from './LobbyScreen';
|
||||
23
react/features/lobby/functions.js
Normal file
23
react/features/lobby/functions.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
react/features/lobby/index.js
Normal file
6
react/features/lobby/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
export * from './components';
|
||||
5
react/features/lobby/logger.js
Normal file
5
react/features/lobby/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/lobby');
|
||||
153
react/features/lobby/middleware.js
Normal file
153
react/features/lobby/middleware.js
Normal 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
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
93
react/features/lobby/reducer.js
Normal file
93
react/features/lobby/reducer.js
Normal 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
Reference in New Issue
Block a user