Compare commits

...

22 Commits

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

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

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

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

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

* squash: Renames function.

* ref: Removes filtering jicofo setting owners.

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

* fix: Skips allowners logic for admins.
2020-06-05 07:57:49 -05:00
Deepak Verma
ff23f81dfe flags: fix comment 2020-06-05 13:53:35 +02:00
Jaya Allamsetty
bc66c9063a chore(deps): update lib-jitsi-meet 2020-06-04 10:54:06 -04:00
Emil Ivov
974ef4a382 Merge pull request #6941 from jitsi/saghul-patch-5
shared-video: use a more recent video by default
2020-06-03 06:42:41 -05:00
Saúl Ibarra Corretgé
3bf82b573c shared-video: use a more recent video by default 2020-06-03 11:27:08 +02:00
hmuresan
b4b4339a1a external_api: add start/stop recording commands 2020-06-03 09:30:19 +02:00
Hristo Terezov
6773aed67f feat(recording): Limit notification 2020-06-02 16:00:54 -05:00
Saúl Ibarra Corretgé
d740752522 rn,responsive-ui: refactor dimensions detection
Use a dimensions detecting root component. The Dimensions module does not
measure the app's view size, but the Window, which may not be the same, for
example on iOS when PiP is used.

Also refactor the aspect ratio wrap component since it can be taken directly
from the store.

Last, remove the use of DimensionsDetector on LargeVideo and TileView since they
occupy the full-screen anyway.

Fixes PiP mode on iOS.
2020-06-02 16:54:28 +02:00
Marius Bardan
d93b219c7f lang: update RO translations 2020-06-02 10:19:06 +02:00
Marius Bardan
10cd150a07 lang: update RO translations 2020-06-02 10:19:06 +02:00
142 changed files with 5272 additions and 1237 deletions

View File

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

View File

@@ -217,6 +217,21 @@ var config = {
// Default value for the channel "last N" attribute. -1 for unlimited.
channelLastN: -1,
// // Options for the recording limit notification.
// recordingLimit: {
//
// // The recording limit in minutes. Note: This number appears in the notification text
// // but doesn't enforce the actual recording time limit. This should be configured in
// // jibri!
// limit: 60,
//
// // The name of the app with unlimited recordings.
// appName: 'Unlimited recordings APP',
//
// // The URL of the app with unlimited recordings.
// appURL: 'https://unlimited.recordings.app.com/'
// },
// Disables or enables RTX (RFC 4588) (defaults to false).
// disableRtx: false,
@@ -497,6 +512,23 @@ var config = {
// If set to true all muting operations of remote participants will be disabled.
// disableRemoteMute: true,
/**
External API url used to receive branding specific information.
If there is no url set or there are missing fields, the defaults are applied.
None of the fieds are mandatory and the response must have the shape:
{
// The hex value for the colour used as background
backgroundColor: '#fff',
// The url for the image used as background
backgroundImageUrl: 'https://example.com/background-img.png',
// The anchor url used when clicking the logo image
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png'
}
*/
// brandingDataUrl: '',
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold

View File

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

View File

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

140
css/_lobby.scss Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

46
docker/Dockerfile Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -1,38 +1,45 @@
{
"en": "Engleză",
"af": "Afrikaans",
"az": "",
"ar": "Arabă",
"bg": "Bulgară",
"ca": "Catalană",
"cs": "Cehă",
"da": "Daneză",
"de": "Germană",
"el": "Greacă",
"enGB": "Engleză (Regatul Unit)",
"eo": "Esperanto",
"es": "Spaniolă",
"esUS": "Spaniolă (America Latină)",
"et": "Estonă",
"eu": "Bască",
"fi": "Finlandeză",
"fr": "Franceză",
"hy": "Armeniana",
"frCA": "Franceză (Canada)",
"he": "Ebraică",
"hr": "Croată",
"hu": "Maghiară",
"hy": "Armeană",
"id": "Indoneziană",
"it": "Italiană",
"ja": "Japoneză",
"ko": "Koreană",
"nb": "",
"oc": "Occitan",
"lt": "Lituaniană",
"nl": "Olandeză",
"oc": "Occitană",
"pl": "Poloneză",
"ptBR": "Portugheză (Brazilia)",
"ru": "Rusă",
"ro": "Română",
"sc": "Sardă",
"sk": "Slovacă",
"sl": "Slovenă",
"sv": "Suedeză",
"th": "Thailandeză",
"tr": "Turcă",
"uk": "Ucraineană",
"vi": "Vietnameză",
"zhCN": "Chineză (China)",
"zhTW": "Chineză (Taiwan)",
"nl": "Olandeză",
"hu": "Maghiară",
"hr": "Croată",
"frCA": "Franceză (Canadia)",
"fi": "Finlandeză",
"et": "Estoniană",
"esUS": "Spaniolă (America Latină)",
"enGB": "Engleză (Regatul Unit)",
"da": "Daneză",
"că": "Catalană"
"zhTW": "Chineză (Taiwan)"
}

View File

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

View File

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

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

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

View File

@@ -21,7 +21,8 @@
"bluetooth": "Bluetooth",
"headphones": "Căști",
"phone": "Telefon",
"speaker": "Difuzor"
"speaker": "Difuzor",
"none": ""
},
"audioOnly": {
"audioOnly": "Doar audio"
@@ -51,7 +52,12 @@
"popover": "Alegeți un pseudonim",
"title": "Introduceți un pseudonim pentru a conversa"
},
"title": "Apel video"
"title": "Apel video",
"you": "",
"privateNotice": "",
"noMessagesMessage": "",
"messageTo": "",
"fieldPlaceHolder": ""
},
"connectingOverlay": {
"joiningRoom": "Sunteți conectat la conversația dumneavoastră ..."
@@ -66,7 +72,11 @@
"DISCONNECTED": "Deconectat",
"DISCONNECTING": "Se deconectează",
"ERROR": "Eroare",
"RECONNECTING": "A apărut o eroare de rețea. Reconectare..."
"RECONNECTING": "A apărut o eroare de rețea. Reconectare...",
"LOW_BANDWIDTH": "",
"GOT_SESSION_ID": "",
"GET_SESSION_ID_ERROR": "",
"FETCH_SESSION_ID": ""
},
"connectionindicator": {
"address": "Adresă:",
@@ -97,7 +107,8 @@
"status": "Conexiune:",
"transport": "Mod Transport:",
"transport_plural": "Moduri Transport:",
"turn": " (turn)"
"turn": " (turn)",
"e2e_rtt": ""
},
"dateUtils": {
"earlier": "Mai devreme",
@@ -109,6 +120,8 @@
"description": "Nu s-a întâmplat nimic? Am încercat să vă deschidem conversația în {{app}} aplicația pentru desktop. Încercați din nou sau deschideți {{app}} aplicația web.",
"descriptionWithoutWeb": "",
"downloadApp": "Descărcați aplicația",
"ifDoNotHaveApp": "Dacă nu aveti încă aplicația atunci:",
"ifHaveApp": "Dacă aveti deja aplicația:",
"launchWebButton": "Deschideți în browser",
"openApp": "Continuați spre aplicație",
"title": "Deschidere apel video în {{app}}...",
@@ -256,13 +269,24 @@
"WaitForHostMsgWOk": "Conferința {{room}} nu a început. Daca sunteți moderatorul, apăsați butonul OK pentru autentificare. Dacă nu, așteptați ca moderatorul să înceapă conferința.",
"WaitingForHost": "Așteptare moderator conferință ...",
"Yes": "Da",
"yourEntireScreen": "Întregul ecran"
"yourEntireScreen": "Întregul ecran",
"sendPrivateMessageTitle": "",
"sendPrivateMessageOk": "",
"sendPrivateMessageCancel": "",
"sendPrivateMessage": "",
"screenSharingAudio": "",
"muteEveryoneStartMuted": "",
"muteEveryoneSelf": "",
"muteEveryoneTitle": "",
"muteEveryoneDialog": "",
"muteEveryoneElseTitle": "",
"muteEveryoneElseDialog": ""
},
"dialOut": {
"statusMessage": "Este {{status}}"
},
"feedback": {
"average": "Mediu",
"average": "Medie",
"bad": "Rău",
"detailsLabel": "Spuneți-ne mai multe despre experiența dumneavoastră.",
"good": "Bine",
@@ -335,7 +359,8 @@
"toggleFilmstrip": "Afișați sau ascundeți imagini video",
"toggleScreensharing": "Comutați între cameră și partajare ecran",
"toggleShortcuts": "Arătați sau ascundeți comenzi rapide tastatură",
"videoMute": "Porniți sau opriți camera"
"videoMute": "Porniți sau opriți camera",
"videoQuality": ""
},
"liveStreaming": {
"busy": "Lucrăm la eliberarea resurselor de transmitere. Vă rugam să încercați din nou în câteva minute.",
@@ -363,7 +388,11 @@
"signOut": "Deconectare",
"start": "Începeți o transmitere live",
"streamIdHelp": "Ce înseamnă acest lucru?",
"unavailableTitle": "Transmitere live indisponibilă"
"unavailableTitle": "Transmitere live indisponibilă",
"onBy": "",
"offBy": "",
"googlePrivacyPolicy": "Politica de confidențialitate Google",
"youtubeTerms": "Termeni și condiții Youtube"
},
"localRecording": {
"clientState": {
@@ -426,9 +455,10 @@
"unmute": "",
"newDeviceCameraTitle": "A fost detectată o cameră noua",
"newDeviceAudioTitle": "A fost detectat un dispozitiv audio nou",
"newDeviceAction": "Utilizați"
"newDeviceAction": "Utilizați",
"suboptimalBrowserWarning": "Folosind acest browser nu veți beneficia de cea mai bună experiență pentru aceste apeluri video. În timp ce lucrăm la asta, vă recomandăm să folosiți unul din <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser-ele suportate</a>."
},
"passwordSetRemotely": "Setat de un alt membru",
"passwordSetRemotely": "Setată de un alt membru",
"passwordDigitsOnly": "Până la {{number}} cifre",
"poweredby": "cu sprijinul",
"presenceStatus": {
@@ -474,7 +504,9 @@
"signIn": "Conectare",
"signOut": "Deconectare",
"unavailable": "Oops! Serviciul {{serviceName}} este indisponibil momentan. Se lucrează la remedierea acestei probleme. Vă rugam să încercați mai tărziu.",
"unavailableTitle": "Înregistrare indisponibilă"
"unavailableTitle": "Înregistrare indisponibilă",
"onBy": "{{name}} a pornit înregistrarea",
"offBy": "{{name}} a oprit înregistrarea"
},
"sectionList": {
"pullToRefresh": "Trageți pentru a reîmprospătă"
@@ -499,8 +531,10 @@
"selectCamera": "Cameră",
"selectMic": "Microfon",
"startAudioMuted": "Toată lumea începe cu sunetul dezactivat",
"startVideoMuted": "Toată lumea începe cu sunetul dezactivat",
"title": "Setări"
"startVideoMuted": "Toată lumea începe cu fară video",
"title": "Setări",
"speakers": "Difuzoare",
"microphones": "Microfoane"
},
"settingsView": {
"alertOk": "OK",
@@ -515,7 +549,11 @@
"serverURL": "Server URL",
"startWithAudioMuted": "Începeți cu sunetul dezactivat",
"startWithVideoMuted": "Începeți cu video dezactivat",
"version": "Version"
"version": "Version",
"showAdvanced": "Arată setările avansate",
"disableP2P": "Dezactivează modul Peer-To-Peer",
"disableCallIntegration": "Dezactivează integrarea cu apelurile native",
"advanced": "Avansat"
},
"share": {
"dialInfoText": "\n\n=====\n\nVreti doar să apelați telefonic?\n\n{{defaultDialInNumber}}Faceți click pe acest link pentru a vizualiza numerele pentru apelare în acest apel video\n{{dialInfoPageUrl}}",
@@ -525,10 +563,10 @@
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Name",
"name": "Nume",
"seconds": "{{count}}s",
"speakerStats": "Parametrii difuzorului",
"speakerTime": "Durată participant"
"speakerStats": "Statistici participanți",
"speakerTime": "Durată vorbire participant"
},
"startupoverlay": {
"policyText": " ",
@@ -572,9 +610,14 @@
"tileView": "Afișați/ascundeți miniatura video",
"toggleCamera": "Afișați / ascundeți camera",
"videomute": "Activați / dezactivați înregistrarea",
"videoblur": ""
"videoblur": "",
"privateMessage": "Trimite un mesaj privat",
"muteEveryone": "Oprește microfonul tuturor",
"moreOptions": "Arată mai multe opțiuni",
"help": "Ajutor",
"download": "Descarcă aplicațiile noastre"
},
"addPeople": "Adaugați persoane în apel",
"addPeople": "Adăugați persoane în apel",
"audioOnlyOff": "Dezactivați modul 'doar audio'",
"audioOnlyOn": "Activați modul 'doar audio'",
"audioRoute": "Selectați dispozitivul pentru sunet",
@@ -664,9 +707,6 @@
"lowDefinition": "Calitate redusă",
"onlyAudioAvailable": "Doar audio este disponibil",
"onlyAudioSupported": "În acest navigator este suportat doar 'mod audio'.",
"p2pEnabled": "Peer-to-Peer activat",
"p2pVideoQualityDescription": "În modul peer-to-peer pentru calitatea apelurilor primite puteți alege doar între calitate superioară sau 'mod audio'. Alte setări nu pot fi activate până când modul peer-to-peer nu este dezactivat.",
"recHighDefinitionOnly": "Este de preferat o calitate superioară.",
"sd": "SD",
"standardDefinition": "Calitate standard"
},
@@ -698,7 +738,7 @@
"go": "ÎNCEPEȚI",
"join": "ACCESARE",
"info": "Informații",
"privacy": "Securitate",
"privacy": "Confidențialitate",
"recentList": "Recent",
"recentListDelete": "Ștergeți",
"recentListEmpty": "Lista dumneavoastră recentă este momentan goală. Discutați cu echipa dumneavoastră și veți găsi toate conversațiile aici.",
@@ -707,13 +747,16 @@
"roomnameHint": "Introduceți numele sau adresa web a ședinței la care doriți să vă conectați. Puteți asocia un nume, dar transmiteți și celorlalți participanți acest nume.",
"sendFeedback": "Lăsați-ne feedback",
"terms": "Termeni",
"title": "Video-conferință securizata, cu opțiuni multiple și complet gratuită "
"title": "Video-conferință securizată, cu multiple funcționalități și complet gratuită"
},
"lonelyMeetingExperience": {
"button": "Invită alte persoane",
"youAreAlone": "Ești singura persoană din acest apel"
"documentSharing": {
"title": ""
},
"helpView": {
"header": "Centru de ajutor"
}
"defaultNickname": "",
"chromeExtensionBanner": {
"dontShowAgain": "",
"buttonText": "",
"installExtensionText": ""
},
"raisedHand": "Ar dori să vorbească"
}

View File

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

View File

@@ -395,6 +395,8 @@
"videoQuality": "Manage call quality"
},
"liveStreaming": {
"limitNotificationDescriptionWeb": "Due to high demand your streaming will be limited to {{limit}} min. For unlimited streaming try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Your streaming will be limited to {{limit}} min. For unlimited streaming try {{app}}.",
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"changeSignIn": "Switch accounts.",
@@ -552,6 +554,8 @@
},
"raisedHand": "Would like to speak",
"recording": {
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
"authDropboxText": "Upload to Dropbox",
"availableSpace": "Available space: {{spaceLeft}} MB (approximately {{duration}} minutes of recording)",
"beta": "BETA",
@@ -671,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",
@@ -718,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",
@@ -857,5 +864,32 @@
},
"helpView": {
"header": "Help center"
},
"lobby": {
"allow": "Allow",
"backToKnockModeButton": "No password, ask to join instead",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
"emailField": "Enter your email address",
"enableDialogPasswordField": "Set password (optional)",
"enableDialogSubmit": "Enable",
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "Enter meeting password",
"enterPasswordTitle": "Enter password to join meeting",
"invalidPassword": "Invalid password",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
"joinWithPasswordMessage": "Trying to join with password, please wait...",
"joinRejectedMessage": "Your join request was rejected by a moderator.",
"joinTitle": "Join Meeting",
"joiningTitle": "Asking to join meeting...",
"joiningWithPasswordTitle": "Joining with password...",
"knockButton": "Ask to Join",
"knockTitle": "Someone wants to join the meeting",
"nameField": "Enter your name",
"passwordField": "Enter meeting password",
"passwordJoinButton": "Join",
"reject": "Reject",
"toggleLabel": "Enable lobby"
}
}

View File

@@ -8,16 +8,21 @@ import {
sendAnalytics
} from '../../react/features/analytics';
import {
getCurrentConference,
sendTones,
setPassword,
setSubject
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
import { setE2EEKey } from '../../react/features/e2ee';
import { invite } from '../../react/features/invite';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
import { toggleTileView } from '../../react/features/video-layout';
import { setVideoQuality } from '../../react/features/video-quality';
@@ -190,6 +195,114 @@ function initCommands() {
logger.debug('Set video quality command received');
sendAnalytics(createApiEvent('set.video.quality'));
APP.store.dispatch(setVideoQuality(frameHeight));
},
/**
* Starts a file recording or streaming depending on the passed on params.
* For youtube streams, `youtubeStreamKey` must be passed on. `youtubeBroadcastID` is optional.
* For dropbox recording, recording `mode` should be `file` and a dropbox oauth2 token must be provided.
* For file recording, recording `mode` should be `file` and optionally `shouldShare` could be passed on.
* No other params should be passed.
*
* @param { string } arg.mode - Recording mode, either `file` or `stream`.
* @param { string } arg.dropboxToken - Dropbox oauth2 token.
* @param { boolean } arg.shouldShare - Whether the recording should be shared with the participants or not.
* Only applies to certain jitsi meet deploys.
* @param { string } arg.youtubeStreamKey - The youtube stream key.
* @param { string } arg.youtubeBroadcastID - The youtube broacast ID.
* @returns {void}
*/
'start-recording': ({ mode, dropboxToken, shouldShare, youtubeStreamKey, youtubeBroadcastID }) => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (dropboxToken && !isDropboxEnabled(state)) {
logger.error('Failed starting recording: dropbox is not enabled on this deployment');
return;
}
if (mode === JitsiRecordingConstants.mode.STREAM && !youtubeStreamKey) {
logger.error('Failed starting recording: missing youtube stream key');
return;
}
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
if (dropboxToken) {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
'upload_credentials': {
'service_name': RECORDING_TYPES.DROPBOX,
'token': dropboxToken
}
}
})
};
} else {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
'share': shouldShare
}
})
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey
};
} else {
logger.error('Invalid recording mode provided');
return;
}
conference.startRecording(recordingConfig);
},
/**
* Stops a recording or streaming in progress.
*
* @param {string} mode - `file` or `stream`.
* @returns {void}
*/
'stop-recording': mode => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
logger.error('Invalid recording mode provided!');
return;
}
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
conference.stopRecording(activeSession.id);
} else {
logger.error('No recording or streaming session found');
}
}
};
transport.on('event', ({ data, name }) => {
@@ -514,7 +627,8 @@ class API {
notifyDeviceListChanged(devices: Object) {
this._sendEvent({
name: 'device-list-changed',
devices });
devices
});
}
/**

View File

@@ -37,6 +37,8 @@ const commands = {
sendEndpointTextMessage: 'send-endpoint-text-message',
sendTones: 'send-tones',
setVideoQuality: 'set-video-quality',
startRecording: 'start-recording',
stopRecording: 'stop-recording',
subject: 'subject',
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',

View File

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

View File

@@ -31,7 +31,7 @@ export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
* Example shared video link.
* @type {string}
*/
const defaultSharedVideoLink = 'https://www.youtube.com/watch?v=xNXN7CZk8X0';
const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
const updateInterval = 5000; // milliseconds
/**

View File

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

10
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import { DialogContainer } from '../../base/dialog';
import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
import '../../base/jwt';
import { Platform } from '../../base/react';
import '../../base/responsive-ui';
import { DimensionsDetector, clientResized } from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import '../../google-api';
import '../../mobile/audio-mode';
@@ -78,6 +78,9 @@ export class App extends AbstractApp {
// This will effectively kill the app. In accord with the Web, do not
// kill the app.
this._maybeDisableExceptionsManager();
// Bind event handler so it is only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
/**
@@ -107,6 +110,21 @@ export class App extends AbstractApp {
});
}
/**
* Overrides the parent method to inject {@link DimensionsDetector} as
* the top most component.
*
* @override
*/
_createMainElement(component, props) {
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
{ super._createMainElement(component, props) }
</DimensionsDetector>
);
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in
@@ -144,6 +162,22 @@ export class App extends AbstractApp {
}
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Updates the known available size for the app to occupy.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
const { dispatch } = this.state.store;
dispatch(clientResized(width, height));
}
/**
* Renders the platform specific dialog container.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 374 B

View File

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

After

Width:  |  Height:  |  Size: 287 B

View File

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

View File

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

After

Width:  |  Height:  |  Size: 224 B

View File

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

After

Width:  |  Height:  |  Size: 156 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,70 +0,0 @@
// @flow
import React, { Component } from 'react';
import { connect } from '../../redux';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from '../constants';
/**
* The type of the React {@code Component} props of {@link AspectRatioAware}.
*/
type Props = {
aspectRatio: ASPECT_RATIO_NARROW | ASPECT_RATIO_WIDE
};
/**
* Determines whether a specific React {@code Component} decorated into an
* {@link AspectRatioAware} has {@link ASPECT_RATIO_NARROW} as the value of its
* {@code aspectRatio} React prop.
*
* @param {AspectRatioAware} component - An {@link AspectRatioAware} which may
* have an {@code aspectRatio} React prop.
* @returns {boolean}
*/
export function isNarrowAspectRatio(component: React$Component<*>) {
return component.props.aspectRatio === ASPECT_RATIO_NARROW;
}
/**
* Decorates a specific React {@code Component} class into an
* {@link AspectRatioAware} which provides the React prop {@code aspectRatio}
* updated on each redux state change.
*
* @param {Class<React$Component>} WrappedComponent - A React {@code Component}
* class to be wrapped.
* @returns {AspectRatioAwareWrapper}
*/
export function makeAspectRatioAware(
WrappedComponent: Class<React$Component<*>>
): Class<React$Component<*>> {
/**
* Renders {@code WrappedComponent} with the React prop {@code aspectRatio}.
*/
class AspectRatioAware extends Component<Props> {
/**
* Implement's React render method to wrap the nested component.
*
* @returns {React$Element}
*/
render(): React$Element<*> {
return <WrappedComponent { ...this.props } />;
}
}
return connect(_mapStateToProps)(AspectRatioAware);
}
/**
* Maps (parts of) the redux state to {@link AspectRatioAware} props.
*
* @param {Object} state - The whole redux state.
* @private
* @returns {{
* aspectRatio: Symbol
* }}
*/
function _mapStateToProps(state) {
return {
aspectRatio: state['features/base/responsive-ui'].aspectRatio
};
}

View File

@@ -1,13 +1,9 @@
// @flow
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import styles from './styles';
/**
* AspectRatioDetector component's property types.
*/
type Props = {
/**
@@ -64,7 +60,7 @@ export default class DimensionsDetector extends PureComponent<Props> {
return (
<View
onLayout = { this._onLayout }
style = { styles.dimensionsDetector } >
style = { StyleSheet.absoluteFillObject } >
{ this.props.children }
</View>
);

View File

@@ -1,2 +1 @@
export * from './AspectRatioAware';
export { default as DimensionsDetector } from './DimensionsDetector';

View File

@@ -1,14 +0,0 @@
import { createStyleSheet } from '../../styles';
/**
* The styles of the feature base/responsive-ui.
*/
export default createStyleSheet({
/**
* The style of {@link DimensionsDetector} used on react-native.
*/
dimensionsDetector: {
alignSelf: 'stretch',
flex: 1
}
});

View File

@@ -1,16 +1,10 @@
// @flow
import { Dimensions } from 'react-native';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import { CLIENT_RESIZED } from './actionTypes';
import { setAspectRatio, setReducedUI } from './actions';
/**
* Dimensions change handler.
*/
let handler;
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio and
@@ -19,65 +13,19 @@ let handler;
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
MiddlewareRegistry.register(({ dispatch }) => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_UNMOUNT: {
_appWillUnmount();
case CLIENT_RESIZED: {
const { clientWidth: width, clientHeight: height } = action;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
break;
}
case APP_WILL_MOUNT:
_appWillMount(store);
break;
}
return result;
});
/**
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _appWillMount(store) {
handler = dim => {
_onDimensionsChange(dim, store);
};
Dimensions.addEventListener('change', handler);
}
/**
* Notifies this feature that the action {@link APP_WILL_UNMOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @private
* @returns {void}
*/
function _appWillUnmount() {
Dimensions.removeEventListener('change', handler);
handler = undefined;
}
/**
* Handles window dimension changes.
*
* @param {Object} dimensions - The new dimensions.
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _onDimensionsChange(dimensions, store) {
const { width, height } = dimensions.window;
const { dispatch } = store;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
}

View File

@@ -23,7 +23,6 @@ const DEFAULT_STATE = {
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CLIENT_RESIZED: {
return {
...state,
clientWidth: action.clientWidth,

View File

@@ -8,10 +8,7 @@ import { appNavigate } from '../../../app';
import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { TestConnectionInfo } from '../../../base/testing';
import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
import { Chat } from '../../../chat';
@@ -25,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';
@@ -45,10 +43,13 @@ import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
*/
type Props = AbstractProps & {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* Wherther the calendar feature is enabled or not.
*
* @private
*/
_calendarEnabled: boolean,
@@ -57,15 +58,11 @@ type Props = AbstractProps & {
* conference which includes establishing the XMPP connection and then
* joining the room. If truthy, then an activity/loading indicator will be
* rendered.
*
* @private
*/
_connecting: boolean,
/**
* Set to {@code true} when the filmstrip is currently visible.
*
* @private
*/
_filmstripVisible: boolean,
@@ -76,34 +73,17 @@ type Props = AbstractProps & {
/**
* Whether Picture-in-Picture is enabled.
*
* @private
*/
_pictureInPictureEnabled: boolean,
/**
* The indicator which determines whether the UI is reduced (to accommodate
* smaller display areas).
*
* @private
*/
_reducedUI: boolean,
/**
* The handler which dispatches the (redux) action {@link setToolboxVisible}
* to show/hide the {@link Toolbox}.
*
* @param {boolean} visible - {@code true} to show the {@code Toolbox} or
* {@code false} to hide it.
* @private
* @returns {void}
*/
_setToolboxVisible: Function,
/**
* The indicator which determines whether the Toolbox is visible.
*
* @private
*/
_toolboxVisible: boolean,
@@ -249,6 +229,7 @@ class Conference extends AbstractConference<Props, *> {
*/
_renderContent() {
const {
_aspectRatio,
_connecting,
_filmstripVisible,
_largeVideoParticipantId,
@@ -257,7 +238,8 @@ class Conference extends AbstractConference<Props, *> {
_toolboxVisible
} = this.props;
const showGradient = _toolboxVisible;
const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView;
const applyGradientStretching
= _filmstripVisible && _aspectRatio === ASPECT_RATIO_NARROW && !_shouldDisplayTileView;
if (_reducedUI) {
return this._renderContentForReducedUi();
@@ -339,6 +321,7 @@ class Conference extends AbstractConference<Props, *> {
style = { styles.navBarSafeView }>
<NavigationBar />
{ this._renderNotificationsContainer() }
<KnockingParticipantList />
</SafeAreaView>
<TestConnectionInfo />
@@ -393,7 +376,9 @@ class Conference extends AbstractConference<Props, *> {
// flex layout. The only option that seemed to limit the notification's
// size was explicit 'width' value which is not better than the margin
// added here.
if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) {
const { _aspectRatio, _filmstripVisible } = this.props;
if (_filmstripVisible && _aspectRatio !== ASPECT_RATIO_NARROW) {
notificationsStyle.marginRight = FILMSTRIP_SIZE;
}
@@ -431,9 +416,10 @@ function _mapStateToProps(state) {
const {
conference,
joining,
membersOnly,
leaving
} = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
@@ -445,65 +431,19 @@ 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),
/**
* Wherther the calendar feature is enabled or not.
*
* @private
* @type {boolean}
*/
_aspectRatio: aspectRatio,
_calendarEnabled: isCalendarEnabled(state),
/**
* The indicator which determines that we are still connecting to the
* conference which includes establishing the XMPP connection and then
* joining the room. If truthy, then an activity/loading indicator will
* be rendered.
*
* @private
* @type {boolean}
*/
_connecting: Boolean(connecting_),
/**
* Is {@code true} when the filmstrip is currently visible.
*/
_filmstripVisible: isFilmstripVisible(state),
/**
* The ID of the participant currently on stage.
*/
_largeVideoParticipantId: state['features/large-video'].participantId,
/**
* Whether Picture-in-Picture is enabled.
*
* @private
* @type {boolean}
*/
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
/**
* The indicator which determines whether the UI is reduced (to
* accommodate smaller display areas).
*
* @private
* @type {boolean}
*/
_reducedUI: reducedUI,
/**
* The indicator which determines whether the Toolbox is visible.
*
* @private
* @type {boolean}
*/
_toolboxVisible: isToolboxVisible(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));
export default connect(_mapStateToProps)(Conference);

View File

@@ -5,10 +5,7 @@ import { TouchableOpacity, View } from 'react-native';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import {
RecordingExpandedLabel
} from '../../../recording';
@@ -29,14 +26,19 @@ import styles from './styles';
type Props = AbstractLabelsProps & {
/**
* Function to translate i18n labels.
* Application's aspect ratio.
*/
t: Function,
_aspectRatio: Symbol,
/**
* True if the labels should be visible, false otherwise.
*/
_visible: boolean
_visible: boolean,
/**
* Function to translate i18n labels.
*/
t: Function
};
type State = {
@@ -149,12 +151,13 @@ class Labels extends AbstractLabels<Props, State> {
* @inheritdoc
*/
render() {
if (!this.props._visible) {
const { _aspectRatio, _filmstripVisible, _visible } = this.props;
if (!_visible) {
return null;
}
const wide = !isNarrowAspectRatio(this);
const { _filmstripVisible } = this.props;
const wide = _aspectRatio === ASPECT_RATIO_WIDE;
return (
<View
@@ -354,8 +357,9 @@ class Labels extends AbstractLabels<Props, State> {
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_visible: !shouldDisplayNotifications(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Labels));
export default connect(_mapStateToProps)(Labels);

View File

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

View File

@@ -41,8 +41,7 @@ class Labels extends AbstractLabels<Props, State> {
*/
static getDerivedStateFromProps(props: Props, prevState: State) {
return {
filmstripBecomingVisible: !prevState.filmstripBecomingVisible
&& props._filmstripVisible
filmstripBecomingVisible: !prevState.filmstripBecomingVisible && props._filmstripVisible
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,7 @@ import { ScrollView } from 'react-native';
import { Container, Platform } from '../../../base/react';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { isFilmstripVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail';
@@ -20,24 +17,23 @@ import styles from './styles';
*/
type Props = {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* The indicator which determines whether the filmstrip is enabled.
*
* @private
*/
_enabled: boolean,
/**
* The participants in the conference.
*
* @private
*/
_participants: Array<any>,
/**
* The indicator which determines whether the filmstrip is visible.
*
* @private
*/
_visible: boolean
};
@@ -90,40 +86,36 @@ class Filmstrip extends Component<Props> {
* @returns {ReactElement}
*/
render() {
if (!this.props._enabled) {
const { _aspectRatio, _enabled, _participants, _visible } = this.props;
if (!_enabled) {
return null;
}
const isNarrowAspectRatio_ = isNarrowAspectRatio(this);
const filmstripStyle
= isNarrowAspectRatio_
? styles.filmstripNarrow
: styles.filmstripWide;
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
return (
<Container
style = { filmstripStyle }
visible = { this.props._visible }>
visible = { _visible }>
{
this._separateLocalThumbnail
&& !isNarrowAspectRatio_
&& !isNarrowAspectRatio
&& <LocalThumbnail />
}
<ScrollView
horizontal = { isNarrowAspectRatio_ }
horizontal = { isNarrowAspectRatio }
showsHorizontalScrollIndicator = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
{
!this._separateLocalThumbnail
&& !isNarrowAspectRatio_
!this._separateLocalThumbnail && !isNarrowAspectRatio
&& <LocalThumbnail />
}
{
this._sort(
this.props._participants,
isNarrowAspectRatio_)
this._sort(_participants, isNarrowAspectRatio)
.map(p => (
<Thumbnail
key = { p.id }
@@ -131,14 +123,12 @@ class Filmstrip extends Component<Props> {
}
{
!this._separateLocalThumbnail
&& isNarrowAspectRatio_
!this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail />
}
</ScrollView>
{
this._separateLocalThumbnail
&& isNarrowAspectRatio_
this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail />
}
</Container>
@@ -150,13 +140,13 @@ class Filmstrip extends Component<Props> {
*
* @param {Participant[]} participants - The array of {@code Participant}s
* to sort in display order.
* @param {boolean} isNarrowAspectRatio_ - Indicates if the aspect ratio is
* @param {boolean} isNarrowAspectRatio - Indicates if the aspect ratio is
* wide or narrow.
* @private
* @returns {Participant[]} A new array containing the elements of the
* specified {@code participants} array sorted in display order.
*/
_sort(participants, isNarrowAspectRatio_) {
_sort(participants, isNarrowAspectRatio) {
// XXX Array.prototype.sort() is not appropriate because (1) it operates
// in place and (2) it is not necessarily stable.
@@ -164,7 +154,7 @@ class Filmstrip extends Component<Props> {
...participants
];
if (isNarrowAspectRatio_) {
if (isNarrowAspectRatio) {
// When the narrow aspect ratio is used, we want to have the remote
// participants from right to left with the newest added/joined to
// the leftmost side. The local participant is the leftmost item.
@@ -180,42 +170,18 @@ class Filmstrip extends Component<Props> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _participants: Participant[],
* _visible: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const participants = state['features/base/participants'];
const { enabled } = state['features/filmstrip'];
return {
/**
* The indicator which determines whether the filmstrip is enabled.
*
* @private
* @type {boolean}
*/
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_enabled: enabled,
/**
* The remote participants in the conference.
*
* @private
* @type {Participant[]}
*/
_participants: participants.filter(p => !p.local),
/**
* The indicator which determines whether the filmstrip is visible. The
* mobile/react-native Filmstrip is visible when there are at least 2
* participants in the conference (including the local one).
*
* @private
* @type {boolean}
*/
_visible: isFilmstripVisible(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Filmstrip));
export default connect(_mapStateToProps)(Filmstrip);

View File

@@ -13,11 +13,7 @@ import {
setMaxReceiverVideoQuality
} from '../../../base/conference';
import { connect } from '../../../base/redux';
import {
DimensionsDetector,
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import Thumbnail from './Thumbnail';
import styles from './styles';
@@ -27,11 +23,26 @@ import styles from './styles';
*/
type Props = {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* Application's viewport height.
*/
_height: number,
/**
* The participants in the conference.
*/
_participants: Array<Object>,
/**
* Application's viewport height.
*/
_width: number,
/**
* Invoked to update the receiver video quality.
*/
@@ -43,22 +54,6 @@ type Props = {
onClick: Function
};
/**
* The type of the React {@link Component} state of {@link TileView}.
*/
type State = {
/**
* The available width for {@link TileView} to occupy.
*/
height: number,
/**
* The available height for {@link TileView} to occupy.
*/
width: number
};
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.
@@ -82,25 +77,7 @@ const TILE_ASPECT_RATIO = 1;
*
* @extends Component
*/
class TileView extends Component<Props, State> {
state = {
height: 0,
width: 0
};
/**
* Initializes a new {@code TileView} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
class TileView extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
@@ -126,32 +103,27 @@ class TileView extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { onClick } = this.props;
const { height, width } = this.state;
const rowElements = this._groupIntoRows(
this._renderThumbnails(), this._getColumnCount());
const { _height, _width, onClick } = this.props;
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
<ScrollView
style = {{
...styles.tileView,
height,
width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: height,
minWidth: width
}}>
{ rowElements }
</View>
</TouchableWithoutFeedback>
</ScrollView>
</DimensionsDetector>
<ScrollView
style = {{
...styles.tileView,
height: _height,
width: _width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: _height,
minWidth: _width
}}>
{ rowElements }
</View>
</TouchableWithoutFeedback>
</ScrollView>
);
}
@@ -167,7 +139,7 @@ class TileView extends Component<Props, State> {
// For narrow view, tiles should stack on top of each other for a lonely
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
// two.
if (isNarrowAspectRatio(this)) {
if (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
return participantCount >= 3 ? 2 : 1;
}
@@ -209,19 +181,17 @@ class TileView extends Component<Props, State> {
* @returns {Object}
*/
_getTileDimensions() {
const { _participants } = this.props;
const { height, width } = this.state;
const { _height, _participants, _width } = this.props;
const columns = this._getColumnCount();
const participantCount = _participants.length;
const heightToUse = height - (MARGIN * 2);
const widthToUse = width - (MARGIN * 2);
const heightToUse = _height - (MARGIN * 2);
const widthToUse = _width - (MARGIN * 2);
let tileWidth;
// If there is going to be at least two rows, ensure that at least two
// rows display fully on screen.
if (participantCount / columns > 1) {
tileWidth
= Math.min(widthToUse / columns, heightToUse / 2);
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
} else {
tileWidth = Math.min(widthToUse / columns, heightToUse);
}
@@ -247,8 +217,7 @@ class TileView extends Component<Props, State> {
for (let i = 0; i < thumbnails.length; i++) {
if (i % rowLength === 0) {
const thumbnailsInRow
= thumbnails.slice(i, i + rowLength);
const thumbnailsInRow = thumbnails.slice(i, i + rowLength);
rowElements.push(
<View
@@ -263,23 +232,6 @@ class TileView extends Component<Props, State> {
return rowElements;
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Updates the known available state for {@link TileView} to occupy.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
this.setState({
height,
width
});
}
/**
* Creates React Elements to display each participant in a thumbnail. Each
* tile will be.
@@ -326,14 +278,17 @@ class TileView extends Component<Props, State> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _participants: Participant[]
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const responsiveUi = state['features/base/responsive-ui'];
return {
_participants: state['features/base/participants']
_aspectRatio: responsiveUi.aspectRatio,
_height: responsiveUi.clientHeight,
_participants: state['features/base/participants'],
_width: responsiveUi.clientWidth
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(TileView));
export default connect(_mapStateToProps)(TileView);

View File

@@ -1,11 +1,10 @@
// @flow
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { ParticipantView } from '../../base/participants';
import { connect } from '../../base/redux';
import { DimensionsDetector } from '../../base/responsive-ui';
import { StyleType } from '../../base/styles';
import { AVATAR_SIZE } from './styles';
@@ -15,6 +14,11 @@ import { AVATAR_SIZE } from './styles';
*/
type Props = {
/**
* Application's viewport height.
*/
_height: number,
/**
* The ID of the participant (to be) depicted by LargeVideo.
*
@@ -27,6 +31,11 @@ type Props = {
*/
_styles: StyleType,
/**
* Application's viewport height.
*/
_width: number,
/**
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
*/
@@ -62,50 +71,33 @@ const DEFAULT_STATE = {
*
* @extends Component
*/
class LargeVideo extends Component<Props, State> {
class LargeVideo extends PureComponent<Props, State> {
state = {
...DEFAULT_STATE
};
/** Initializes a new {@code LargeVideo} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Handle this component's dimension changes. In case we deem it's too
* Handles dimension changes. In case we deem it's too
* small, the connectivity indicator won't be rendered and the avatar
* will occupy the entirety of the available screen state.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
* @inheritdoc
*/
_onDimensionsChanged(width: number, height: number) {
static getDerivedStateFromProps(props: Props) {
const { _height, _width } = props;
// Get the size, rounded to the nearest even number.
const size = 2 * Math.round(Math.min(height, width) / 2);
let nextState;
const size = 2 * Math.round(Math.min(_height, _width) / 2);
if (size < AVATAR_SIZE * 1.5) {
nextState = {
return {
avatarSize: size - 15, // Leave some margin.
useConnectivityInfoLabel: false
};
} else {
nextState = DEFAULT_STATE;
}
this.setState(nextState);
return DEFAULT_STATE;
}
/**
@@ -126,18 +118,15 @@ class LargeVideo extends Component<Props, State> {
} = this.props;
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
<ParticipantView
avatarSize = { avatarSize }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
testHintId = 'org.jitsi.meet.LargeVideo'
useConnectivityInfoLabel = { useConnectivityInfoLabel }
zOrder = { 0 }
zoomEnabled = { true } />
</DimensionsDetector>
<ParticipantView
avatarSize = { avatarSize }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
testHintId = 'org.jitsi.meet.LargeVideo'
useConnectivityInfoLabel = { useConnectivityInfoLabel }
zOrder = { 0 }
zoomEnabled = { true } />
);
}
}
@@ -147,15 +136,16 @@ class LargeVideo extends Component<Props, State> {
*
* @param {Object} state - Redux state.
* @private
* @returns {{
* _participantId: string,
* _styles: StyleType
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
return {
_height: height,
_participantId: state['features/large-video'].participantId,
_styles: ColorSchemeRegistry.get(state, 'LargeVideo')
_styles: ColorSchemeRegistry.get(state, 'LargeVideo'),
_width: width
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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