Compare commits

..

1 Commits

Author SHA1 Message Date
damencho
b022ce60ac fix: Optimizes hot paths in prosody modules, string comparisons. 2020-11-06 12:55:02 -06:00
77 changed files with 2091 additions and 1933 deletions

View File

@@ -53,7 +53,6 @@ import {
updateDeviceList
} from './react/features/base/devices';
import {
browser,
isFatalJitsiConnectionError,
JitsiConferenceErrors,
JitsiConferenceEvents,
@@ -494,9 +493,9 @@ export default {
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
browserName =>
browser =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browserName))
mediaPermissionPromptVisibilityChanged(true, browser))
);
let tryCreateLocalTracks;
@@ -1606,10 +1605,8 @@ export default {
*/
async _createPresenterStreamEffect(height = null, cameraDeviceId = null) {
if (!this.localPresenterVideo) {
const camera = cameraDeviceId ?? getUserSelectedCameraDeviceId(APP.store.getState());
try {
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId: camera }, height);
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId }, height);
} catch (err) {
logger.error('Failed to create a camera track for presenter', err);
@@ -1650,38 +1647,38 @@ export default {
// Create a new presenter track and apply the presenter effect.
if (!this.localPresenterVideo && !mute) {
const { height, width } = this.localVideo.track.getSettings() ?? this.localVideo.track.getConstraints();
const isPortrait = height >= width;
let { aspectRatio, height } = this.localVideo.track.getSettings();
const { width } = this.localVideo.track.getSettings();
let desktopResizeConstraints = {};
let resizeDesktopStream = false;
const DESKTOP_STREAM_CAP = 720;
// Config.js setting for resizing high resolution desktop tracks to 720p when presenter is turned on.
const resizeEnabled = config.videoQuality && config.videoQuality.resizeDesktopForPresenter;
const highResolutionTrack
= (isPortrait && width > DESKTOP_STREAM_CAP) || (!isPortrait && height > DESKTOP_STREAM_CAP);
// Determine the constraints if the desktop track needs to be resized.
// Resizing is needed when the resolution cannot be determined or when
// the window is bigger than 720p.
if (height && width) {
aspectRatio = aspectRatio ?? (width / height).toPrecision(4);
const advancedConstraints = [ { aspectRatio } ];
const isPortrait = height >= width;
// Resizing the desktop track for presenter is causing blurriness of the desktop share on chrome.
// Disable resizing by default, enable it only when config.js setting is enabled.
// Firefox doesn't return width and height for desktop tracks. Therefore, track needs to be resized
// for creating the canvas for presenter.
const resizeDesktopStream = browser.isFirefox() || (highResolutionTrack && resizeEnabled);
if (resizeDesktopStream) {
let desktopResizeConstraints = {};
if (height && width) {
const advancedConstraints = [ { aspectRatio: (width / height).toPrecision(4) } ];
const constraint = isPortrait ? { width: DESKTOP_STREAM_CAP } : { height: DESKTOP_STREAM_CAP };
advancedConstraints.push(constraint);
desktopResizeConstraints.advanced = advancedConstraints;
} else {
desktopResizeConstraints = {
width: 1280,
height: 720
};
// Determine which dimension needs resizing and resize only that side
// keeping the aspect ratio same as before.
if (isPortrait && width > DESKTOP_STREAM_CAP) {
resizeDesktopStream = true;
advancedConstraints.push({ width: DESKTOP_STREAM_CAP });
} else if (!isPortrait && height > DESKTOP_STREAM_CAP) {
resizeDesktopStream = true;
advancedConstraints.push({ height: DESKTOP_STREAM_CAP });
}
// Apply the contraints on the desktop track.
desktopResizeConstraints.advanced = advancedConstraints;
} else {
resizeDesktopStream = true;
desktopResizeConstraints = {
width: 1280,
height: 720
};
}
if (resizeDesktopStream) {
try {
await this.localVideo.track.applyConstraints(desktopResizeConstraints);
} catch (err) {
@@ -1689,22 +1686,20 @@ export default {
return;
}
height = this.localVideo.track.getSettings().height ?? DESKTOP_STREAM_CAP;
}
const trackHeight = resizeDesktopStream
? this.localVideo.track.getSettings().height ?? DESKTOP_STREAM_CAP
: height;
const defaultCamera = getUserSelectedCameraDeviceId(APP.store.getState());
let effect;
try {
effect = await this._createPresenterStreamEffect(trackHeight);
effect = await this._createPresenterStreamEffect(height,
defaultCamera);
} catch (err) {
logger.error('Failed to unmute Presenter Video', err);
logger.error('Failed to unmute Presenter Video');
maybeShowErrorDialog(err);
return;
}
// Replace the desktop track on the peerconnection.
try {
await this.localVideo.setEffect(effect);
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));

View File

@@ -275,13 +275,9 @@ var config = {
// // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
// // the high quality.
// minHeightForQualityLvl: {
// 360: 'standard',
// 360: 'standard,
// 720: 'high'
// },
//
// // Provides a way to resize the desktop track to 720p (if it is greater than 720p) before creating a canvas
// // for the presenter mode (camera picture-in-picture mode with screenshare).
// resizeDesktopForPresenter: false
// }
// },
// // Options for the recording limit notification.

View File

@@ -28,6 +28,9 @@ body {
overflow: hidden;
color: $defaultColor;
background: $defaultBackground;
&.filmstrip-only {
background: transparent;
}
}
/**
@@ -67,6 +70,16 @@ body {
cursor: pointer;
}
/**
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
* preventing transparency in filmstrip-only mode. The selector chosen to
* override this behavior is specific to where the AtlasKitThemeProvider might
* be placed within the app hierarchy.
*/
.filmstrip-only #react > .ckAJgx {
background: transparent;
}
p {
margin: 0;
}

View File

@@ -27,4 +27,84 @@
font-size: 50px;
}
&-filmstrip-only {
background-color: $inlayFilmstripOnlyBg;
color: $inlayFilmstripOnlyColor;
margin-left: 20px;
margin-right: 20px;
margin-top: 20px;
bottom: 30px;
position: absolute;
display: flex;
max-height: 120px;
height: 80%;
right: 0px;
border-radius: 4px;
overflow: hidden;
&__content {
padding: 20px;
display: flex;
justify-content: center;
position: relative;
> .button-control {
align-self: center;
}
> #reloadProgressBar {
position: absolute;
left: 0px;
bottom: 0px;
margin-bottom: 0px;
width: 100%;
border-radius: 0px;
}
}
&__title {
font-size: 18px;
font-weight: 600;
}
&__container {
align-self: center;
}
&__text {
margin-top: 10px;
font-size: 14px;
font-weight: 600;
}
&__icon {
font-size: 50px;
align-self: center;
color: $inlayIconColor;
opacity: 0.6;
}
&__icon-container {
text-align: center;
display: flex;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
&__avatar-container {
height: 100%;
position: relative;
> img {
height: 100%;
}
}
&__icon-background {
background: $inlayIconBg;
opacity: 0.6;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
}
}

View File

@@ -31,8 +31,6 @@
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
#enter_room {
position: relative;
height: 42px;
@@ -48,7 +46,8 @@
}
.header {
background-color: #002637;
background: #06345E;
background-image: linear-gradient(180deg, rgba(8, 110, 202, 0.8) 0%, rgba(8, 110, 202, 0) 100%);
#enter_room {
.enter-room-input-container {
@@ -90,10 +89,6 @@
.welcome-badge {
margin-right: 16px;
}
.welcome-footer {
display: none;
}
}
#videoResolutionLabel {

View File

@@ -175,8 +175,8 @@ $welcomePageFontFamily: inherit;
$welcomePageBackground: none;
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('../images/welcome-background.png');
$welcomePageHeaderBackgroundPosition: center;
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/images/welcome-background.png');
$welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: cover;
$welcomePageHeaderPaddingBottom: 0px;
@@ -184,7 +184,7 @@ $welcomePageHeaderTitleMaxWidth: initial;
$welcomePageHeaderTextAlign: center;
$welcomePageHeaderContainerDisplay: flex;
$welcomePageHeaderContainerMargin: 104px 32px 0 32px;
$welcomePageHeaderContainerMargin: 146px 32px 0 32px;
$welcomePageHeaderTextTitleMarginBottom: 0;
$welcomePageHeaderTextTitleFontSize: 42px;

View File

@@ -19,8 +19,8 @@ body.welcome-page {
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
background-color: #131519;
height: 400px;
background-color: #002637;
height: 480px;
overflow: hidden;
position: relative;
@@ -224,7 +224,6 @@ body.welcome-page {
&.without-content {
.welcome-card {
min-width: 500px;
max-width: 580px;
}
}

View File

@@ -67,6 +67,20 @@
}
}
/**
* Style the filmstrip videos in filmstrip-only mode.
*/
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
.remote-videos-container {
transition: opacity 1s;
}

View File

@@ -145,6 +145,26 @@
}
}
/**
* Override other styles to support vertical filmstrip mode.
*/
.filmstrip-only .vertical-filmstrip {
.filmstrip {
flex-direction: row-reverse;
}
.filmstrip__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
height: 100%;
}
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should

View File

@@ -1,4 +1,9 @@
.video-quality-dialog {
.hide-warning {
height: 0;
visibility: hidden;
}
.video-quality-dialog-title {
margin-bottom: 10px;
}
@@ -104,6 +109,30 @@
word-spacing: unset;
}
}
&.video-not-supported {
.video-quality-dialog-labels {
color: gray;
}
.video-quality-dialog-slider {
@mixin sliderTrackDisabledStyles() {
background: rgba(14, 22, 36, 0.1);
}
&::-ms-track {
@include sliderTrackDisabledStyles();
}
&::-moz-range-track {
@include sliderTrackDisabledStyles();
}
&::-webkit-slider-runnable-track {
@include sliderTrackDisabledStyles();
}
}
}
}
.modal-dialog-form {

View File

@@ -8,10 +8,16 @@
position: fixed;
z-index: $overlayZ;
background: $defaultBackground;
&.filmstrip-only {
@include transparentBg($filmstripOnlyOverlayBg, 0.8);
}
}
&__container-light {
@include transparentBg($defaultBackground, 0.7);
&.filmstrip-only {
@include transparentBg($filmstripOnlyOverlayBg, 0.2);
}
}
&__content {
@@ -21,6 +27,11 @@
width: 56%;
left: 50%;
@include transform(translateX(-50%));
&.filmstrip-only {
left: 0px;
width: 100%;
@include transform(none);
}
&_bottom {
position: absolute;

View File

@@ -41,6 +41,7 @@ $overlayButtonBg: #0074E0;
* Color variables
**/
$defaultBackground: #474747;
$filmstripOnlyOverlayBg: #000;
$reloadProgressBarBg: #0074E0;
/**
@@ -58,6 +59,10 @@ $dialogErrorText: #344563;
**/
$inlayColorBg: lighten($defaultBackground, 20%);
$inlayBorderColor: lighten($baseLight, 10%);
$inlayIconBg: #000;
$inlayIconColor: #fff;
$inlayFilmstripOnlyColor: #474747;
$inlayFilmstripOnlyBg: #fff;
// Main controls
$placeHolderColor: #a7a7a7;

View File

@@ -13,7 +13,9 @@
height: 180,
parentNode: undefined,
configOverwrite: {},
interfaceConfigOverwrite: {}
interfaceConfigOverwrite: {
filmStripOnly: true
}
}
var api = new JitsiMeetExternalAPI(domain, options);
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 290 KiB

View File

@@ -97,6 +97,11 @@ var interfaceConfig = {
FILM_STRIP_MAX_HEIGHT: 120,
/**
* Whether to only show the filmstrip (and hide the toolbar).
*/
filmStripOnly: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
/**

View File

@@ -27,12 +27,12 @@
"enGB": "Inglese (Regno Unito)",
"da": "Danese",
"ca": "Catalano",
"zhTW": "Cinese (Taiwan)",
"nl": "Olandese",
"hu": "Ungaro",
"hr": "Croato",
"frCA": "Francese (Canada)",
"fi": "Finlandese",
"et": "Etiope",
"esUS": "Spagnolo (USA)"
"zhTW": "",
"nl": "",
"hu": "",
"hr": "",
"frCA": "",
"fi": "",
"et": "",
"esUS": ""
}

View File

@@ -1,48 +1,27 @@
{
"en": "英語",
"af": "アフリカーンス語",
"ar": "アラビア語",
"az": "アゼルバイジャン語",
"bg": "ブルガリア語",
"ca": "カタルーニャ語",
"cs": "チェコ語",
"da": "デンマーク語",
"de": "ドイツ語",
"el": "ギリシア語",
"enGB": "英語 (英国)",
"eo": "エスペラント語",
"es": "スペイン語",
"esUS": "スペイン語 (ラテンアメリカ)",
"et": "エストニア語",
"eu": "バスク語",
"fi": "フィンランド語",
"fr": "フランス語",
"frCA": "フランス語 (カナダ)",
"he": "ヘブライ語",
"mr": "マラーティー語",
"hr": "クロアチア語",
"hu": "ハンガリー語",
"hy": "アルメニア語",
"id": "インドネシア語",
"it": "イタリア語",
"ja": "日本語",
"kab": "カビル語",
"ko": "韓国語",
"lt": "リトアニア語",
"nl": "オランダ語",
"nb": "ノルウェー語 (ブークモール)",
"oc": "オック語",
"pl": "ポーランド語",
"ptBR": "ポルトガル語 (ブラジル)",
"ru": "ロシア語",
"ro": "ルーマニア語",
"sc": "サルデーニャ語",
"sk": "スロバキア語",
"sl": "スロベニア語",
"sr": "セルビア語",
"sv": "スウェーデン語",
"th": "タイ語",
"tr": "トルコ語",
"uk": "ウクライナ語",
"vi": "ベトナム語",
"zhCN": "中国語 (中国)",
"zhTW": "中国語 (台湾)"
}
"zhCN": "中国語 (中国)"
}

View File

@@ -1,50 +1,48 @@
{
"en": "Anglés",
"af": "Afrikaans",
"ar": "Arabi",
"bg": "Bulgar",
"ca": "Catalan",
"cs": "Chèc",
"da": "Danés",
"de": "Aleman",
"el": "Grèc",
"enGB": "Anglés (Reialme Unit)",
"eo": "Esperanto",
"es": "Castelhan",
"esUS": "Espanhòl (America latina)",
"et": "Estonian",
"eu": "Basc",
"fi": "Finés",
"fr": "Francés",
"frCA": "Francés (Canadian)",
"he": "Ebrèu",
"mr":"Marathi",
"hr": "Croat",
"hu": "Ongrés",
"hy": "Armenian",
"id": "Indonesian",
"it": "Italian",
"ja": "Japonés",
"kab": "Cabil",
"ko": "Corean",
"lt": "Lituanian",
"nl": "Neerlandés",
"oc": "Occitan",
"pl": "Polonés",
"ptBR": "Portugués (Brasil)",
"ru": "Rus",
"ro": "Romanian",
"sc": "Sarde",
"sk": "Eslovac",
"sl": "Eslovèn",
"sr": "Sèrbe",
"sv": "Suedés",
"th": "Tai",
"tr": "Turc",
"uk": "Ucraïnian",
"vi": "Vietnamian",
"zhCN": "Chinés (China)",
"zhTW": "Chinés (Taiwan)"
"zhTW": "Chinés (Taiwan)",
"et": "Estonian",
"da": "Danés",
"uk": "Ucraïnian",
"th": "Tai",
"sk": "Eslovac",
"sc": "Sarde",
"lt": "Lituanian",
"id": "Indonesian",
"he": "Ebrèu",
"eu": "Basc",
"mr": "Marathi",
"sl": "Eslovèn",
"ro": "Romanian",
"ar": "Arabi"
}

View File

@@ -99,11 +99,9 @@
},
"connectionindicator": {
"address": "Adresse:",
"audio_ssrc": "Audio-SSRC:",
"bandwidth": "Geschätzte Bandbreite:",
"bitrate": "Bitrate:",
"bridgeCount": "Serverzahl: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Verbunden mit:",
"e2e_rtt": "E2E RTT:",
"framerate": "Bildwiederholrate:",
@@ -127,12 +125,9 @@
"remoteport": "Entfernter Port:",
"remoteport_plural": "Entfernte Ports:",
"resolution": "Auflösung:",
"savelogs": "Logs speichern",
"participant_id": "Teilnehmer-ID:",
"status": "Verbindung:",
"transport": "Protokoll:",
"transport_plural": "Protokolle:",
"video_ssrc": "Video-SSRC:"
"transport_plural": "Protokolle:"
},
"dateUtils": {
"earlier": "Früher",
@@ -201,7 +196,10 @@
"displayNameRequired": "Hallo! Wie ist Ihr Name?",
"done": "Fertig",
"e2eeDescription": "Ende-zu-Ende-Verschlüsselung ist derzeit noch EXPERIMENTELL. Bitte beachten Sie, dass das Aktivieren der Ende-zu-Ende-Verschlüsselung diverse serverseitige Funktionen deaktiviert: Aufnahmen, Livestreaming und Telefoneinwahl. Bitte beachten Sie außerdem, dass der Konferenz dann nur noch mit Browsern beigetreten werden kann, die Insertable Streams unterstützen.",
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeLabel": "E2EE-Schlüssel",
"e2eeNoKey": "Keiner",
"e2eeToggleSet": "Schlüssel festlegen",
"e2eeSet": "Setzen",
"e2eeWarning": "WARNUNG: Nicht alle Teilnehmer dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Teilnehmer nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
@@ -366,7 +364,7 @@
"password": "$t(lockRoomPasswordUppercase):",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"label": "Einwahlinformationen"
"label": "Konferenzinformationen"
},
"inviteDialog": {
"alertText": "Die Einladung einiger Teilnehmer ist fehlgeschlagen.",
@@ -505,7 +503,6 @@
"poweredby": "Betrieben von",
"prejoin": {
"audioAndVideoError": "Audio- und Videofehler:",
"audioDeviceProblem": "Es gibt ein Problem mit Ihrem Audiogerät.",
"audioOnlyError": "Audiofehler:",
"audioTrackError": "Audiotrack konnte nicht erstellt werden.",
"calling": "Rufaufbau",
@@ -513,35 +510,15 @@
"callMeAtNumber": "Mich unter dieser Nummer anrufen:",
"configuringDevices": "Geräte werden eingerichtet …",
"connectedWithAudioQ": "Sie sind mit Audio verbunden?",
"connection": {
"good": "Ihre Internetverbindung sieht gut aus!",
"nonOptimal": "Ihre Internetverbindung ist nicht optimal.",
"poor": "Sie haben eine schlechte Internetverbindung."
},
"connectionDetails": {
"audioClipping": "Ihr Ton wird wahrscheinlich abgehackt sein.",
"audioHighQuality": "Ihr Ton sollte exzellent klingen.",
"audioLowNoVideo": "Ihr Ton wird wahrscheinlich schlecht klingen und es wird kein Video geben.",
"goodQuality": "Großartig! Ihre Bild- und Tonqualität sollte super sein.",
"noMediaConnectivity": "Es konnte für diesen Test keine Medienverbindung hergestellt werden. Das wird gewöhnlich durch eine Firewall oder ein NAT ausgelöst.",
"noVideo": "Ihr Bild wird wahrscheinlich eine schlechte Qualität haben.",
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Entwickler der Webanwendung.",
"veryPoorConnection": "Ihre Konferenzqualität wird wahrscheinlich sehr schlecht sein.",
"videoFreezing": "Ihr Bild wird wahrscheinlich einfrieren, schwarz werden und eine geringe Auflösung haben.",
"videoHighQuality": "Ihr Bild sollte sehr gut aussehen.",
"videoLowQuality": "Ihr Bild wird wahrscheinlich eine geringe Auflösung und Bildrate haben.",
"videoTearing": "Ihr Bild wird wahrscheinlich eine geringe Auflösung haben oder Artefakte aufweisen."
},
"copyAndShare": "Konferenzlink kopieren & teilen",
"dialInMeeting": "Telefoneinwahl",
"dialInPin": "In die Konferenz einwählen und PIN eingeben:",
"dialing": "Wählen",
"doNotShow": "Diesen Bildschirm nicht mehr anzeigen",
"doNotShow": "Nicht mehr anzeigen",
"errorDialOut": "Anruf fehlgeschlagen",
"errorDialOutDisconnected": "Anruf fehlgeschlagen. Verbindungsabbruch",
"errorDialOutFailed": "Anruf fehlgeschlagen. Anruf fehlgeschlagen",
"errorDialOutStatus": "Fehler beim Abrufen des Anrufstatus",
"errorMissingName": "Bitte geben Sie Ihren Namen ein, um der Konferenz beizutreten.",
"errorStatusCode": "Anruf fehlgeschlagen. Statuscode: {{status}}",
"errorValidation": "Nummerverifikation fehlgeschlagen",
"iWantToDialIn": "Ich möchte mich einwählen",
@@ -698,6 +675,7 @@
"document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten",
"e2ee": "Ende-zu-Ende-Verschlüsselung",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Zum Moderator machen",
@@ -876,12 +854,12 @@
"getHelp": "Hilfe",
"go": "Los",
"goSmall": "Los",
"info": "Einwahlinformationen",
"info": "Informationen",
"join": "ERSTELLEN / BEITRETEN",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, unter der Sie der einzige Moderator sind.",
"privacy": "Datenschutz",
"recentList": "Verlauf",
"recentListDelete": "Eintrag löschen",
"recentListDelete": "Löschen",
"recentListEmpty": "Ihr Konferenzverlauf ist derzeit leer. Reden Sie mit Ihrem Team und Ihre vergangenen Konferenzen landen hier.",
"reducedUIText": "Willkommen bei {{app}}!",
"roomNameAllowedChars": "Der Konferenzname sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,7 @@
"bluetooth": "Bluetooth",
"headphones": "Cuffie",
"phone": "Telefono",
"speaker": "Vivavoce",
"none": ""
"speaker": "Vivavoce"
},
"audioOnly": {
"audioOnly": "Solo audio"
@@ -52,12 +51,7 @@
"popover": "Scegli un nickname",
"title": "Inserire un nickname per utilizzare la chat"
},
"title": "Chat",
"you": "",
"privateNotice": "",
"noMessagesMessage": "",
"messageTo": "",
"fieldPlaceHolder": ""
"title": "Chat"
},
"connectingOverlay": {
"joiningRoom": "Collegamento al tuo meeting in corso…"
@@ -72,11 +66,7 @@
"DISCONNECTED": "Disconnesso",
"DISCONNECTING": "Disconnessione in corso",
"ERROR": "Errore",
"RECONNECTING": "Si è verificato un problema di rete. Riconnessione...",
"LOW_BANDWIDTH": "",
"GOT_SESSION_ID": "",
"GET_SESSION_ID_ERROR": "",
"FETCH_SESSION_ID": ""
"RECONNECTING": "Si è verificato un problema di rete. Riconnessione..."
},
"connectionindicator": {
"address": "Indirizzo:",
@@ -106,9 +96,7 @@
"resolution": "Risoluzione:",
"status": "Connessione:",
"transport": "Trasporto:",
"transport_plural": "Trasporti:",
"turn": "(ruota)",
"e2e_rtt": ""
"turn": "(ruota)"
},
"dateUtils": {
"earlier": "Prima",
@@ -120,10 +108,8 @@
"description": "Non è successo nulla? Abbiamo provato ad avviare la tua videoconferenza sull'app desktop di {{app}}. Prova di nuovo o avviala nell'app web di {{app}}.",
"descriptionWithoutWeb": "",
"downloadApp": "Scarica l'app",
"ifDoNotHaveApp": "Se non hai ancora l'app:",
"ifHaveApp": "Se hai già l'app:",
"joinInApp": "Entra in riunione usando l'app",
"launchWebButton": "Avvia sul web",
"openApp": "Prosegui verso l'app",
"title": "Sto avviando la tua videoconferenza su {{app}}…",
"tryAgainButton": "Prova di nuovo sul desktop"
},
@@ -191,7 +177,7 @@
"kickParticipantDialog": "Sei sicuro di voler espellere questo partecipante?",
"kickParticipantTitle": "Espellere questi partecipante?",
"kickTitle": "Espulso dal meeting",
"liveStreaming": "Diretta",
"liveStreaming": "Live Streaming",
"liveStreamingDisabledForGuestTooltip": "Gli ospiti non possono avviare una diretta.",
"liveStreamingDisabledTooltip": "Trasmissioni in diretta disabilitate.",
"lockMessage": "Impossibile bloccare la conferenza.",
@@ -255,7 +241,7 @@
"stopLiveStreaming": "Ferma la diretta",
"stopRecording": "Ferma registrazione",
"stopRecordingWarning": "Sei sicuro di voler interrompere la registrazione?",
"stopStreamingWarning": "Sei sicuro di voler interrompere la diretta?",
"stopStreamingWarning": "Sei sicuro di voler interrompere il live streaming?",
"streamKey": "Chiave per trasmissione in diretta",
"Submit": "Invia",
"thankYou": "Grazie per aver usato {{appName}}!",
@@ -269,18 +255,7 @@
"WaitForHostMsgWOk": "La conferenza <b>{{room}}</b> non è ancora cominciata. Se sei l'organizzatore, allora premi OK per autenticarti. Altrimenti, aspetta l'arrivo dell'organizzatore.",
"WaitingForHost": "In attesa dell'organizzatore ...",
"Yes": "Sì",
"yourEntireScreen": "Schermo intero",
"sendPrivateMessageTitle": "",
"sendPrivateMessageOk": "",
"sendPrivateMessageCancel": "",
"sendPrivateMessage": "",
"screenSharingAudio": "",
"muteEveryoneStartMuted": "",
"muteEveryoneSelf": "",
"muteEveryoneTitle": "",
"muteEveryoneDialog": "",
"muteEveryoneElseTitle": "",
"muteEveryoneElseDialog": ""
"yourEntireScreen": "Schermo intero"
},
"dialOut": {
"statusMessage": "è ora {{status}}"
@@ -359,28 +334,27 @@
"toggleFilmstrip": "Mostra o nascondi anteprime video",
"toggleScreensharing": "Cambia modalità tra videocamera e condivisione schermo",
"toggleShortcuts": "Mostra o nascondi le scorciatoie",
"videoMute": "Accendo o spegni la videocamera",
"videoQuality": ""
"videoMute": "Accendo o spegni la videocamera"
},
"liveStreaming": {
"busy": "Stiamo cercando di liberare risorse per la diretta. Riprova tra qualche minuto.",
"busy": "Stiamo cercando di liberare risorse per lo streaming. Riprova tra qualche minuto.",
"busyTitle": "Tutti gli streamer sono impegnati al momento",
"changeSignIn": "Cambia account",
"choose": "Scegli una trasmissione in diretta",
"chooseCTA": "Scegli un'opzione di trasmissione. Attualmente sei loggato come {{email}}.",
"enterStreamKey": "Inserisci qui la tua chiave YouTube per le trasmissioni in diretta.",
"error": "Diretta fallita. Prova di nuovo.",
"error": "Live streaming fallito. Prova di nuovo.",
"errorAPI": "Si è verificato un errore durante l'accesso ai tuoi broadcast YouTube. Prova a effettuare nuovamente il login.",
"errorLiveStreamNotEnabled": "La diretta non è attivata su {{email}}. Per favore abilita la diretta o effettua l'accesso con un account abilitato alle dirette.",
"expandedOff": "La diretta è stata interrotta",
"expandedOn": "La conferenza è attualmente in diretta su YouTube.",
"expandedPending": "La diretta è in fase di avvio...",
"expandedOff": "La diretta &egrave; stata interrotta",
"expandedOn": "La conferenza &egrave; attualmente in diretta su YouTube.",
"expandedPending": "La diretta &egrave; in fase di avvio...",
"failedToStart": "Avvio trasmissione in diretta fallito",
"getStreamKeyManually": "Non siamo stati in grado di trovare nessuna trasmissione dal vivo. Prova ad ottenere una chiave stream da Youtube",
"invalidStreamKey": "La chiave stream potrebbe non essere corretta.",
"off": "La diretta si è interrotta",
"on": "Trasmissione in diretta",
"pending": "Avvio diretta...",
"pending": "Avvio live stream...",
"serviceName": "Servizio live streaming",
"signedInAs": "Sei attualmente collegato come:",
"signIn": "Registrati con Google",
@@ -388,11 +362,7 @@
"signOut": "Esci",
"start": "Inizia una diretta",
"streamIdHelp": "Cos'è questo?",
"unavailableTitle": "La diretta non è disponibile",
"onBy": "",
"offBy": "",
"googlePrivacyPolicy": "Google Privacy Policy",
"youtubeTerms": "YouTube terms of services"
"unavailableTitle": "Live streaming non disponibile"
},
"localRecording": {
"clientState": {
@@ -410,8 +380,8 @@
"me": "io",
"messages": {
"engaged": "Registrazione locale avviata.",
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione al moderatore.",
"finishedModerator": "La registrazione della sessione {{token}} è terminata. Il file della traccia local è stato salvato. Richiedere ai partecipanti di inviare le loro registrazioni.",
"finished": "La registrazione della sessione {{token}} &egrave; terminata. Invia il file della registrazione al moderatore.",
"finishedModerator": "La registrazione della sessione {{token}} &egrave; terminata. Il file della traccia local &egrave; stato salvato. Richiedere ai partecipanti di inviare le loro registrazioni.",
"notModerator": "Non sei un moderatore. Non puoi avviare o interrompere la registrazione"
},
"moderator": "Moderatore",
@@ -455,8 +425,7 @@
"unmute": "",
"newDeviceCameraTitle": "Trovata nuova videocamera",
"newDeviceAudioTitle": "Trovata nuova origine audio",
"newDeviceAction": "Usala",
"suboptimalBrowserWarning": "Ci spiace che la tua videoconferenza non sarà ottimale, qui. Stiamo cercando modi per migliorare, ma fino ad allora per favore prova ad usare <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser supportati completamente/a>."
"newDeviceAction": "Usala"
},
"passwordSetRemotely": "definita da altro utente",
"passwordDigitsOnly": "Fino a {{number}} cifre",
@@ -504,9 +473,7 @@
"signIn": "Entra",
"signOut": "Esci",
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
"unavailableTitle": "Registrazione non disponibile",
"onBy": "{{name}} registrazione iniziata",
"offBy": "{{name}} registrazione fermata"
"unavailableTitle": "Registrazione non disponibile"
},
"sectionList": {
"pullToRefresh": "Trascina per aggiornare"
@@ -532,9 +499,7 @@
"selectMic": "Microfono",
"startAudioMuted": "Tutti cominciano con il microfono disattivato",
"startVideoMuted": "Tutti cominciano con il video disattivato",
"title": "Impostazioni",
"speakers": "Altoparlanti",
"microphones": "Microfoni"
"title": "Impostazioni"
},
"settingsView": {
"alertOk": "OK",
@@ -549,11 +514,7 @@
"serverURL": "URL del server",
"startWithAudioMuted": "Inizia con l'audio disattivato",
"startWithVideoMuted": "Avvia con il video disattivato",
"version": "Versione",
"showAdvanced": "Mostra impostazioni avanzate",
"disableP2P": "Disabilita modalità uno-a-uno",
"disableCallIntegration": "Disabilita integrazione nativa delle chiamate",
"advanced": "Avanzate"
"version": "Versione"
},
"share": {
"dialInfoText": "\n\n=====\n\nVuoi solo ascoltare la conferenza da un telefono?\n\n{{defaultDialInNumber}}Clicca questo link per vedere i numeri telefonici di questo meeting\n{{dialInfoPageUrl}}",
@@ -610,12 +571,7 @@
"tileView": "Vedi tutti i partecipanti insieme, o uno solo",
"toggleCamera": "Cambia videocamera",
"videomute": "Attiva/disattiva videocamera",
"videoblur": "Attiva/disattiva offuscamento video",
"privateMessage": "Invia messaggio privato",
"muteEveryone": "Zittisci tutti",
"moreOptions": "Mostra più opzioni",
"help": "Aiuto",
"download": "Scarica altre app"
"videoblur": "Attiva/disattiva offuscamento video"
},
"addPeople": "Aggiungi persone alla chiamata",
"audioOnlyOff": "Anche video",
@@ -638,6 +594,7 @@
"logout": "Logout",
"lowerYourHand": "Abbassa la mano",
"moreActions": "Più azioni",
"moreOptions": "Più opzioni",
"mute": "Microfono Attiva / Disattiva",
"openChat": "Apri una chat",
"pip": "Abilita visualizzazione immagine nellimmagine",
@@ -741,7 +698,7 @@
"privacy": "Privacy",
"recentList": "Recente",
"recentListDelete": "Cancella",
"recentListEmpty": "La tua lista è vuota. Chatta con qualcuno del tuo team e lo vedrai apparire nella lista di meeting recenti.",
"recentListEmpty": "La tua lista &egrave; vuota. Chatta con qualcuno del tuo team e lo vedrai apparire nella lista di meeting recenti.",
"reducedUIText": "",
"roomname": "Inserisci Nome Stanza",
"roomnameHint": "Inserisci il nome o l'URL della stanza alla quale vuoi accedere. Puoi anche inventarti un nome, assicurati solo che le persone che vuoi contattare lo sappiano, così che possano inserire lo stesso nome.",
@@ -749,18 +706,8 @@
"terms": "Termini di utilizzo",
"title": "Il sistema di conferenza sicuro, funzionale e completamente gratuito."
},
"documentSharing": {
"title": ""
},
"defaultNickname": "",
"chromeExtensionBanner": {
"dontShowAgain": "",
"buttonText": "",
"installExtensionText": ""
},
"raisedHand": "Vorrebbe parlare",
"lonelyMeetingExperience": {
"button": "Invita altri",
"button": "Invita gli altri",
"youAreAlone": "Sei l'unico in riunione"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -122,13 +122,7 @@
"status": "Connexion :",
"transport": "Transpòrt :",
"transport_plural": "Transpòrts :",
"e2e_rtt": "E2E RTT:",
"codecs": "Codecs (A/V): ",
"video_ssrc": "Vidèo SSRC:",
"maxEnabledResolution": "enviar max",
"savelogs": "Enregistrar jornals",
"participant_id": "Id participant:",
"audio_ssrc": "Àudio SSRC:"
"e2e_rtt": "E2E RTT:"
},
"dateUtils": {
"earlier": "Mai dora",
@@ -301,20 +295,7 @@
"muteEveryoneSelf": "vos",
"muteEveryoneTitle": "Rendre mut tot lo monde?",
"muteEveryoneDialog": "Volètz vertadièrament copar lo son a tot lo monde? Poiretz pas lo restablir, mas eles poiràn o far quora que vòlgan.",
"muteEveryoneElseTitle": "Copar lo son a totes levat {{whom}}?",
"add": "Ajustar",
"copied": "Copiat",
"grantModeratorDialog": "Volètz vertadièrament far venir aqueste participant moderator?",
"readMore": "mai",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible pendent una difusion activa",
"screenSharingFailedTitle": "Fracàs del partiment d'ecran!",
"e2eeLabel": "Activar lo chiframant del cap a la fin",
"grantModeratorTitle": "Passar moderator",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible pendent un enregistrament actiu",
"e2eeDescription": "Lo chiframent del cap a la fin es actualament EXPERIMENTALA. Mercés de gardar a l'esperit qu'activar lo chiframent del cap a la fin desactivarà en efièch los servicis costat servidor coma: l'enregistrament, la difusion en dirècte e las participacions telefonicas. Remembratz tanben que la conferéncia foncionarà pas que per lo monde que participan amb un navigador compatible amb los fluxes inseribles.",
"screenSharingFailed": "Ops! Quicòm a trucat, avèm pas pogut començar lo partiment d'ecran!",
"e2eeWarning": "AVERTIMENT: pas totes los participants d'aquesta conferéncia semblan poder suportar lo chiframent del cap a la fin. Se l'activatz poiràn pas vos veire nimai vos entendre.",
"muteEveryoneElseDialog": "Un còp mut, poiretz pas mai lo tornar la paraula, mas la se pòdon tornar quora vòlgan."
"muteEveryoneElseTitle": "Copar lo son a totes levat {{whom}}?"
},
"dialOut": {
"statusMessage": "ara es {{status}}"
@@ -429,9 +410,7 @@
"streamIdHelp": "Ques aquò ?",
"unavailableTitle": "Difusion en dirècte indisponibla",
"googlePrivacyPolicy": "Politica de confidencialitat de Google",
"youtubeTerms": "Condicions dutilizacion de YouTube",
"limitNotificationDescriptionWeb": "A causa d'una brava demanda vòstra difusion serà limitada a {{limit}} min. Per de difusion illimitada ensajatz <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Vòstra difusion serà limitada a {{limit}} min. Per de difusions illimitada ensajatz {{app}}."
"youtubeTerms": "Condicions dutilizacion de YouTube"
},
"localRecording": {
"clientState": {
@@ -663,10 +642,7 @@
"muteEveryone": "Rendre mut tot lo monde",
"moreOptions": "Mostrar mai dopcions",
"e2ee": "Chiframent del cap a la fin",
"security": "Opcions de seguretat",
"embedMeeting": "Conferéncia integrada",
"grantModerator": "Passar moderator",
"lobbyButton": "Activar/Desactivar mòde sala d'espèra"
"security": "Opcions de seguretat"
},
"addPeople": "Ajustar de monde a vòstra sonada",
"audioOnlyOff": "Desactivar lo mòde connexion febla",
@@ -717,17 +693,14 @@
"videomute": "Aviar / Arrestar la camèra",
"startvideoblur": "Trebolar mon rèire-plan",
"stopvideoblur": "Desactivar lo borrolatge del rèire-plan",
"noisyAudioInputDesc": "Sembla que vòstre microfòn mene bruch, pensatz de lo copar o de lo cambiar.",
"noisyAudioInputDesc": "",
"noisyAudioInputTitle": "Vòstre microfòn sembla brusent !",
"noAudioSignalDialInLinkDesc": "",
"noAudioSignalDialInDesc": "",
"muteEveryone": "Rendre mut tot lo monde",
"moreOptions": "Autras opcions",
"e2ee": "Chiframent del cap a la fin",
"security": "Opcions de seguretat",
"embedMeeting": "Integrar conferéncia",
"lobbyButtonDisable": "Desactivar lo mòde sala d'espèra",
"lobbyButtonEnable": "Activar mòde sala d'espèra"
"security": "Opcions de seguretat"
},
"transcribing": {
"ccButtonTooltip": "Aviar / Arrestat los sostítols",
@@ -822,12 +795,7 @@
"sendFeedback": "Mandar vòstra opinion",
"terms": "Tèrmes",
"title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas",
"getHelp": "Obténer dajuda",
"startMeeting": "Començar la reünion",
"jitsiOnMobile": "Jitsi sus mobil telecargatz nòstra aplicacion e començatz de conferéncias de pertot",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservatz una URL de conferéncia</a> a l'avança ont sètz l'unic moderator.",
"jitsiMeet": "Jitsi Meet",
"secureMeetings": "Conferéncias seguras e de nauta qualitat"
"getHelp": "Obténer dajuda"
},
"helpView": {
"header": "Centre dajuda"
@@ -864,27 +832,7 @@
"initiated": "Sonada aviada",
"joinWithoutAudio": "Rejónher sens àudio",
"joinMeeting": "Rejónher la conferéncia",
"joinAudioByPhone": "Rejónher amb làudio del telefòn",
"audioDeviceProblem": "I a un problèma amb vòstre periferic àudio",
"showScreen": "Activar l'ecran de preconferéncia",
"connection": {
"good": "Vòstra connexion Internet sembla bona!",
"nonOptimal": "Vòstra connexion Internet es pas optimala",
"poor": "Vòstra connexion Internet es febla"
},
"connectionDetails": {
"videoHighQuality": "Nos esperam a trobar vòstra qualitat vidèo de bona qualitat.",
"audioClipping": "Nos esperam a trobar vòstre àudio troncat.",
"audioHighQuality": "Nos esperam a trobar vòstra qualitat àudio excellenta.",
"audioLowNoVideo": "Nos esperam a trobar vòstra qualitat àudio febla e cap de vidèo.",
"goodQuality": "Crane! Vòstra qualitat serà geniala.",
"noMediaConnectivity": "Avèm pas trobat cap de biais d'establir una connectivitat mèdia per aquesta pròva. Sovent es a causat d'un parafòc o un NAT.",
"noVideo": "Nos esperam a trobat vòstra qualitat vidèo òrra.",
"veryPoorConnection": "Nos esperam a trobar vòstra qualitat vidèo plan òrra.",
"videoFreezing": "Nos esperam a veire vòstra vidèo se gelar, venir negra e se pixelizar."
},
"premeeting": "Preconferéncia",
"errorMissingName": "Mercés de picar vòstre nom per rejónher la conferéncia"
"joinAudioByPhone": "Rejónher amb làudio del telefòn"
},
"lobby": {
"reject": "Regetar",
@@ -907,15 +855,7 @@
"emailField": "Picata vòstra adreça electronica",
"disableDialogSubmit": "Desactivar",
"backToKnockModeButton": "Cap de senhal, demandar a participar a la plaça",
"allow": "Autorizar",
"knockingParticipantList": "Lista de participants en espèra",
"dialogTitle": "Mòde sala d'espèra",
"notificationLobbyDisabled": "Lo mòde sala d'espèra es estat desactivat per {{originParticipantName}}",
"notificationLobbyEnabled": "Lo mòde sala d'espèra activat per {{originParticipantName}}",
"notificationTitle": "Sala d'espèra",
"toggleLabel": "Activar la sala d'espèra",
"notificationLobbyAccessDenied": "{{originParticipantName}} a decidit de regetar la demanda de {{targetParticipantName}}",
"notificationLobbyAccessGranted": "{{originParticipantName}} a autorizat {{targetParticipantName}} a dintrar"
"allow": "Autorizar"
},
"security": {
"securityOptions": "Opcions de seguretat",
@@ -925,8 +865,7 @@
},
"e2ee": {
"labelToolTip": "La comunicacion àudio e vidèo daquesta sonada es chifrada del cap a la fin"
},
"embedMeeting": {
"title": "Integrar aquesta conferéncia"
}
}

View File

@@ -149,10 +149,7 @@
"launchWebButton": "Запустить в браузере",
"openApp": "Перейти к приложению",
"title": "Запуск вашей встречи в {{app}}...",
"tryAgainButton": "Повторите в настольном приложении",
"joinInApp": "Присоединиться к этой встрече с помощью приложения",
"ifHaveApp": "Если у Вас уже есть приложение:",
"ifDoNotHaveApp": "Если у Вас ещё нет приложения:"
"tryAgainButton": "Повторите в настольном приложении"
},
"defaultLink": "напр. {{url}}",
"defaultNickname": "напр. Яна Цветкова",

View File

@@ -678,7 +678,6 @@
},
"startupoverlay": {
"policyText": " ",
"genericTitle": "The meeting needs to use your microphone and camera.",
"title": "{{app}} needs to use your microphone and camera."
},
"suspendedoverlay": {
@@ -838,6 +837,8 @@
"ld": "LD",
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"onlyAudioAvailable": "Only audio is available",
"onlyAudioSupported": "We only support audio in this browser.",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition"
@@ -872,10 +873,9 @@
"getHelp": "Get help",
"go": "GO",
"goSmall": "GO",
"headerTitle": "Jitsi Meet",
"headerSubtitle": "Secure and high quality meetings",
"info": "Dial-in info",
"join": "CREATE / JOIN",
"jitsiMeet": "Jitsi Meet",
"jitsiOnMobile": "Jitsi on mobile download our apps and start a meeting from anywhere",
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
"privacy": "Privacy",
@@ -887,6 +887,7 @@
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"secureMeetings": "Secure and high quality meetings",
"startMeeting": "Start meeting",
"terms": "Terms",
"title": "Secure, fully featured, and completely free video conferencing"

View File

@@ -124,13 +124,16 @@ function changeParticipantNumber(APIInstance, number) {
* configuration options defined in interface_config.js to be overridden.
* @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
* authentication.
* @param {boolean} [options.noSSL] - If the value is true https won't be used.
* @param {string} [options.roomName] - The name of the room to join.
* @returns {string} The URL.
*/
function generateURL(domain, options = {}) {
return urlObjectToString({
...options,
url: `https://${domain}/#jitsi_meet_external_api_id=${id}`
url:
`${options.noSSL ? 'http' : 'https'}://${
domain}/#jitsi_meet_external_api_id=${id}`
});
}
@@ -161,6 +164,7 @@ function parseArguments(args) {
parentNode,
configOverwrite,
interfaceConfigOverwrite,
noSSL,
jwt,
onload
] = args;
@@ -172,6 +176,7 @@ function parseArguments(args) {
parentNode,
configOverwrite,
interfaceConfigOverwrite,
noSSL,
jwt,
onload
};
@@ -232,6 +237,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* configuration options defined in config.js to be overridden.
* @param {Object} [options.interfaceConfigOverwrite] - Object containing
* configuration options defined in interface_config.js to be overridden.
* @param {boolean} [options.noSSL] - If the value is true https won't be
* used.
* @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
* authentication.
* @param {string} [options.onload] - The onload function that will listen
@@ -254,6 +261,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
parentNode = document.body,
configOverwrite = {},
interfaceConfigOverwrite = {},
noSSL = false,
jwt = undefined,
onload = undefined,
invitees,
@@ -268,6 +276,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
configOverwrite,
interfaceConfigOverwrite,
jwt,
noSSL,
roomName,
devices,
userInfo,

View File

@@ -1,4 +1,4 @@
/* global APP, $, config */
/* global APP, $, config, interfaceConfig */
const UI = {};
@@ -143,7 +143,9 @@ UI.start = function() {
$.prompt.setDefaults({ persistent: false });
VideoLayout.init(eventEmitter);
VideoLayout.initLargeVideo();
if (!interfaceConfig.filmStripOnly) {
VideoLayout.initLargeVideo();
}
// Do not animate the video area on UI start (second argument passed into
// resizeVideoArea) because the animation is not visible anyway. Plus with
@@ -159,7 +161,10 @@ UI.start = function() {
$('body').addClass('desktop-browser');
}
if (config.iAmRecorder) {
if (interfaceConfig.filmStripOnly) {
$('body').addClass('filmstrip-only');
APP.store.dispatch(setNotificationsEnabled(false));
} else if (config.iAmRecorder) {
// in case of iAmSipGateway keep local video visible
if (!config.iAmSipGateway) {
VideoLayout.setLocalVideoVisible(false);

View File

@@ -135,6 +135,10 @@ export default class RemoteVideo extends SmallVideo {
* @private
*/
_generatePopupContent() {
if (interfaceConfig.filmStripOnly) {
return;
}
const remoteVideoMenuContainer
= this.container.querySelector('.remotevideomenu');

View File

@@ -699,7 +699,7 @@ export default class SmallVideo {
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { true }
enableStatsDisplay = { !interfaceConfig.filmStripOnly }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }

View File

@@ -1,4 +1,4 @@
/* global APP, $ */
/* global APP, $, interfaceConfig */
import Logger from 'jitsi-meet-logger';
@@ -264,6 +264,10 @@ const VideoLayout = {
* @returns {void}
*/
onPinChange(pinnedParticipantID) {
if (interfaceConfig.filmStripOnly) {
return;
}
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
},

View File

@@ -1,4 +1,4 @@
/* global APP, $ */
/* global APP, $, interfaceConfig */
import Logger from 'jitsi-meet-logger';
@@ -203,12 +203,14 @@ const KeyboardShortcut = {
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
if (!interfaceConfig.filmStripOnly) {
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
}
/**
* FIXME: Currently focus keys are directly implemented below in

4
package-lock.json generated
View File

@@ -10771,8 +10771,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"from": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"version": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"from": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",

View File

@@ -54,5 +54,6 @@ export default [
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED'
'VIDEO_QUALITY_LABEL_DISABLED',
'filmStripOnly'
];

View File

@@ -10,6 +10,7 @@ import {
import { APP_STATE_CHANGED } from '../../mobile/background';
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
import { isRoomValid, SET_ROOM } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
@@ -161,33 +162,41 @@ function _setRoom({ dispatch, getState }, next, action) {
// XXX After the introduction of the "Video <-> Voice" toggle on the
// WelcomePage, startAudioOnly is utilized even outside of
// conferences/meetings.
const audioOnly
= Boolean(
getPropertyValue(
state,
'startAudioOnly',
/* sources */ {
// FIXME Practically, base/config is (really) correct
// only if roomIsValid. At the time of this writing,
// base/config is overwritten by URL params which leaves
// base/config incorrect on the WelcomePage after
// leaving a conference which explicitly overwrites
// base/config with URL params.
config: roomIsValid,
let audioOnly;
// XXX We've already overwritten base/config with
// urlParams if roomIsValid. However, settings are more
// important than the server-side config. Consequently,
// we need to read from urlParams anyway. We also
// probably want to read from urlParams when
// !roomIsValid.
urlParams: true,
if (JitsiMeetJS.mediaDevices.supportsVideo()) {
audioOnly
= Boolean(
getPropertyValue(
state,
'startAudioOnly',
/* sources */ {
// FIXME Practically, base/config is (really) correct
// only if roomIsValid. At the time of this writing,
// base/config is overwritten by URL params which leaves
// base/config incorrect on the WelcomePage after
// leaving a conference which explicitly overwrites
// base/config with URL params.
config: roomIsValid,
// The following don't have complications around whether
// they are defined or not:
jwt: false,
settings: true
}));
// XXX We've already overwritten base/config with
// urlParams if roomIsValid. However, settings are more
// important than the server-side config. Consequently,
// we need to read from urlParams anyway. We also
// probably want to read from urlParams when
// !roomIsValid.
urlParams: true,
// The following don't have complications around whether
// they are defined or not:
jwt: false,
settings: true
}));
} else {
// Default to audio-only if the (execution) environment does not
// support (sending and/or receiving) video.
audioOnly = true;
}
sendAnalytics(createStartAudioOnlyEvent(audioOnly));
logger.log(`Start audio only set to ${audioOnly.toString()}`);

View File

@@ -216,10 +216,9 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
function mapStateToProps(state) {
const { enableAutomaticUrlCopy } = state['features/base/config'];
const { customizationReady } = state['features/dynamic-branding'];
return {
url: customizationReady ? getCurrentConferenceUrl(state) : '',
url: getCurrentConferenceUrl(state),
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
};
}

View File

@@ -84,7 +84,13 @@ class Watermarks extends Component<Props, State> {
constructor(props: Props) {
super(props);
const showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
let showBrandWatermark;
if (interfaceConfig.filmStripOnly) {
showBrandWatermark = false;
} else {
showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
}
this.state = {
brandWatermarkLink:
@@ -231,11 +237,12 @@ function _mapStateToProps(state, ownProps) {
const {
DEFAULT_LOGO_URL,
JITSI_WATERMARK_LINK,
SHOW_JITSI_WATERMARK
SHOW_JITSI_WATERMARK,
filmStripOnly
} = interfaceConfig;
let _showJitsiWatermark = (
customizationReady && !customizationFailed
&& SHOW_JITSI_WATERMARK)
let _showJitsiWatermark = (!filmStripOnly
&& (customizationReady && !customizationFailed)
&& SHOW_JITSI_WATERMARK)
|| !isValidRoom;
let _logoUrl = logoImageUrl;
let _logoLink = logoClickUrl;

View File

@@ -514,7 +514,7 @@ export function urlObjectToString(o: Object): ?string {
// pathname
// Web's ExternalAPI roomName
const room = _fixRoom(o.roomName || o.room);
const room = o.roomName || o.room;
if (room
&& (url.pathname.endsWith('/')

View File

@@ -187,7 +187,7 @@ class CalendarList extends AbstractPage<Props> {
return (
<div className = 'meetings-list-empty'>
<div className = 'meetings-list-empty-image'>
<img src = './images/calendar.svg' />
<img src = '/images/calendar.svg' />
</div>
<div className = 'description'>
{ t('welcomepage.connectCalendarText', {

View File

@@ -152,9 +152,11 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
if ((typeof APP !== 'undefined' && !isButtonEnabled('chat'))
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if the chat button is not enabled in interfaceConfig
// We don't register anything on web if we're in filmStripOnly mode, or
// the chat button is not enabled in interfaceConfig.
// or we are in iAmRecorder mode
return;
}

View File

@@ -14,7 +14,7 @@ import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { fullScreenChanged, setToolboxAlwaysVisible, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
@@ -28,6 +28,7 @@ import Labels from './Labels';
import { default as Notice } from './Notice';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
@@ -174,13 +175,18 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const {
// XXX The character casing of the name filmStripOnly utilized by
// interfaceConfig is obsolete but legacy support is required.
filmStripOnly: filmstripOnly
} = interfaceConfig;
const {
_iAmRecorder,
_isLobbyScreenVisible,
_layoutClassName,
_showPrejoin
} = this.props;
const hideLabels = _iAmRecorder;
const hideLabels = filmstripOnly || _iAmRecorder;
return (
<div
@@ -192,18 +198,18 @@ class Conference extends AbstractConference<Props, *> {
<div id = 'videospace'>
<LargeVideo />
<KnockingParticipantList />
<Filmstrip />
<Filmstrip filmstripOnly = { filmstripOnly } />
{ hideLabels || <Labels /> }
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
<Chat />
{ filmstripOnly || _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
{ filmstripOnly || <Chat /> }
{ this.renderNotificationsContainer() }
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
{ !filmstripOnly && _showPrejoin && <Prejoin />}
</div>
);
}
@@ -250,6 +256,9 @@ class Conference extends AbstractConference<Props, *> {
dispatch(connect());
maybeShowSuboptimalExperienceNotification(dispatch, t);
interfaceConfig.filmStripOnly
&& dispatch(setToolboxAlwaysVisible(true));
}
}

View File

@@ -56,7 +56,7 @@ export function maybeOpenFeedbackDialog(conference: Object) {
const state = getState();
const { feedbackPercentage = 100 } = state['features/base/config'];
if (config.iAmRecorder) {
if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
// Intentionally fall through the if chain to prevent further action
// from being taken with regards to showing feedback.
} else if (state['features/base/dialog'].component === FeedbackDialog) {

View File

@@ -17,6 +17,8 @@ import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Toolbar from './Toolbar';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -40,6 +42,11 @@ type Props = {
*/
_columns: number,
/**
* Whether the UI/UX is filmstrip-only.
*/
_filmstripOnly: boolean,
/**
* The width of the filmstrip.
*/
@@ -135,12 +142,14 @@ class Filmstrip extends Component <Props> {
* @inheritdoc
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
if (!this.props._filmstripOnly) {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
}
}
/**
@@ -198,7 +207,7 @@ class Filmstrip extends Component <Props> {
let toolbar = null;
if (!this.props._hideToolbar) {
toolbar = this._renderToggleButton();
toolbar = this.props._filmstripOnly ? <Toolbar /> : this._renderToggleButton();
}
return (
@@ -358,20 +367,24 @@ class Filmstrip extends Component <Props> {
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const { hovered, visible } = state['features/filmstrip'];
const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly);
const reduceHeight
= state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
= !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const videosClassName = `filmstrip__videos${
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripOnly: isFilmstripOnly,
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),

View File

@@ -0,0 +1,101 @@
// @flow
import React, { Component } from 'react';
import { connect, equals } from '../../../base/redux';
import { SettingsButton } from '../../../settings';
import {
AudioMuteButton,
HangupButton,
VideoMuteButton
} from '../../../toolbox/components';
declare var interfaceConfig: Object;
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will. This has to be retrieved from the store.
const visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
/**
* The type of the React {@code Component} props of {@link Toolbar}.
*/
type Props = {
/**
* The set of buttons which should be visible in this {@code Toolbar}.
*/
_visibleButtons: Set<string>
};
/**
* Implements the conference toolbar on React/Web for filmstrip-only mode.
*
* @extends Component
*/
class Toolbar extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'filmstrip-toolbox'
id = 'new-toolbox'>
<HangupButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('hangup') } />
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<VideoMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('camera') } />
<SettingsButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('fodeviceselection') } />
</div>
);
}
_shouldShowButton: (string) => boolean;
/**
* Returns if a button name has been explicitly configured to be displayed.
*
* @param {string} buttonName - The name of the button, as expected in
* {@link intefaceConfig}.
* @private
* @returns {boolean} True if the button should be displayed, false
* otherwise.
*/
_shouldShowButton(buttonName) {
return this.props._visibleButtons.has(buttonName);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _visibleButtons: Set<string>
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
//
// NB: We compute the buttons again here because if URL parameters were used to
// override them we'd miss it.
const buttons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
return {
_visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
};
}
export default connect(_mapStateToProps)(Toolbar);

View File

@@ -56,6 +56,9 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| ((pinnedParticipant = getPinnedParticipant(state))
&& pinnedParticipant.local)))
|| (typeof interfaceConfig === 'object'
&& interfaceConfig.filmStripOnly)
|| state['features/base/config'].disable1On1Mode);
}

View File

@@ -0,0 +1,112 @@
// @flow
import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import OverlayFrame from './OverlayFrame';
/**
* The type of the React {@code Component} props of
* {@link FilmstripOnlyOverlayFrame}.
*/
type Props = {
/**
* The ID of the local participant.
*/
_localParticipantId: string,
/**
* The children components to be displayed into the overlay frame for
* filmstrip only mode.
*/
children: React$Node,
/**
* The css class name for the icon that will be displayed over the avatar.
*/
icon: string,
/**
* Indicates the css style of the overlay. If true, then lighter; darker,
* otherwise.
*/
isLightOverlay: boolean
};
/**
* Implements a React Component for the frame of the overlays in filmstrip only
* mode.
*/
class FilmstripOnlyOverlayFrame extends Component<Props> {
/**
* Renders content related to the icon.
*
* @returns {ReactElement|null}
* @private
*/
_renderIcon() {
if (!this.props.icon) {
return null;
}
const iconClass = `inlay-filmstrip-only__icon ${this.props.icon}`;
const iconBGClass = 'inlay-filmstrip-only__icon-background';
return (
<div>
<div className = { iconBGClass } />
<div className = 'inlay-filmstrip-only__icon-container'>
<span className = { iconClass } />
</div>
</div>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<OverlayFrame isLightOverlay = { this.props.isLightOverlay }>
<div className = 'inlay-filmstrip-only'>
<div className = 'inlay-filmstrip-only__content'>
{
this.props.children
}
</div>
<div className = 'inlay-filmstrip-only__avatar-container'>
<Avatar participantId = { this.props._localParticipantId } />
{
this._renderIcon()
}
</div>
</div>
</OverlayFrame>
);
}
}
/**
* Maps (parts of) the Redux state to the associated FilmstripOnlyOverlayFrame
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _localParticipantId: string
* }}
*/
function _mapStateToProps(state) {
return {
_localParticipantId: (getLocalParticipant(state) || {}).id
};
}
export default connect(_mapStateToProps)(FilmstripOnlyOverlayFrame);

View File

@@ -21,10 +21,44 @@ type Props = {
isLightOverlay?: boolean
};
/**
* The type of the React {@code Component} state of {@link OverlayFrame}.
*/
type State = {
/**
* Whether or not the application is currently displaying in filmstrip only
* mode.
*/
filmstripOnly: boolean
};
/**
* Implements a React {@link Component} for the frame of the overlays.
*/
export default class OverlayFrame extends Component<Props> {
export default class OverlayFrame extends Component<Props, State> {
/**
* Initializes a new AbstractOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props: Props) {
super(props);
this.state = {
/**
* Indicates whether the filmstrip only mode is enabled or not.
*
* @type {boolean}
*/
filmstripOnly:
typeof interfaceConfig !== 'undefined'
&& interfaceConfig.filmStripOnly
};
}
/**
* Implements React's {@link Component#render()}.
*
@@ -32,11 +66,20 @@ export default class OverlayFrame extends Component<Props> {
* @returns {ReactElement|null}
*/
render() {
let containerClass = this.props.isLightOverlay
? 'overlay__container-light' : 'overlay__container';
let contentClass = 'overlay__content';
if (this.state.filmstripOnly) {
containerClass += ' filmstrip-only';
contentClass += ' filmstrip-only';
}
return (
<div
className = { this.props.isLightOverlay ? 'overlay__container-light' : 'overlay__container' }
className = { containerClass }
id = 'overlay'>
<div className = { 'overlay__content' }>
<div className = { contentClass }>
{
this.props.children
}

View File

@@ -0,0 +1,50 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractPageReloadOverlay, { type Props, abstractMapStateToProps }
from '../AbstractPageReloadOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
/**
* Implements a React Component for page reload overlay for filmstrip only
* mode. Shown before the conference is reloaded. Shows a warning message and
* counts down towards the reload.
*/
class PageReloadFilmstripOnlyOverlay extends AbstractPageReloadOverlay<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<FilmstripOnlyOverlayFrame>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t(title) }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ t(message, { seconds: timeLeft }) }
</div>
</div>
{ this._renderButton() }
{ this._renderProgressBar() }
</FilmstripOnlyOverlayFrame>
);
}
_renderButton: () => React$Element<*> | null
_renderProgressBar: () => React$Element<*> | null
}
export default translate(
connect(abstractMapStateToProps)(PageReloadFilmstripOnlyOverlay));

View File

@@ -0,0 +1,41 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import AbstractSuspendedOverlay from './AbstractSuspendedOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
import ReloadButton from './ReloadButton';
/**
* Implements a React Component for suspended overlay for filmstrip only mode.
* Shown when suspended is detected.
*/
class SuspendedFilmstripOnlyOverlay extends AbstractSuspendedOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<FilmstripOnlyOverlayFrame isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t('suspendedoverlay.title') }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ translateToHTML(t, 'suspendedoverlay.text') }
</div>
</div>
<ReloadButton textKey = 'suspendedoverlay.rejoinKeyTitle' />
</FilmstripOnlyOverlayFrame>
);
}
}
export default translate(SuspendedFilmstripOnlyOverlay);

View File

@@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractUserMediaPermissionsOverlay, { abstractMapStateToProps }
from './AbstractUserMediaPermissionsOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
declare var interfaceConfig: Object;
/**
* Implements a React Component for overlay with guidance how to proceed with
* gUM prompt. This component will be displayed only for filmstrip only mode.
*/
class UserMediaPermissionsFilmstripOnlyOverlay
extends AbstractUserMediaPermissionsOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const textKey = `userMedia.${this.props.browser}GrantPermissions`;
return (
<FilmstripOnlyOverlayFrame
icon = 'icon-mic-camera-combined'
isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
}
</div>
<div className = 'inlay-filmstrip-only__text'>
{
translateToHTML(t, textKey)
}
</div>
</div>
</FilmstripOnlyOverlayFrame>
);
}
}
export default translate(
connect(abstractMapStateToProps)(UserMediaPermissionsFilmstripOnlyOverlay));

View File

@@ -32,7 +32,8 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
<span className = 'inlay__icon icon-camera' />
<h3 className = 'inlay__title'>
{
t('startupoverlay.genericTitle')
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
}
</h3>
<span className = 'inlay__text'>

View File

@@ -1,7 +1,19 @@
// @flow
export { default as FilmstripOnlyOverlayFrame } from './FilmstripOnlyOverlayFrame';
export { default as OverlayFrame } from './OverlayFrame';
export {
default as PageReloadFilmstripOnlyOverlay
} from './PageReloadFilmstripOnlyOverlay';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export {
default as SuspendedFilmstripOnlyOverlay
} from './SuspendedFilmstripOnlyOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';
export {
default as UserMediaPermissionsFilmstripOnlyOverlay
} from './UserMediaPermissionsFilmstripOnlyOverlay';
export {
default as UserMediaPermissionsOverlay
} from './UserMediaPermissionsOverlay';

View File

@@ -1,8 +1,11 @@
// @flow
import {
PageReloadFilmstripOnlyOverlay,
PageReloadOverlay,
SuspendedFilmstripOnlyOverlay,
SuspendedOverlay,
UserMediaPermissionsFilmstripOnlyOverlay,
UserMediaPermissionsOverlay
} from './components/web';
@@ -14,9 +17,22 @@ declare var interfaceConfig: Object;
* @returns {Array<Object>}
*/
export function getOverlays(): Array<Object> {
return [
PageReloadOverlay,
const overlays = [
SuspendedOverlay,
UserMediaPermissionsOverlay
];
const filmstripOnly
= typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly;
if (filmstripOnly) {
overlays.push(
PageReloadFilmstripOnlyOverlay,
SuspendedFilmstripOnlyOverlay,
UserMediaPermissionsFilmstripOnlyOverlay);
} else {
overlays.push(PageReloadOverlay);
}
return overlays;
}

View File

@@ -1,102 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../base/dialog';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from './AbstractMuteRemoteParticipantDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryoneDialog}.
*/
export type Props = AbstractProps & {
content: string,
exclude: Array<string>,
title: string
};
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteRemoteParticipantDialog
*/
export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state).id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}

View File

@@ -1,48 +0,0 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconMuteEveryone } from '../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryoneDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to keep unmuted.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes all the other participants.
*/
export default class AbstractMuteEveryoneElseButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElse';
icon = IconMuteEveryone;
label = 'videothumbnail.domuteOthers';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}

View File

@@ -1,67 +0,0 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractMuteEveryoneDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the base/dialog feature.
*/
_dialogStyles: StyleType
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteEveryoneDialog
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<ConfirmDialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
</Text>
</ConfirmDialog>
);
}
_onSubmit: () => boolean;
}
/**
* 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 {{
* _dialogStyles: StyleType
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
return {
...abstractMapStateToProps(state, ownProps),
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
};
}
export default translate(connect(_mapStateToProps)(MuteEveryoneDialog));

View File

@@ -1,20 +0,0 @@
// @flow
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElseButton from '../AbstractMuteEveryoneElseButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteEveryoneElseButton));

View File

@@ -16,7 +16,6 @@ import { hideRemoteVideoMenu } from '../../actions';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import PinButton from './PinButton';
import styles from './styles';
@@ -105,7 +104,6 @@ class RemoteVideoMenu extends PureComponent<Props> {
<GrantModeratorButton { ...buttonProps } />
<PinButton { ...buttonProps } />
<PrivateMessageButton { ...buttonProps } />
<MuteEveryoneElseButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -1,7 +1,12 @@
// @flow
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@@ -5,15 +5,53 @@ import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from '../AbstractMuteEveryoneDialog';
import { muteAllParticipants } from '../../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from '../AbstractMuteRemoteParticipantDialog';
declare var APP: Object;
/**
* The type of the React {@code Component} props of
* {@link MuteEveryoneDialog}.
*/
type Props = AbstractProps & {
/**
* The IDs of the remote participants to exclude from being muted.
*/
exclude: Array<string>
};
/**
* Translations needed for dialog rendering.
*/
type Translations = {
/**
* Content text.
*/
content: string,
/**
* Title text.
*/
title: string
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
* from the user before muting a remote participant.
*
* @extends AbstractMuteEveryoneDialog
* @extends Component
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
@@ -21,20 +59,64 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { content, title } = this._getTranslations();
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { this.props.title }
titleString = { title }
width = 'small'>
<div>
{ this.props.content }
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
/**
* Method to get translations depending on whether we have an exclusive
* mute or not.
*
* @returns {Translations}
* @private
*/
_getTranslations(): Translations {
const { exclude, t } = this.props;
const { conference } = APP;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => conference.isLocalId(id)
? t('dialog.muteEveryoneSelf')
: conference.getParticipantDisplayName(id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}
}
export default translate(connect(abstractMapStateToProps)(MuteEveryoneDialog));
export default translate(connect()(MuteEveryoneDialog));

View File

@@ -2,13 +2,17 @@
import React from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMuteEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElseButton, {
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteEveryoneElseButton';
} from '../AbstractMuteButton';
import MuteEveryoneDialog from './MuteEveryoneDialog';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
@@ -16,9 +20,9 @@ import RemoteVideoMenuButton from './RemoteVideoMenuButton';
* every participant in the conference except the one with the given
* participantID
*/
class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
class MuteEveryoneElseButton extends AbstractMuteButton {
/**
* Instantiates a new {@code Component}.
* Instantiates a new {@code MuteEveryoneElseButton}.
*
* @inheritdoc
*/
@@ -49,6 +53,19 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
}
_handleClick: () => void;
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}
export default translate(connect()(MuteEveryoneElseButton));
export default translate(connect(_mapStateToProps)(MuteEveryoneElseButton));

View File

@@ -9,11 +9,10 @@ import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import {
GrantModeratorButton,
MuteButton,
MuteEveryoneElseButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,

View File

@@ -1,15 +1,26 @@
// @flow
export { default as GrantModeratorButton } from './GrantModeratorButton';
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export {
REMOTE_CONTROL_MENU_STATES,
default as RemoteControlButton
} from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
export {
default as RemoteVideoMenuTriggerButton
} from './RemoteVideoMenuTriggerButton';
export { default as VolumeSlider } from './VolumeSlider';

View File

@@ -104,7 +104,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
name = 'lockKey'
onChange = { this._onPasswordChanged }
shouldFitContainer = { true }
type = 'password'
type = 'text'
value = { this.state.password } />
</div>
);

View File

@@ -5,14 +5,22 @@ import { translate } from '../../../base/i18n';
import { IconSettings } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openDeviceSelectionPopup } from '../../../device-selection';
import { openSettingsDialog } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link SettingsButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether we are in filmstrip only mode or not.
*/
_filmstripOnly: boolean,
/**
* The default tab at which the settings dialog will be opened.
*/
@@ -41,12 +49,36 @@ class SettingsButton extends AbstractButton<Props, *> {
*/
_handleClick() {
const {
_filmstripOnly,
defaultTab = SETTINGS_TABS.DEVICES,
dispatch } = this.props;
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab));
if (_filmstripOnly) {
dispatch(openDeviceSelectionPopup());
} else {
dispatch(openSettingsDialog(defaultTab));
}
}
}
export default translate(connect()(SettingsButton));
/**
* Maps (parts of) the redux state to the associated props for the
* {@code SettingsButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _filmstripOnly: boolean
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
return {
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
};
}
export default translate(connect(_mapStateToProps)(SettingsButton));

View File

@@ -141,7 +141,14 @@ export default class JitsiStreamPresenterEffect {
timeMs: 1000 / this._frameRate
});
return this._canvas.captureStream(this._frameRate);
const capturedStream = this._canvas.captureStream(this._frameRate);
// Put emphasis on the text details for the presenter's stream
// See https://www.w3.org/TR/mst-content-hint/
// $FlowExpectedError
capturedStream.getVideoTracks()[0].contentHint = 'text';
return capturedStream;
}
/**

View File

@@ -25,6 +25,10 @@ export * from './actions.native';
*/
export function dockToolbox(dock: boolean): Function {
return (dispatch: Dispatch<any>, getState: Function) => {
if (interfaceConfig.filmStripOnly) {
return;
}
const { timeoutMS, visible } = getState()['features/toolbox'];
if (dock) {

View File

@@ -19,7 +19,6 @@ import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import { VideoShareButton } from '../../../youtube-player/components';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioOnlyButton from './AudioOnlyButton';
import MoreOptionsButton from './MoreOptionsButton';
@@ -144,7 +143,6 @@ class OverflowMenu extends PureComponent<Props, State> {
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<MuteEveryoneButton { ...buttonProps } />
<HelpButton { ...buttonProps } />
</Collapsible>
</BottomSheet>

View File

@@ -1,13 +1,13 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { IconMuteEveryone } from '../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryoneDialog } from '../../remote-video-menu/components';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMuteEveryone } from '../../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { MuteEveryoneDialog } from '../../../remote-video-menu';
type Props = AbstractButtonProps & {

View File

@@ -79,9 +79,9 @@ import { isToolboxVisible } from '../../functions';
import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioSettingsButton from './AudioSettingsButton';
import MuteEveryoneButton from './MuteEveryoneButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';

View File

@@ -99,6 +99,9 @@ export function shouldDisplayTileView(state: Object = {}) {
// Editing etherpad
state['features/etherpad']?.editing
// We're in filmstrip-only mode
|| (typeof interfaceConfig === 'object' && interfaceConfig?.filmStripOnly)
// We pinned a participant
|| getPinnedParticipant(state)

View File

@@ -1,11 +1,13 @@
// @flow
import InlineMessage from '@atlaskit/inline-message';
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { setAudioOnly } from '../../base/audio-only';
import { translate } from '../../base/i18n';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import { connect } from '../../base/redux';
import { setPreferredVideoQuality } from '../actions';
import { VIDEO_QUALITY_LEVELS } from '../constants';
@@ -55,6 +57,12 @@ type Props = {
*/
_sendrecvVideoQuality: Number,
/**
* Whether or not displaying video is supported in the current
* environment. If false, the slider will be disabled.
*/
_videoSupported: Boolean,
/**
* Invoked to request toggling of audio only mode.
*/
@@ -133,14 +141,25 @@ class VideoQualitySlider extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { _videoSupported, t } = this.props;
const activeSliderOption = this._mapCurrentQualityToSliderValue();
let classNames = 'video-quality-dialog';
let warning = null;
if (!_videoSupported) {
classNames += ' video-not-supported';
warning = this._renderAudioOnlyLockedMessage();
}
return (
<div className = { 'video-quality-dialog' }>
<div className = { classNames }>
<h3 className = 'video-quality-dialog-title'>
{ t('videoStatus.callQuality') }
</h3>
<div className = { warning ? '' : 'hide-warning' }>
{ warning }
</div>
<div className = 'video-quality-dialog-contents'>
<div className = 'video-quality-dialog-slider-container'>
{ /* FIXME: onChange and onMouseUp are both used for
@@ -149,6 +168,7 @@ class VideoQualitySlider extends Component<Props> {
*/ }
<input
className = 'video-quality-dialog-slider'
disabled = { !_videoSupported }
max = { this._sliderOptions.length - 1 }
min = '0'
onChange = { this._onSliderChange }
@@ -167,6 +187,24 @@ class VideoQualitySlider extends Component<Props> {
);
}
/**
* Creates a React Element for notifying that the browser is in audio only
* and cannot be changed.
*
* @private
* @returns {ReactElement}
*/
_renderAudioOnlyLockedMessage() {
const { t } = this.props;
return (
<InlineMessage
title = { t('videoStatus.onlyAudioAvailable') }>
{ t('videoStatus.onlyAudioSupported') }
</InlineMessage>
);
}
/**
* Creates React Elements to display mock tick marks with associated labels.
*
@@ -355,7 +393,11 @@ class VideoQualitySlider extends Component<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
* @returns {{
* _audioOnly: boolean,
* _p2p: boolean,
* _sendrecvVideoQuality: number
* }}
*/
function _mapStateToProps(state) {
const { enabled: audioOnly } = state['features/base/audio-only'];
@@ -365,7 +407,8 @@ function _mapStateToProps(state) {
return {
_audioOnly: audioOnly,
_p2p: p2p,
_sendrecvVideoQuality: preferredVideoQuality
_sendrecvVideoQuality: preferredVideoQuality,
_videoSupported: JitsiMeetJS.mediaDevices.supportsVideo()
};
}

View File

@@ -200,10 +200,10 @@ class WelcomePage extends AbstractWelcomePage {
<div className = 'header-image' />
<div className = 'header-container'>
<h1 className = 'header-text-title'>
{ t('welcomepage.headerTitle') }
{ t('welcomepage.jitsiMeet') }
</h1>
<span className = 'header-text-subtitle'>
{ t('welcomepage.headerSubtitle')}
{ t('welcomepage.secureMeetings')}
</span>
<div id = 'enter_room'>
<div className = 'enter-room-input-container'>

View File

@@ -24,7 +24,7 @@ end
-- -> true, room_name, subdomain
-- -> true, room_name, nil (if no subdomain is used for the room)
local function is_moderated(room_jid)
if moderated_subdomains:empty() and moderated_rooms:empty() then
if #moderated_subdomains == 0 and #moderated_rooms == 0 then
return false;
end

View File

@@ -39,7 +39,7 @@ function filter_stanza(stanza)
end
function filter_session(session)
-- module:log("warn", "Session filters applied");
module:log("warn", "Session filters applied");
filters.add_filter(session, "stanzas/out", filter_stanza);
end

View File

@@ -102,7 +102,7 @@ const config = {
// dependencies including lib-jitsi-meet.
loader: 'expose-loader?$!expose-loader?jQuery',
test: /[/\\]node_modules[/\\]jquery[/\\].*\.js$/
test: /\/node_modules\/jquery\/.*\.js$/
}, {
// Allow CSS to be imported into JavaScript.