Compare commits

...

40 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
fe9fd1c0bc squash! 2022-04-28 16:51:16 +02:00
Saúl Ibarra Corretgé
e4704ae032 fix(config) add a link to config options docs 2022-04-28 16:46:19 +02:00
Дамян Минков
132b44a8b6 feat: Drops nginx dependency for turnserver config.
We used to multiplex the ports in nginx, but we dropped that at some point, so now coturn is on its own listening and nginx dependency is no longer needed. Our turnserver config can be used with nginx | apache2.
2022-04-28 06:12:13 -05:00
Mihaela Dumitru
72111114b6 fix(breakout-rooms) reset rooms when conference is left or failed (#11447) 2022-04-28 11:34:23 +03:00
Hristo Terezov
550c730ed4 fix(tile-view):Recalculate on window height change 2022-04-28 08:35:39 +02:00
Nicolas
2ac2138982 fix(lang) update Russian translation 2022-04-27 14:01:55 +02:00
Robert Pintilii
a84d7c17fa fix(avatar) Center phone icon in participants pane avatars (#11440) 2022-04-27 10:53:58 +03:00
bgrozev
586ad30ed4 feat(config) add testing.setScreenSharingResolutionConstraints to config.js 2022-04-27 09:25:48 +02:00
Jaya Allamsetty
f1c5f314e5 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1422.0.0+cf22aa36...v1425.0.0+6b629a19
2022-04-26 18:19:13 -04:00
bgrozev
64d7305598 chore(deps) lib-jitsi-meet@latest (#11437)
https://github.com/jitsi/lib-jitsi-meet/compare/v1422.0.0+cf22aa36...v1423.0.0+6870779a
2022-04-26 15:52:30 -05:00
pangrr
c03d86e0e3 fix: disabled connectStatusIndicatorIcon cause video not displayed (#11377)
* add and remove JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED listener in middlewares

* add/remove listeners in components that use track streaming status

* remove track streaming status handler from ConnectionIndicatorIcon and ConnectionIndicatorContent

* check video track change before hanlding track streaming status
2022-04-26 15:33:50 -05:00
Дамян Минков
0ae2693116 fix: Fixes let's encrypt for latest ubuntu versions. (#11434)
* fix: Fixes let's encrypt for latest ubuntu versions.

* squash: Simplifies the logic.
2022-04-26 13:48:25 -05:00
Horatiu Muresan
20f6ba1736 fix(premeeting) Detach premeeting toolbar buttons visibility
- if hiddenPremeetingButtons is undefined, toolbarButtons overwrite decides what buttons to show
- if hiddenPremeetingButtons is empty array, all buttons are show on premeeting screen regardless of toolbarButtons
- if hiddenPremeetingButtons hides some buttons, only those buttons will be hidden regardless of toolbarButtons overwrite
2022-04-26 15:33:09 +03:00
Horatiu Muresan
eb64ea6aba fix(always-on-top) Fix audio mute button disabled status 2022-04-26 15:32:18 +03:00
Calin Chitu
3e004811e0 feat(lobby/native) LobbyScreen and LobbyChatScreen 2022-04-26 14:55:15 +03:00
Дамян Минков
037b9202a6 fix: Fixes Let's Encrypt script. (#11430)
* fix: Fixes Let's Encrypt script.

It fails when certbot is not installed and exits with an error without installing anything.

* squash: Fixes certbot command after install.
2022-04-26 06:32:08 -05:00
dependabot[bot]
8b8a42e0d1 chore(deps): bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-26 12:29:56 +02:00
Jaya Allamsetty
4315e19780 fix(device-selection) Enable device selection on mobile Safari. (#11427)
* fix(device-selection) Enable device selection on mobile Safari.
With https://bugs.webkit.org/show_bug.cgi?id=179363 being fixed, we should now be able to switch between devices in call. Also, before the webkit fix, we were able to continue to use the old track when a new track was created for preview in device settings before joining the call. This doesn't work anymore after the fix. Therefore, always replace the track in redux even if the selected device hasn't changed. Depends on https://github.com/jitsi/lib-jitsi-meet/pull/1993.

* chore(deps): update lib-jitsi-meet@latest.
2022-04-25 15:01:10 -04:00
Saúl Ibarra Corretgé
4d2bd932a7 fix(lastN) fix last N getting stuck on 1
If last N goes down to 1 it will be stuck there since it's > 0 and will
be our `lastNSelected`. When limits are applied we'll take the minimum,
so it will end up being 1.

Once can end up in last N being 1 by several means, the more obvious one
by entering Picture-in-Picture mode on mobile.

Fix it by not using the previous last N value for the current
calculation, at all.

Fixes: https://github.com/jitsi/jitsi-meet/issues/10257
Closes: https://github.com/jitsi/jitsi-meet/pull/10491
2022-04-25 17:44:54 +02:00
SiderealArt
6e1f56fad1 fix(lang) update Traditional Chinese translation 2022-04-25 14:18:13 +02:00
Ali Alhaidary
7d2f62a614 fix(lang) update Arabic translation 2022-04-25 11:52:39 +02:00
Robert Pintilii
97b958e9ea Fix import and naming 2022-04-21 09:01:51 -05:00
Robert Pintilii
b00fc92ee6 Native fix 2022-04-21 09:01:51 -05:00
Robert Pintilii
f9d1003527 Fixes 2022-04-21 09:01:51 -05:00
robertpin
0abefa87aa ref(stage-filmstrip) Refactor as new layout
Fixes screensharing selection issues. Now when there’s a screen share we just use the old VERTICAL_FILMSTRIP_VIEW layout
Add THUMBAIL_TYPE to determine how to display thumbnails
2022-04-21 09:01:51 -05:00
Gabriel Borlea
dde8c586da feat(external-api): add toggle subtitles command (#10070)
* feat(external-api): add toggle subtitles command

* feat(external-api): add set subtitles command
2022-04-20 11:43:18 +03:00
Seda Çağlar
aa944e76ad fix(lang) update Turkish translation 2022-04-19 17:33:24 -05:00
Hristo Terezov
4153097cc9 fix(prejoin):Disable device selection on iosSafari
It seems that showing the device selection dialog on ios Safari will
leads to not working audio. This is temporary fix until we find out
better solution.
2022-04-19 15:32:45 -05:00
Hristo Terezov
2a5be074d0 fix(video-layout): functions imports. 2022-04-19 15:15:44 -05:00
Saúl Ibarra Corretgé
2e0ae75774 fix(debian) make sure we install the latest version of luajwtjitsi
Also on update, since we might start depending on a more recent version.
2022-04-19 14:59:51 +02:00
Saúl Ibarra Corretgé
a8017149a0 fix(debian) update Prosody related dependencies 2022-04-19 14:59:51 +02:00
Jaya Allamsetty
e99fc4394d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1418.0.0+da6d04c2...v1419.0.0+607646a1
2022-04-19 08:30:19 -04:00
philip-cc
46dd88c91b feat(jwt) refactor token authentication plugin to use new luajwtjitsi version 2022-04-19 13:06:20 +02:00
Horatiu Muresan
dbc8f21b01 fix(hangup-button) Add to notify toolbar button clicked 2022-04-19 13:26:07 +03:00
William Liang
5ebe308953 fix(multi-stream) add screenshare display name to i18n 2022-04-18 17:37:22 -05:00
Jaya Allamsetty
7420113079 fix(multi-stream) Do not show join notifications for SS tiles. 2022-04-18 13:19:17 -04:00
Hristo Terezov
221ecac12d feat(tile-vew): Calculate maxColumns dynamically 2022-04-18 11:19:58 -05:00
Jaya Allamsetty
744607a5cc fix(stageFilmstrip) Disable stage filmstrip by default. 2022-04-18 09:57:25 -05:00
Horatiu Muresan
8f641b7bb1 fix(start-silent) Disable AOT mic and unmute notif when start silent 2022-04-18 17:17:51 +03:00
chipechop
045bd44407 fix(lang) update Italian translation 2022-04-18 10:55:05 +02:00
79 changed files with 1791 additions and 986 deletions

View File

@@ -1,6 +1,11 @@
/* eslint-disable no-unused-vars, no-var */
/*
* NOTE: If you add a new option please remember to document it here:
* https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-configuration
*/
var config = {
// Connection
//
@@ -69,6 +74,11 @@ var config = {
// or disabled for the screenshare.
// capScreenshareBitrate: 1 // 0 to disable - deprecated.
// Whether to use fake constraints (height: 99999, width: 99999) when calling getDisplayMedia on
// Chromium based browsers. This is intended as a workaround for
// https://bugs.chromium.org/p/chromium/issues/detail?id=1056311
// setScreenSharingResolutionConstraints: true
// Enable callstats only for a percentage of users.
// This takes a value between 0 and 100 which determines the probability for
// the callstats to be enabled.

View File

@@ -1,175 +1,177 @@
.vertical-filmstrip span:not(.tile-view) .filmstrip {
&.hide-videos {
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
.vertical-filmstrip, .stage-filmstrip {
span:not(.tile-view) .filmstrip {
&.hide-videos {
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
}
}
}
}
/*
* Firefox sets flex items to min-height: auto and min-width: auto,
* preventing flex children from shrinking like they do on other browsers.
* Setting min-height and min-width 0 is a workaround for the issue so
* Firefox behaves like other browsers.
* https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
*/
@mixin minHWAutoFix() {
min-height: 0;
min-width: 0;
}
/*
* Firefox sets flex items to min-height: auto and min-width: auto,
* preventing flex children from shrinking like they do on other browsers.
* Setting min-height and min-width 0 is a workaround for the issue so
* Firefox behaves like other browsers.
* https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
*/
@mixin minHWAutoFix() {
min-height: 0;
min-width: 0;
}
@extend %align-right;
align-items: flex-end;
bottom: 0;
box-sizing: border-box;
display: flex;
flex-direction: column-reverse;
height: 100%;
width: 100%;
padding: 0;
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
* a library called popper which will position its elements fixed if
* any parent is also fixed.
*/
position: fixed;
top: 0;
right: 0;
z-index: $filmstripVideosZ;
&.no-vertical-padding {
padding: 0;
}
/**
* Hide videos by making them slight to the right.
*/
.filmstrip__videos {
@extend %align-right;
align-items: flex-end;
bottom: 0;
box-sizing: border-box;
display: flex;
flex-direction: column-reverse;
height: 100%;
width: 100%;
padding: 0;
position:relative;
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
* a library called popper which will position its elements fixed if
* any parent is also fixed.
*/
position: fixed;
top: 0;
right: 0;
width: auto;
z-index: $filmstripVideosZ;
&.no-vertical-padding {
padding: 0;
}
/**
* An id selector is used to match id specificity with existing
* filmstrip styles.
* Hide videos by making them slight to the right.
*/
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: 0;
border-left: 0;
.filmstrip__videos {
@extend %align-right;
bottom: 0;
padding: 0;
position:relative;
right: 0;
width: auto;
/**
* An id selector is used to match id specificity with existing
* filmstrip styles.
*/
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: 0;
border-left: 0;
width: 100%;
height: 100%;
justify-content: center;
}
}
/**
* Re-styles the local Video to better fit vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
height: 100%;
justify-content: center;
}
}
/**
* Re-styles the local Video to better fit vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalVideoThumbnail {
width: calc(100% - 15px);
#filmstripLocalVideoThumbnail {
width: calc(100% - 15px);
.videocontainer {
height: 0px;
width: 100%;
.videocontainer {
height: 0px;
width: 100%;
}
}
}
}
#filmstripLocalScreenShare {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalScreenShare {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalScreenShareThumbnail {
width: calc(100% - 15px);
#filmstripLocalScreenShareThumbnail {
width: calc(100% - 15px);
.videocontainer {
height: 0px;
width: 100%;
.videocontainer {
height: 0px;
width: 100%;
}
}
}
}
/**
* Remove unnecssary padding that is normally used to prevent horizontal
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripLocalScreenShare,
.remote-videos {
padding: 0;
}
#remoteVideos {
@include minHWAutoFix();
flex-direction: column;
flex-grow: 1;
}
.resizable-filmstrip #remoteVideos .videocontainer {
border-left: 0;
margin: 0;
}
&.reduce-height {
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
}
.filmstrip__videos.vertical-view-grid#remoteVideos {
align-items: 'center';
border: 0px;
padding-right: 7px;
&.has-scroll {
padding-right: 0px;
/**
* Remove unnecssary padding that is normally used to prevent horizontal
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripLocalScreenShare,
.remote-videos {
padding: 0;
}
.remote-videos > div {
left: 0px; // fixes an issue on FF - the div is aligned to the right by default for some reason
#remoteVideos {
@include minHWAutoFix();
flex-direction: column;
flex-grow: 1;
}
.videocontainer {
.resizable-filmstrip #remoteVideos .videocontainer {
border-left: 0;
margin: 0;
}
&.reduce-height {
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
}
.filmstrip__videos.vertical-view-grid#remoteVideos {
align-items: 'center';
border: 0px;
margin: 2px;
}
}
padding-right: 7px;
.remote-videos {
display: flex;
overscroll-behavior: contain;
&.has-scroll {
padding-right: 0px;
}
&.height-transition {
transition: height .3s ease-in;
.remote-videos > div {
left: 0px; // fixes an issue on FF - the div is aligned to the right by default for some reason
}
.videocontainer {
border: 0px;
margin: 2px;
}
}
& > div {
position: absolute;
transition: opacity 1s;
}
.remote-videos {
display: flex;
overscroll-behavior: contain;
&.is-not-overflowing > div {
bottom: 0px;
&.height-transition {
transition: height .3s ease-in;
}
& > div {
position: absolute;
transition: opacity 1s;
}
&.is-not-overflowing > div {
bottom: 0px;
}
}
}
}

View File

@@ -3,14 +3,17 @@
* clashing with the filmstrip.
*/
.vertical-filmstrip #etherpad,
.vertical-filmstrip #sharedvideo {
.stage-filmstrip #etherpad,
.vertical-filmstrip #sharedvideo,
.stage-filmstrip #sharedvideo {
text-align: left;
}
/**
* Overrides for small videos in vertical filmstrip mode.
*/
.vertical-filmstrip .filmstrip__videos .videocontainer {
.vertical-filmstrip .filmstrip__videos .videocontainer,
.stage-filmstrip .filmstrip__videos .videocontainer {
.self-view-mobile-portrait video {
object-fit: contain;
}
@@ -27,7 +30,8 @@
* The class opening is for when the filmstrip is transitioning from hidden
* to visible.
*/
.vertical-filmstrip .large-video-labels {
.vertical-filmstrip .large-video-labels,
.stage-filmstrip .large-video-labels {
&.with-filmstrip {
right: 150px;
}
@@ -47,6 +51,7 @@
* Overrides for self view when in portrait mode on mobile.
* This is done in order to keep the aspect ratio.
*/
.vertical-filmstrip .self-view-mobile-portrait #localVideo_container {
.vertical-filmstrip .self-view-mobile-portrait #localVideo_container,
.stage-filmstrip .self-view-mobile-portrait #localVideo_container {
object-fit: contain;
}

4
debian/control vendored
View File

@@ -47,12 +47,12 @@ Description: Prosody configuration for Jitsi Meet
Package: jitsi-meet-tokens
Architecture: all
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl1.0-dev | libssl-dev, luarocks, jitsi-meet-prosody, git
Depends: ${misc:Depends}, prosody-trunk | prosody-0.11 | prosody-0.12 | prosody (>= 0.11.2), libssl-dev, luarocks, jitsi-meet-prosody, git, lua-basexx
Description: Prosody token authentication plugin for Jitsi Meet
Package: jitsi-meet-turnserver
Architecture: all
Breaks: apache2
Pre-Depends: jitsi-meet-web-config
Depends: ${misc:Depends}, nginx (>= 1.13.10) | nginx-full (>= 1.13.10) | nginx-extras (>= 1.13.10), jitsi-meet-prosody, coturn, dnsutils
Depends: ${misc:Depends}, jitsi-meet-prosody, coturn, dnsutils
Description: Configures coturn to be used with Jitsi Meet

View File

@@ -48,6 +48,11 @@ case "$1" in
db_stop
if [ -f "$PROSODY_HOST_CONFIG" ] ; then
# Install luajwt (also on update, to make sure we get the latest version).
if ! luarocks install luajwtjitsi 3.0-0; then
echo "Failed to install luajwtjitsi - try installing it manually"
fi
# search for the token auth, if this is not enabled this is the
# first time we install tokens package and needs a config change
if ! egrep -q '^\s*authentication\s*=\s*"token"' "$PROSODY_HOST_CONFIG"; then
@@ -60,16 +65,6 @@ case "$1" in
sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
sed -i '/^\s*--\s*"token_verification"/ s/--\s*//' $PROSODY_HOST_CONFIG
# Install luajwt
if ! luarocks install luajwtjitsi 2.0-0; then
echo "Failed to install luajwtjitsi - try installing it manually"
fi
# Install basexx
if ! luarocks install basexx; then
echo "Failed to install basexx - try installing it manually"
fi
PR10_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'prosody-0.10' 2>/dev/null | awk '{print $3}' || true)"
PRTRUNK_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'prosody-trunk' 2>/dev/null | awk '{print $3}' || true)"
PR_VER_INSTALLED=$(dpkg-query -f='${Version}\n' --show prosody 2>/dev/null || true)

View File

@@ -33,7 +33,6 @@ case "$1" in
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
TURN_CONFIG="/etc/turnserver.conf"
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
# if there was a turn config backup it so we can configure
@@ -51,19 +50,6 @@ case "$1" in
fi
fi
# this detect only old installations with no nginx
db_get jitsi-meet/jvb-serve || true
if [ ! -f $NGINX_CONFIG -o "$RET" = "true" ] ; then
# nothing to do
echo "------------------------------------------------"
echo ""
echo "turnserver not configured"
echo ""
echo "------------------------------------------------"
db_stop
exit 0
fi
if [[ -f $TURN_CONFIG ]] ; then
echo "------------------------------------------------"
echo ""
@@ -117,7 +103,7 @@ denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
# SSL for nginx
# SSL settings
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"

View File

@@ -23,26 +23,12 @@ set -e
case "$1" in
remove)
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload || true
fi
if [ -x "/etc/init.d/apache2" ]; then
invoke-rc.d apache2 reload || true
fi
;;
purge)
rm -rf /etc/turnserver.conf
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload || true
fi
if [ -x "/etc/init.d/apache2" ]; then
invoke-rc.d apache2 reload || true
fi
# Clear the debconf variable
db_purge
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)

View File

@@ -31,6 +31,7 @@
},
"audioDevices": {
"bluetooth": "بلوتوث",
"car": "مسجل السيارة",
"headphones": "سماعات رأس",
"none": "لا يوجد أي أجهزة صوت",
"phone": "هاتف",
@@ -919,6 +920,7 @@
"incomingMessage": "رسالة واردة",
"language": "اللغة",
"loggedIn": "الدخول باسم {{name}}",
"maxStageParticipants": "الحد الأقصى لعدد المشاركين الذين يمكن تثبيتهم في المرحلة الرئيسية",
"microphones": "المجهار (المايكروفون)",
"moderator": "رئيس الجلسة",
"more": "المزيد",

View File

@@ -31,6 +31,7 @@
},
"audioDevices": {
"bluetooth": "Bluetooth",
"car": "Vivavoce Auto",
"headphones": "Cuffie",
"none": "Nessun dispositivo audio esistente",
"phone": "Telefono",
@@ -39,6 +40,25 @@
"audioOnly": {
"audioOnly": "Utilizzo di minore banda"
},
"breakoutRooms": {
"actions": {
"add": "Crea sottogruppo",
"autoAssign": "Assegna automaticamente a sottogruppi",
"close": "Chiudi",
"join": "Entra",
"leaveBreakoutRoom": "Esci",
"more": "Mostra di più",
"remove": "Elimina",
"sendToBreakoutRoom": "Invia partecipante a:"
},
"defaultName": "Sottogruppo {{index}}",
"mainRoom": "Riunione principale",
"notifications": {
"joined": "Entrato nel sottogruppo \"{{name}}\"",
"joinedMainRoom": "Entrato nella riunione principale",
"joinedTitle": "Sottogruppo"
}
},
"calendarSync": {
"addMeetingURL": "Aggiungi un collegamento alla riunione",
"confirmAddLink": "Vuoi aggiungere un collegamento Jitsi a questo evento?",
@@ -61,10 +81,11 @@
"enter": "Entra nella conversazione",
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
"lobbyChatMessageTo": "Messaggio a {{recipient}} in sala d'attesa",
"message": "Messaggio",
"messageAccessibleTitle": "{{user}} dice:",
"messageAccessibleTitleMe": "io dico:",
"messageTo": "Messaggio privato per {{recipient}}",
"messageTo": "Messaggio privato a {{recipient}}",
"messagebox": "Digitare un messaggio",
"nickname": {
"popover": "Scegli un nickname",
@@ -72,10 +93,10 @@
"titleWithPolls": "Inserire un nickname per utilizzare la conversazione"
},
"noMessagesMessage": "Non ci sono ancora messaggi nella riunione. Comincia una conversazione, qui!",
"privateNotice": "Messaggio privato per {{recipient}}",
"privateNotice": "Messaggio privato a {{recipient}}",
"smileysPanel": "Pannello emoji",
"tabs": {
"chat": "Chat",
"chat": "Conversazione",
"polls": "Sondaggi"
},
"title": "Conversazione",
@@ -158,7 +179,8 @@
"joinInApp": "Entra in riunione usando l'app",
"launchWebButton": "Avvia sul web",
"title": "Sto avviando la riunione su {{app}}...",
"tryAgainButton": "Prova di nuovo sul desktop"
"tryAgainButton": "Prova di nuovo sul desktop",
"unsupportedBrowser": "Sembra tu stia usando un browser che non supportiamo."
},
"defaultLink": "es. {{url}}",
"defaultNickname": "es. Anna Rossi",
@@ -186,12 +208,14 @@
"Share": "Condividi",
"Submit": "Invia",
"WaitForHostMsg": "La riunione non è ancora cominciata. Se sei l'organizzatore, per favore autenticati. Altrimenti, aspetta l'arrivo dell'organizzatore.",
"WaitingForHost": "In attesa dell'organizzatore...",
"WaitingForHostTitle": "In attesa dell'organizzatore...",
"Yes": "Sì",
"accessibilityLabel": {
"liveStreaming": "Diretta streaming"
},
"add": "Aggiungi",
"addMeetingNote": "Aggiungi una a questa riunione",
"addOptionalNote": "Aggiungi una nota (facoltativo):",
"allow": "Consenti",
"alreadySharedVideoMsg": "Un altro utente sta condividendo un video. Questa riunione permette di condividere un solo video alla volta.",
"alreadySharedVideoTitle": "È permesso un solo video alla volta",
@@ -243,6 +267,8 @@
"kickParticipantDialog": "Sei sicuro di voler escludere questo partecipante?",
"kickParticipantTitle": "Escludi questo partecipante?",
"kickTitle": "Escluso dalla riunione",
"linkMeeting": "Link meeting",
"linkMeetingTitle": "Link meeting to Salesforce",
"liveStreaming": "Diretta",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossibile durante la registrazione.",
"liveStreamingDisabledTooltip": "Trasmissioni in diretta disabilitate.",
@@ -262,9 +288,9 @@
"micPermissionDeniedError": "Non hai concesso il permesso di usare il microfono. Puoi comunque partecipare alla riunione ma gli altri non potranno sentirti. Usa il bottone a forma di telecamera nella barra degli indirizzi per cambiare impostazioni.",
"micTimeoutError": "Impossibile avviare la fonte audio. Tempo di attesa scaduto.",
"micUnknownError": "Impossibile usare il microfono per un motivo sconosciuto.",
"moderationAudioLabel": "Permetti ai partecipenti di accendere il microfono",
"moderationVideoLabel": "Permetti ai partecipanti di arrivare la videocamera",
"muteEveryoneDialog": "Sei sicuro di voler spegnere il microfono a tutti? Non potrai riattivarli, ma loro potranno farlo in qualsiasi momento.",
"moderationAudioLabel": "Permetti ai partecipenti di riaccendere il microfono",
"moderationVideoLabel": "Permetti ai partecipanti di riattivare la videocamera",
"muteEveryoneDialog": "I partecipanti possono riaccenderli in quasiasi momento.",
"muteEveryoneDialogModerationOn": "I partecipanti possono fare richiesta di parlare in ogni momento.",
"muteEveryoneElseDialog": "Una volta spenti i microfoni non potrai riattivarli, ma loro potranno farlo in qualsiasi momento.",
"muteEveryoneElseTitle": "Spengo il microfono a tutti, eccetto a {{whom}}?",
@@ -279,11 +305,11 @@
"muteEveryonesVideoTitle": "Vuoi spegnere le videocamere di tutti?",
"muteParticipantBody": "Non sarai in grado di riattivare il loro microfono, ma loro potranno riattivarlo in qualsiasi momento.",
"muteParticipantButton": "Spegni microfono",
"muteParticipantDialog": "Sei sicuro di voler spegnere il microfono di questo partecipante? Lui potrà riattivarlo in ogni momento.",
"muteParticipantTitle": "Spengo il microfono a questo partecipante?",
"muteParticipantsVideoBody": "Una volta spenta la videocamera non potrai riaccenderla, ma lui potrà riattivarla in qualsiasi momento.",
"muteParticipantsVideoBodyModerationOn": "Non potrai riaccendere le videocamere, né potranno loro.",
"muteParticipantsVideoButton": "Spegni videocamera",
"muteParticipantsVideoDialog": "Sei sicuro di voler spegnere la videocamera di questo partecipante? Lui potrà riattivarla in ogni momento.",
"muteParticipantsVideoDialogModerationOn": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on and neither will they.",
"muteParticipantsVideoTitle": "Vuoi spegnere la videocamera di questo partecipante?",
"noDropboxToken": "Token Dropbox non valido",
"password": "Password",
@@ -297,6 +323,7 @@
"popupError": "Il tuo browser sta bloccando i pop-up da questo sito. Per favore abilita i pop-up dalle impostazioni di sicurezza del browser e riprova.",
"popupErrorTitle": "Pop-up bloccato",
"readMore": "continua",
"recentlyUsedObjects": "Gli oggetti che hai usato di recente",
"recording": "Registrazione",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossibile durante una diretta.",
"recordingDisabledTooltip": "Registrazione disabilitata.",
@@ -319,6 +346,12 @@
"screenSharingFailed": "Ops! Non è stato possibile avviare la condivisione dello schermo!",
"screenSharingFailedTitle": "Condivisione dello schermo fallita!",
"screenSharingPermissionDeniedError": "Qualcosa non funziona nei permessi di condivisione dello schermo. Ricarica e riprova.",
"searchInSalesforce": "Cerca in Salesforce",
"searchResults": "Risultati ricerca({{count}})",
"searchResultsDetailsError": "Qualcosa non ha funzionato nella ricezione dei dati del proprietario.",
"searchResultsError": "Qualcosa non ha funzionato nella ricezione dei dati.",
"searchResultsNotFound": "Nessun risultato.",
"searchResultsTryAgain": "Prova altre parole di ricerca.",
"sendPrivateMessage": "Hai ricevuto un messaggio privato poco fa. Vorresti rispondergli privatamente o vuoi mandare la risposta al gruppo?",
"sendPrivateMessageCancel": "Invia al gruppo",
"sendPrivateMessageOk": "Invia privatamente",
@@ -341,8 +374,10 @@
"shareVideoTitle": "Condividi un video",
"shareYourScreen": "Condividi schermo",
"shareYourScreenDisabled": "Condivisione schermo disabilitata.",
"sharedVideoDialogError": "Errore: URL non valido",
"sharedVideoLinkPlaceholder": "Link YouTube o link video diretto",
"startLiveStreaming": "Inizia una diretta",
"start": "Avvia ",
"startLiveStreaming": "Avvia diretta",
"startRecording": "Inizia a registrare",
"startRemoteControlErrorMessage": "Si è verificato un errore nel tentativo di avviare la sessione di controllo remoto!",
"stopLiveStreaming": "Ferma la diretta streaming",
@@ -384,6 +419,10 @@
"veryBad": "Pessima",
"veryGood": "Ottima"
},
"giphy": {
"noResults": "Trovato niente :(",
"search": "Cerca in GIPHY"
},
"helpView": {
"header": "Aiuto"
},
@@ -450,6 +489,7 @@
"focusLocal": "Sposta il focus sul tuo video",
"focusRemote": "Sposta il focus sul video di un altro partecipante",
"fullScreen": "Attiva o disattiva schermo intero",
"giphyMenu": "Mostra menù GIPHY",
"keyboardShortcuts": "Scorciatoie da tastiera",
"localRecording": "Mostra o nascondi i controlli per la registrazione",
"mute": "Attiva o disattiva il microfono",
@@ -479,6 +519,7 @@
"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",
"googlePrivacyPolicy": "Norme sulla riservatezza di Google",
"inProgress": "Diretta o registrazione in corso",
"invalidStreamKey": "La chiave per le dirette potrebbe non essere corretta.",
"limitNotificationDescriptionNative": "La tua diretta sarà limitata a {{limit}} minuti. Per dirette illimitate, prova {{app}}.",
"limitNotificationDescriptionWeb": "Data l'alta domanda la tua diretta sarà limitata a {{limit}} minuti. Per dirette illimitate, prova <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
@@ -488,6 +529,7 @@
"onBy": "{{name}} ha iniziato la diretta",
"pending": "Avvio diretta...",
"serviceName": "Servizio dirette",
"sessionAlreadyActive": "Questa sessione è in già in fase di registrazione o trasmessione in diretta.",
"signIn": "Collegati con Google",
"signInCTA": "Collegati o inserisci la tua chiave YouTube per la trasmissione in diretta.",
"signOut": "Scollegati",
@@ -502,6 +544,7 @@
"admitAll": "Ammetti tutti",
"allow": "Autorizza",
"backToKnockModeButton": "Nessuna password, richiedi l'accesso",
"chat": "Conversazione",
"dialogTitle": "Sala d'attesa",
"disableDialogContent": "Sala d'attesa attiva. Questa funzione ti permette di non dare accesso alla riunione a partecipanti indesiderati. Vuoi disattivarla?",
"disableDialogSubmit": "Disattiva",
@@ -514,6 +557,7 @@
"errorMissingPassword": "Per favore, mettere la password della riunione",
"invalidPassword": "Password errata",
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un moderatore.",
"joinRejectedTitle": "Richiesta d'ingresso respinta.",
"joinTitle": "Entra nella riunione",
"joinWithPasswordMessage": "Ho inviato la password per entrare, attendi...",
"joiningMessage": "Entrerai nella riunione non appena qualcuno approva la tua richiesta",
@@ -522,6 +566,8 @@
"knockButton": "Chiedi d'entrare",
"knockTitle": "Qualcuno vuole entrare nella riunione",
"knockingParticipantList": "Lista dei partecipanti in attesa",
"lobbyChatStartedNotification": "{{moderator}} sta parlando con {{attendee}} in sala d'attesa",
"lobbyChatStartedTitle": "{{moderator}} sta parlando con te in sala d'attesa.",
"nameField": "Scrivi il tuo nome",
"notificationLobbyAccessDenied": "{{targetParticipantName}} è stato respinto da {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} è stato autorizzato ad entrare da {{originParticipantName}}",
@@ -574,18 +620,32 @@
"OldElectronAPPTitle": "Falla di sicurezza!",
"allowAction": "Permetti",
"allowedUnmute": "Puoi accendere il microfono, avviare la videocamera, o condividere il tuo schermo.",
"audioUnmuteBlockedDescription": "Lo sblocco dei microfoni è stato temporaneament bloccato per limiti del sistema.",
"audioUnmuteBlockedTitle": "Riattivazione dei microfoni bloccata!",
"chatMessages": "Messaggi delle conversazioni",
"connectedOneMember": "{{name}} si è connesso",
"connectedThreePlusMembers": "{{name}} e altri {{count}} si sono connessi",
"connectedTwoMembers": "{{first}} e {{second}} si sono connessi",
"disconnected": "disconnesso",
"displayNotifications": "Mostra le notifiche per",
"focus": "Focus su riunione",
"focusFail": "{{component}} non disponibile - riprova in {{ms}} sec",
"grantedTo": "Permessi di moderatore accordati a {{to}}!",
"gifsMenu": "GIPHY",
"groupTitle": "Notifiche",
"hostAskedUnmute": "Il moderatore vorrebbe che tu parlassi",
"invitedOneMember": "{{displayName}} è stato invitato",
"invitedThreePlusMembers": "Hai invitato {{name}} e altri {{count}}",
"invitedTwoMembers": "Hai invitato {{first}} e {{second}}",
"kickParticipant": "{{kicked}} è stato espulso da {{kicker}}",
"leftOneMember": "{{name}} ha lasciato la riunione",
"leftThreePlusMembers": "{{name}} e molti altri hanno lasciato la riunione",
"leftTwoMembers": "{{first}} e {{second}} hanno lasciato la riunione",
"linkToSalesforce": "Collega a Salesforce",
"linkToSalesforceDescription": "Puoi collegare il sommario della riunione ad un oggetto Salesforce.",
"linkToSalesforceError": "Failed to link meeting to Salesforce",
"linkToSalesforceKey": "Link this meeting",
"linkToSalesforceProgress": "Linking meeting to Salesforce...",
"linkToSalesforceSuccess": "The meeting was linked to Salesforce",
"me": "Io",
"moderationInEffectCSDescription": "Alza la mano, se vuoi condividere lo schermo, per favore.",
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal moderatore",
@@ -609,13 +669,18 @@
"oldElectronClientDescription1": "Sembri stare usando una versione obsoleta del client Jitsi Meet che ha dei problemi di sicurezza noti. Assicurati di aggiornarla presso il nostro ",
"oldElectronClientDescription2": "ultima build",
"oldElectronClientDescription3": " ora!",
"participantWantsToJoin": "Vuole unirsi alla riunione",
"participantsWantToJoin": "Vogliono unirsi alla riunione",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) è stata tolta da un altro partecipante",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) è stata messa da un altro partecipante",
"raiseHandAction": "Alza la mano",
"raisedHand": "{{name}} vorrebbe intervenire.",
"raisedHand": "Vorrebbe intervenire.",
"raisedHands": "{{participantName}} e {{raisedHands}} altre persone",
"reactionSounds": "Disattiva suoni",
"screenShareNoAudio": " L'opzione di condivisione audio non era selezionata nella schermata di selezione della finestra da condividere.",
"reactionSoundsForAll": "Disattiva suoni a tutti",
"screenShareNoAudio": "L'opzione di condivisione audio non era selezionata nella schermata di selezione della finestra da condividere.",
"screenShareNoAudioTitle": "Condividi audio non è stato selezionato",
"selfViewTitle": "Puoi sempre ripristinare la tua immagine nelle impostazioni",
"somebody": "Qualcuno",
"startSilentDescription": "Entra di nuovo nella riunione, per attivare l'audio",
"startSilentTitle": "Sei entrato nella riunione senza aver scelto un dispositivo audio per sentire!",
@@ -623,23 +688,30 @@
"suboptimalExperienceTitle": "Avviso sul browser",
"unmute": "Accendi microfono",
"videoMutedRemotelyDescription": "Puoi riaccenderla in qualsiasi momento.",
"videoMutedRemotelyTitle": "La videocamera ti è stata spenta da {{participantDisplayName}}!"
"videoMutedRemotelyTitle": "La videocamera ti è stata spenta da {{participantDisplayName}}!",
"videoUnmuteBlockedDescription": "Riattivazione video e condivisione schermo sono state momentaneamente bloccate per limiti di sistema.",
"videoUnmuteBlockedTitle": "Riattivazione video e condivisione schermo bloccate!",
"viewLobby": "Vedi sala d'attesa",
"waitingParticipants": "{{waitingParticipants}} persone"
},
"participantsPane": {
"actions": {
"allow": "Permetti ai partecipanti di:",
"allowVideo": "Autorizza video",
"askUnmute": "Chiedi di attivare audio",
"audioModeration": "Possono attivare audio",
"askUnmute": "Chiedi di riattivare audio",
"audioModeration": "Riattivare audio",
"blockEveryoneMicCamera": "Blocca audio e video a tutti",
"invite": "Invita persone",
"moreModerationActions": "Altre opzioni di moderazione",
"moreModerationControls": "Altri controlli di moderazione",
"moreParticipantOptions": "Altre opzioni partecipanti",
"mute": "Silenzia",
"muteAll": "Silenzia tutti",
"muteEveryoneElse": "Silenzia tutti gli altri",
"stopEveryonesVideo": "Ferma il video di tutti",
"stopVideo": "Ferma il video",
"unblockEveryoneMicCamera": "Sblocca audio e video a tutti",
"videoModeration": "Avvia il loro video"
"videoModeration": "Riavviare videocamera"
},
"close": "Chiudi",
"header": "Partecipanti",
@@ -647,15 +719,18 @@
"lobby": "Sala d'attesa ({{count}})",
"participantsList": "Partecipanti alla riunione ({{count}})",
"waitingLobby": "In attesa ({{count}})"
}
},
"search": "Cerca partecipanti"
},
"passwordDigitsOnly": "Fino a {{number}} cifre",
"passwordSetRemotely": "definita da altro utente",
"pinnedParticipant": "Il partecipante è in evidenza",
"polls": {
"answer": {
"skip": "Salta",
"submit": "Invia"
},
"by": "Da {{ name }}",
"create": {
"addOption": "Aggiungi risposta",
"answerPlaceholder": "Risposta {{index}}",
@@ -731,9 +806,9 @@
"linkCopied": "Collegamento copiato negli appunti",
"lookGood": "Sembra che il tuo microfono funzioni correttamente",
"or": "o",
"premeeting": "Attesa riunione",
"premeeting": "Pre-riunione",
"screenSharingError": "Errore di condivisione dello schermo:",
"showScreen": "Avvia la schermata d'attesa della riunione",
"showScreen": "Attiva schermata pre-riunione",
"startWithPhone": "Avvia usando il telefono, per parlare",
"videoOnlyError": "Errore video:",
"videoTrackError": "Impossibile creare la traccia video.",
@@ -753,6 +828,9 @@
"rejected": "Rifiutato",
"ringing": "Sta suonando..."
},
"privacyView": {
"header": "Privacy"
},
"profile": {
"avatar": "avatar",
"setDisplayNameLabel": "Imposta il nome da visualizzare",
@@ -761,6 +839,19 @@
"title": "Profilo"
},
"raisedHand": "Vorrebbe parlare",
"raisedHandsLabel": "Numero di mani alzate",
"record": {
"already": {
"linked": "La riunione è già collegata a questo oggetto Salesforce."
},
"type": {
"account": "Account",
"contact": "Contact",
"lead": "Lead",
"opportunity": "Opportunity",
"owner": "Owner"
}
},
"recording": {
"authDropboxText": "Carica su Dropbox",
"availableSpace": "Spazio disponibile: {{spaceLeft}} MB (rimangono approssimativamente {{duration}} minuti di registrazione)",
@@ -775,6 +866,12 @@
"expandedPending": "La registrazione è in fase di avvio...",
"failedToStart": "Non è stato possibile avviare la registrazione",
"fileSharingdescription": "Condividi la registrazione con i partecipanti alla riunione",
"highlight": "Evidenzia",
"highlightMoment": "Evidenzia momento",
"highlightMomentDisabled": "Puoi evidenziare dei momenti quando parte la registrazione",
"highlightMomentSuccess": "Momento evidenziato",
"highlightMomentSucessDescription": "Il tuo momento evidenziato sarà aggiunto al riepilogo della riunione.",
"inProgress": "Registrazione o diretta in corso",
"limitNotificationDescriptionNative": "La tua registrazione sarà limitata a {{limit}} minuti. Per registrazioni illimitate, prova <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Data l'alta domanda la tua registrazione sarà limitata a {{limit}} minuti. Per registrazioni illimitate, prova <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "Abbiamo generato un collegamento alla tua registrazione.",
@@ -788,7 +885,9 @@
"rec": "REC",
"serviceDescription": "La tua registrazione verrà salvata dal servizio di registrazione che hai scelto",
"serviceDescriptionCloud": "Registrazione in rete",
"serviceDescriptionCloudInfo": "Le riunioni registrate vengono automaticamente cancellate 24 ore dopo la registrazione.",
"serviceName": "Servizio di registrazione",
"sessionAlreadyActive": "Questa sessione è già in corso di registrazione o trasmissione in diretta.",
"signIn": "Entra",
"signOut": "Esci",
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
@@ -806,7 +905,7 @@
},
"settings": {
"calendar": {
"about": "Lintegrazione del calendario con {{appName}} è usata per accedere in sicurezza al proprio calendario per poter leggere i prossimi appuntamenti ",
"about": "Lintegrazione del calendario con {{appName}} è usata per accedere in sicurezza al proprio calendario e poter leggere i prossimi appuntamenti ",
"disconnect": "Disconnetti",
"microsoftSignIn": "Connettiti con un account Microsoft",
"signedIn": "Sto accedendo agli eventi del calendario per {{email}}. Fai click su «Disconnetti» per interrompere laccesso agli eventi del calendario.",
@@ -821,9 +920,10 @@
"incomingMessage": "Messaggio in arrivo",
"language": "Lingua",
"loggedIn": "Connesso come {{name}}",
"maxStageParticipants": "Numero massimo di partecipanti che possono essere aggiunti come oratori",
"microphones": "Microfoni",
"moderator": "Moderatore",
"more": "Altro",
"more": "Mostra di più",
"name": "Nome",
"noDevice": "Nessuno",
"participantJoined": "Partecipante Entrato",
@@ -834,10 +934,12 @@
"selectAudioOutput": "Uscita audio",
"selectCamera": "Videocamera",
"selectMic": "Microfono",
"selfView": "Tua immagine",
"sounds": "Suoni",
"speakers": "Altoparlanti",
"startAudioMuted": "Tutti cominciano a microfono spento",
"startVideoMuted": "Tutti cominciano a video disattivato",
"startReactionsMuted": "Spegni i suoni delle reazioni a tutti",
"startVideoMuted": "Tutti cominciano a videocamera disattivata",
"talkWhileMuted": "Parla senza microfono",
"title": "Impostazioni"
},
@@ -869,13 +971,21 @@
},
"speaker": "Relatore",
"speakerStats": {
"angry": "Arrabbiato",
"disgusted": "Disgustato",
"displayEmotions": "Mostra Emozioni",
"fearful": "Spaventato",
"happy": "Contento",
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Nome",
"neutral": "Neutro",
"sad": "Triste",
"search": "Cerca",
"seconds": "{{count}}s",
"speakerStats": "Statistiche",
"speakerTime": "Tempo"
"speakerTime": "Tempo",
"surprised": "Sorpreso"
},
"startupoverlay": {
"genericTitle": "Per la riunione devono essere usati il tuo microfono e la tua videocamera.",
@@ -887,6 +997,9 @@
"text": "Premi il pulsante <i>Ricollegati</i> per ricollegarti.",
"title": "La video chiamata si è interrotta perché il computer è stato sospeso."
},
"termsView": {
"header": "Termini"
},
"toolbar": {
"Settings": "Impostazioni",
"accessibilityLabel": {
@@ -894,6 +1007,7 @@
"audioOnly": "Spegni/Accendi audio",
"audioRoute": "Scegli l'uscita audio",
"boo": "Boo",
"breakoutRoom": "Entra/Lascia sottogruppo",
"callQuality": "Imposta qualità della chiamata",
"cc": "Avvia/Ferma sottotitoli",
"chat": "Entra/Esci da conversazione",
@@ -905,6 +1019,7 @@
"expand": "Espandi",
"feedback": "Lascia un feedback",
"fullScreen": "Apri/Chiudi schermo intero",
"giphy": "Menù GIPHY",
"grantModerator": "Autorizza moderatore",
"hangup": "Lascia la riunione",
"help": "Aiuto",
@@ -912,6 +1027,7 @@
"kick": "Espelli partecipante",
"laugh": "Ridi",
"like": "Mi piace",
"linkToSalesforce": "Collega a Salesforce",
"lobbyButton": "Attiva/disattiva sala d'attesa",
"localRecording": "Abilita/disattiva controlli di registrazione locale",
"lockRoom": "Attiva o disattiva password",
@@ -934,6 +1050,7 @@
"remoteVideoMute": "Spegni videocamera del partecipante",
"security": "Impostazioni di sicurezza",
"selectBackground": "Scegli sfondo",
"selfView": "Mostra tua immagine",
"shareRoom": "Invita qualcuno",
"shareYourScreen": "Attiva/disattiva condivisione schermo",
"shareaudio": "Condividi audio",
@@ -967,16 +1084,20 @@
"download": "Scarica le nostre app",
"e2ee": "Crittografia punto-punto",
"embedMeeting": "Incorpora riunione altrove",
"enterFullScreen": "Visualizza a schermo intero",
"enterFullScreen": "Schermo intero",
"enterTileView": "Vedi tutti i partecipanti",
"exitFullScreen": "Esci da schermo intero",
"exitTileView": "Vedi una persona sola",
"feedback": "Lascia un feedback",
"giphy": "Menù GIPHY",
"hangup": "Butta giù",
"help": "Aiuto",
"invite": "Invita persone",
"joinBreakoutRoom": "Entra in sottogruppo",
"laugh": "Ridi",
"leaveBreakoutRoom": "Lascia breakout room",
"like": "Mi piace",
"linkToSalesforce": "Collega a Salesforce",
"lobbyButtonDisable": "Disabilita sala d'attesa",
"lobbyButtonEnable": "Abilita sala d'attesa",
"login": "Accedi",
@@ -1018,6 +1139,7 @@
"speakerStats": "Statistiche",
"startScreenSharing": "Inizia la condivisione dello schermo",
"startSubtitles": "Avvia sottotitoli",
"stopAudioSharing": "Ferma condivisione audio",
"stopScreenSharing": "Ferma la condivisione dello schermo",
"stopSharedVideo": "Ferma video",
"stopSubtitles": "Ferma sottotitoli",
@@ -1063,17 +1185,21 @@
"pending": "{{displayName}} è stato invitato"
},
"videoStatus": {
"adjustFor": "Adjust for:",
"audioOnly": "AUD",
"audioOnlyExpanded": "Hai attivato la modalità con banda limitata. Questa modalità permette di risparmiare banda, ma non vedrai gli altri partecipanti.",
"audioOnlyExpanded": "Hai attivato la modalità per banda limitata. Questa modalità permette di risparmiare banda, ma non vedrai gli altri partecipanti.",
"bestPerformance": "Massime prestazioni",
"callQuality": "Qualità video",
"hd": "HD",
"hdTooltip": "Stai vedendo in alta definizione",
"highDefinition": "Alta definizione",
"highestQuality": "Massima definizione",
"labelTooiltipNoVideo": "Nessun video",
"labelTooltipAudioOnly": "Hai attivato la modalità con banda limitata",
"labelTooltipAudioOnly": "Hai attivato la modalità per banda limitata",
"ld": "LD",
"ldTooltip": "Stai vedendo a bassa definizione",
"lowDefinition": "Bassa definizione",
"performanceSettings": "Impostazione prestazioni",
"sd": "SD",
"sdTooltip": "Stai vedendo a definizione standard",
"standardDefinition": "Definizione standard"
@@ -1086,12 +1212,17 @@
"domuteVideoOfOthers": "Disattiva video di tutti gli altri",
"flip": "Rifletti",
"grantModerator": "Autorizza moderatore",
"hideSelfView": "Nascondi tua immagine",
"kick": "Espelli",
"moderator": "Moderatore",
"mute": "Il partecipante ha il microfono spento",
"muted": "Audio disattivato",
"pinToStage": "Aggiungi agli oratori",
"remoteControl": "Avvia/ferma il controllo remoto",
"show": "Mostra in primo piano",
"screenSharing": "Il partecipante sta condividendo lo schermo",
"show": "Mostra tra gli oratori",
"showSelfView": "Mostra tua immagine",
"unpinFromStage": "Togli",
"videoMuted": "Video disattivato",
"videomute": "Il partecipante ha la videocamera spenta"
},
@@ -1116,7 +1247,8 @@
"slightBlur": "Sfuoca leggermente",
"title": "Sfondi",
"uploadedImage": "Carica immagine {{index}}",
"webAssemblyWarning": "Il WebAssembly not è supportato"
"webAssemblyWarning": "Il WebAssembly non è supportato",
"webAssemblyWarningDescription": "Il WebAssembly è disabilitato o non è supportat da questo browser"
},
"volumeSlider": "Sbarra volume",
"welcomepage": {

View File

@@ -60,7 +60,7 @@
},
"calendarSync": {
"addMeetingURL": "Добавить ссылку конференции",
"confirmAddLink": "Вы хотите добавить ссылку Jitsi к этому календарному событию?",
"confirmAddLink": "Вы хотите добавить ссылку {{app}} к этому календарному событию?",
"error": {
"appConfiguration": "Неправильно настроена интеграция календаря.",
"generic": "Произошла ошибка. Проверьте настройки календаря или попробуйте обновить его.",
@@ -429,7 +429,7 @@
"answer": "Ответ",
"audioCallTitle": "Входящий звонок",
"decline": "Отклонить",
"productLabel": "из Jitsi Meet",
"productLabel": "из {{app}}",
"videoCallTitle": "Входящий видеозвонок"
},
"info": {
@@ -665,7 +665,7 @@
"newDeviceAction": "Использовать",
"newDeviceAudioTitle": "Обнаружено новое аудиоустройство",
"newDeviceCameraTitle": "Обнаружена новая камера",
"oldElectronClientDescription1": "Похоже, вы используете старую версию клиента Jitsi Meet, которая имеет известные уязвимости в системе безопасности. Убедитесь, что вы обновили до нашей ",
"oldElectronClientDescription1": "Похоже, вы используете старую версию клиента {{app}}, которая имеет известные уязвимости в системе безопасности. Убедитесь, что вы обновили до нашей ",
"oldElectronClientDescription2": "последней версии",
"oldElectronClientDescription3": " сейчас!",
"participantWantsToJoin": "Хочет присоединиться к митингу",
@@ -1268,9 +1268,9 @@
"go": "ОК",
"goSmall": "ОК",
"headerSubtitle": "Защищенная высококачественная видеосвязь",
"headerTitle": "Сервер видеоконференцсвязи Jitsi Meet",
"headerTitle": "Сервер видеоконференцсвязи {{app}}",
"info": "Инфо",
"jitsiOnMobile": "Jitsy для мобильных устройств — загрузите наши приложения и начните встречу из любого места",
"jitsiOnMobile": "{{app}} для мобильных устройств — загрузите наши приложения и начните встречу из любого места",
"join": "СОЗДАТЬ / ПРИСОЕДИНИТЬСЯ",
"logo": {
"calendar": "Calendar логотип",

View File

@@ -602,10 +602,12 @@
"OldElectronAPPTitle": "Güvenlik açığı!",
"allowAction": "İzin ver",
"allowedUnmute": "Mikrofonunuzu sessizden çıkarabilir, kameranızı başlatabilir veya ekranınızı paylaşabilirsiniz.",
"chatMessages": "Sohbet mesajları",
"connectedOneMember": "{{name}} toplantıya katıldı",
"connectedThreePlusMembers": "{{name}} ve {{count}} kişi daha toplantıya katıldı",
"connectedTwoMembers": "{{first}} ve {{second}} toplantıya katıldı",
"disconnected": "bağlantı kesildi",
"displayNotifications": "Bildirimleri görüntüle",
"focus": "Toplantı odağı",
"focusFail": "{{component}} uygun değil - {{ms}} saniye içinde tekrar deneyin",
"grantedTo": "{{to}} kişisine yönetici hakları verildi!",
@@ -662,6 +664,9 @@
"audioModeration": "Seslerini aç",
"blockEveryoneMicCamera": "Herkesin mikrofonunu ve kamerasını blokla",
"invite": "Birini davet et",
"moreModerationActions": "Daha fazla denetleme seçeneği",
"moreModerationControls": "Daha fazla denetleme kontrolü",
"moreParticipantOptions": "Daha fazla katılımcı seçeneği",
"mute": "Sustur",
"muteAll": "Herkesi sustur",
"muteEveryoneElse": "Diğer herkesi sessize al",
@@ -676,7 +681,8 @@
"lobby": "Lobi ({{count}})",
"participantsList": "Toplantı Katılımcıları ({{count}})",
"waitingLobby": "Lobide bekleyen ({{count}})"
}
},
"search": "Katılımcıları ara"
},
"passwordDigitsOnly": "{{number}} rakama kadar",
"passwordSetRemotely": "başka katılımcı tarafından ayarlandı",
@@ -850,6 +856,7 @@
"incomingMessage": "Gelen mesaj",
"language": "Dil",
"loggedIn": "{{name}} olarak giriş yapıldı",
"maxStageParticipants": "Ana ekrana sabitlenecek maksimum katılımcı sayısı",
"microphones": "Mikrofonlar",
"moderator": "Yönetici",
"more": "Daha fazla",
@@ -1109,6 +1116,7 @@
"lowDefinition": "Düşük çözünürlük",
"onlyAudioAvailable": "Yalnızca ses kullanılabilir",
"onlyAudioSupported": "Bu tarayıcıda yalnızca sesi destekliyoruz.",
"performanceSettings": "Performans ayarları",
"sd": "SD",
"sdTooltip": "Standart çözünürlüklü video görüntüleme",
"standardDefinition": "Standart çözünürlük"

View File

@@ -31,6 +31,7 @@
},
"audioDevices": {
"bluetooth": "藍牙",
"car": "汽車音響",
"headphones": "耳機",
"none": "沒有可用的音效裝置",
"phone": "電話",
@@ -39,9 +40,6 @@
"audioOnly": {
"audioOnly": "低頻寬"
},
"blankPage": {
"meetingEnded": "會議已結束。"
},
"breakoutRooms": {
"actions": {
"add": "新增討論室",
@@ -83,6 +81,7 @@
"enter": "加入聊天室",
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
"fieldPlaceHolder": "在此輸入您的訊息",
"lobbyChatMessageTo": "Lobby chat message to {{recipient}}",
"message": "訊息",
"messageAccessibleTitle": "{{user}} 說:",
"messageAccessibleTitleMe": "您說:",
@@ -130,11 +129,11 @@
},
"connectionindicator": {
"address": "位址:",
"audio_ssrc": "Audio SSRC",
"audio_ssrc": "音訊 SSRC",
"bandwidth": "估計頻寬:",
"bitrate": "位元率:",
"bridgeCount": "伺服器數量:",
"codecs": "Codecs (A/V)",
"codecs": "編碼 (A/V)",
"connectedTo": "已連接至:",
"e2e_rtt": "E2E RTT",
"framerate": "影格率:",
@@ -163,7 +162,7 @@
"status": "連接:",
"transport": "傳輸:",
"transport_plural": "傳輸:",
"video_ssrc": "Video SSRC"
"video_ssrc": "視訊 SSRC"
},
"dateUtils": {
"earlier": "稍早",
@@ -180,7 +179,8 @@
"joinInApp": "使用 App 加入會議",
"launchWebButton": "在瀏覽器開啟",
"title": "正在 {{app}} 發起您的會議...",
"tryAgainButton": "在桌面上再試一次"
"tryAgainButton": "在桌面上再試一次",
"unsupportedBrowser": "您似乎正在使用我們不支援的瀏覽器。"
},
"defaultLink": "例如 {{url}}",
"defaultNickname": "例如 春嬌 志明",
@@ -207,15 +207,17 @@
"Remove": "移除",
"Share": "分享",
"Submit": "提交",
"WaitForHostMsg": "此會議 尚未啟動。如果您是會議主人,請進行認證;否則,請等待會議主人到達。",
"WaitForHostMsg": "此會議尚未開始。如果您是主人,請進行認證;否則,請等待主人到達。",
"WaitingForHost": "等侯主辦人...",
"Yes": "是的",
"accessibilityLabel": {
"liveStreaming": "直播串流"
},
"add": "新增",
"addMeetingNote": "新增此會議的備註",
"addOptionalNote": "新增備註 (選填)",
"allow": "允許",
"alreadySharedVideoMsg": "另一位參與者已經進行分享影像了。此會議同個時間只能允許一人分享影像畫面。",
"alreadySharedVideoMsg": "另一位參與者已經正在分享影像了。此會議同時僅允許一人分享影像畫面。",
"alreadySharedVideoTitle": "一次只允許一位影像分享",
"applicationWindow": "應用程式視窗",
"authenticationRequired": "需要驗證",
@@ -265,11 +267,13 @@
"kickParticipantDialog": "您確定要將這位參與者踢出會議嗎?",
"kickParticipantTitle": "踢出這位參與者?",
"kickTitle": "噢!{{participantDisplayName}} 已將您踢出會議",
"linkMeeting": "連結會議",
"linkMeetingTitle": "將會議連結至 Salesforce",
"liveStreaming": "直播串流中",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "正在錄影,無法使用",
"liveStreamingDisabledTooltip": "啟動直播串流已停用。",
"localUserControls": "本機使用者控制",
"lockMessage": "鎖定會議失敗。",
"lockMessage": "無法鎖定會議。",
"lockRoom": "增加會議 $t(lockRoomPasswordUppercase)",
"lockTitle": "鎖定失敗",
"login": "登入",
@@ -319,6 +323,7 @@
"popupError": "您的瀏覽器在此網站上阻擋彈出視窗。請在瀏覽器的安全設定中啟用並再試一次。",
"popupErrorTitle": "彈出視窗遭到阻擋",
"readMore": "更多",
"recentlyUsedObjects": "您近期使用過的物件",
"recording": "錄影中",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "正在直播時無法使用",
"recordingDisabledTooltip": "啟動錄影已停用。",
@@ -335,19 +340,25 @@
"removeSharedVideoMsg": "您確定要移除自己分享的影像嗎?",
"removeSharedVideoTitle": "移除分享的影像",
"reservationError": "預約系統錯誤",
"reservationErrorMsg": "錯誤碼: {{code}} 訊息: {{msg}}",
"reservationErrorMsg": "錯誤碼:{{code}} 訊息:{{msg}}",
"retry": "重試",
"screenSharingAudio": "分享音訊",
"screenSharingFailed": "噢喔!發生錯誤,我們無法啟動螢幕分享!",
"screenSharingFailedTitle": "螢幕分享失敗!",
"screenSharingPermissionDeniedError": "噢喔!您的影像分享權限發生問題。請重新載入,再試一次。",
"sendPrivateMessage": "您最近有收到私人訊息。您要進行私人回覆,或是要將自己的訊息發佈至群組?",
"searchInSalesforce": "在 Salesforce 中搜尋",
"searchResults": "搜尋結果 ({{count}})",
"searchResultsDetailsError": "取得擁有者資料時發生錯誤。",
"searchResultsError": "取得資料時發生錯誤。",
"searchResultsNotFound": "找不到任何結果。",
"searchResultsTryAgain": "請嘗試使用其他關鍵字。",
"sendPrivateMessage": "您最近有收到私人訊息。您要進行私人回覆,還是要將自己的訊息發佈至群組?",
"sendPrivateMessageCancel": "發佈至群組",
"sendPrivateMessageOk": "私人回覆",
"sendPrivateMessageTitle": "私人傳訊",
"sendPrivateMessageTitle": "私人回覆",
"serviceUnavailable": "服務無法使用",
"sessTerminated": "通話已經終止",
"sessionRestarted": "通話被橋接器重新啟動",
"sessionRestarted": "通話因連線問題重新啟動",
"shareAudio": "繼續",
"shareAudioTitle": "如何分享音訊",
"shareAudioWarningD1": "您必須先停止分享畫面才能分享音訊。",
@@ -359,11 +370,13 @@
"shareScreenWarningD2": "您必須先停止分享音訊,啟動畫面分享,然後勾選 \"分享音訊\" 選項。",
"shareScreenWarningH1": "如果您只要分享畫面:",
"shareScreenWarningTitle": "您必須先停止分享音訊才能分享畫面",
"shareVideoLinkError": "請提供正確的 YouTube 連結。",
"shareVideoLinkError": "請提供正確的影片網址。",
"shareVideoTitle": "分享影像",
"shareYourScreen": "分享自己的螢幕",
"shareYourScreenDisabled": "螢幕分享已停用。",
"sharedVideoLinkPlaceholder": "YouTube 或影片連結",
"sharedVideoDialogError": "錯誤:網址無效",
"sharedVideoLinkPlaceholder": "YouTube 或影片網址",
"start": "開始 ",
"startLiveStreaming": "啟動直播串流",
"startRecording": "啟動錄影作業",
"startRemoteControlErrorMessage": "嘗試啟動遠端控制階段時發生錯誤!",
@@ -388,7 +401,7 @@
"yourEntireScreen": "您的畫面"
},
"documentSharing": {
"title": "分享的文件"
"title": "分享的檔案"
},
"e2ee": {
"labelToolTip": "此通話的音訊及視訊皆已使用端對端加密"
@@ -406,6 +419,10 @@
"veryBad": "極差",
"veryGood": "極好"
},
"giphy": {
"noResults": "找不到任何結果 :(",
"search": "搜尋 GIPHY"
},
"helpView": {
"header": "說明中心"
},
@@ -423,8 +440,8 @@
"conferenceURL": "連結:",
"copyNumber": "複製號碼",
"country": "國家",
"dialANumber": "要參加您的會議,撥打以下其中一支號碼,然後輸入 PIN 碼。",
"dialInConferenceID": "PIN 碼:",
"dialANumber": "要參加您的會議,撥打以下其中一支號碼,然後輸入 PIN 碼。",
"dialInConferenceID": "PIN 碼:",
"dialInNotSupported": "抱歉,目前不支援電話撥入。",
"dialInNumber": "撥入:",
"dialInSummaryError": "目前解析撥入資訊錯誤。請稍後再試一次。",
@@ -472,6 +489,7 @@
"focusLocal": "聚焦於自己的影像",
"focusRemote": "聚焦於另一人的影像",
"fullScreen": "觀看或離開全螢幕",
"giphyMenu": "切換 GIPHY 選單",
"keyboardShortcuts": "快捷鍵",
"localRecording": "顯示或隱藏本機端錄影操控",
"mute": "靜音或解除靜音",
@@ -525,7 +543,8 @@
"admit": "准許",
"admitAll": "准許所有人",
"allow": "允許",
"backToKnockModeButton": "沒有密碼,請要求加入",
"backToKnockModeButton": "求加入",
"chat": "聊天",
"dialogTitle": "大廳模式",
"disableDialogContent": "已開啟大廳模式。此功能能夠確保閒雜人等無法加入您的會議。您確定要停用嗎?",
"disableDialogSubmit": "停用",
@@ -538,6 +557,7 @@
"errorMissingPassword": "請輸入會議密碼",
"invalidPassword": "密碼錯誤",
"joinRejectedMessage": "您的加入請求遭到管理員拒絕。",
"joinRejectedTitle": "加入請求遭拒。",
"joinTitle": "加入會議",
"joinWithPasswordMessage": "正在嘗試透過密碼加入,請稍候...",
"joiningMessage": "一旦他人接受您的請求,即可加入會議",
@@ -546,6 +566,8 @@
"knockButton": "請求加入",
"knockTitle": "有人想要加入會議",
"knockingParticipantList": "要求加入的參與者名單",
"lobbyChatStartedNotification": "{{moderator}} 與 {{attendee}} 開始了大廳聊天",
"lobbyChatStartedTitle": "{{moderator}} 與您開始了大廳聊天。",
"nameField": "輸入您的名字",
"notificationLobbyAccessDenied": "{{originParticipantName}} 拒絕了 {{targetParticipantName}} 的加入請求",
"notificationLobbyAccessGranted": "{{originParticipantName}} 同意了 {{targetParticipantName}} 的加入請求",
@@ -600,13 +622,15 @@
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的畫面。",
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
"audioUnmuteBlockedTitle": "麥克風解除靜音遭封鎖!",
"chatMessages": "聊天訊息",
"connectedOneMember": "{{name}} 加入了會議",
"connectedThreePlusMembers": "{{name}} 及 {{count}} 位人員加入了會議",
"connectedTwoMembers": "{{first}} 及 {{second}} 加入了會議",
"disconnected": "已經中斷連接",
"displayNotifications": "顯示通知",
"focus": "會議焦點",
"focusFail": "{{component}} 無法使用 - 請在 {{ms}} 秒後重試",
"grantedTo": "主持人權限已授予 {{to}}",
"gifsMenu": "GIPHY",
"groupTitle": "通知",
"hostAskedUnmute": "主持人希望您能解除靜音",
"invitedOneMember": "{{name}} 已受邀請",
@@ -616,6 +640,12 @@
"leftOneMember": "{{name}} 已離開會議",
"leftThreePlusMembers": "{{name}} 和其他人已離開會議",
"leftTwoMembers": "{{first}} 和 {{second}} 已離開會議",
"linkToSalesforce": "連結至 Salesforce",
"linkToSalesforceDescription": "您可以將會議摘要連結至 Salesforce 物件。",
"linkToSalesforceError": "無法將會議連結至 Salesforce",
"linkToSalesforceKey": "連結此會議",
"linkToSalesforceProgress": "正在將會議連結至 Salesforce...",
"linkToSalesforceSuccess": "會議已連結至 Salesforce",
"me": "自己",
"moderationInEffectCSDescription": "若要分享視訊,請舉手",
"moderationInEffectCSTitle": "內容分享已被管理員停用",
@@ -639,14 +669,18 @@
"oldElectronClientDescription1": "您似乎正在使用 Jitsi Meet 客戶端的舊版本,其有已知的安全漏洞。請更新到",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": "",
"participantWantsToJoin": "想要加入會議",
"participantsWantToJoin": "想要加入會議",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他參與者移除",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) 已被其他參與者設定",
"raiseHandAction": "舉手",
"raisedHand": "{{name}} 想要發言。",
"raisedHands": "{{participantName}} 和其他 {{raisedHands}} 人",
"reactionSounds": "停用音效",
"reactionSoundsForAll": "為所有人停用音效",
"screenShareNoAudio": "您未在選擇視窗時勾選分享音訊",
"screenShareNoAudioTitle": "未勾選分享音訊",
"selfViewTitle": "您隨時可以在設定中取消隱藏自己的畫面",
"somebody": "某人",
"startSilentDescription": "重新加入會議以啟用語音",
"startSilentTitle": "您加入了會議而無聲音輸出!",
@@ -656,7 +690,9 @@
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
"videoMutedRemotelyTitle": "您的攝影機已被 {{participantDisplayName}} 停用!",
"videoUnmuteBlockedDescription": "視訊鏡頭解除靜音操作由於系統限制而被暫時封鎖。",
"videoUnmuteBlockedTitle": "視訊鏡頭解除靜音遭封鎖!"
"videoUnmuteBlockedTitle": "視訊鏡頭解除靜音遭封鎖!",
"viewLobby": "檢視大廳",
"waitingParticipants": "{{waitingParticipants}} 人"
},
"participantsPane": {
"actions": {
@@ -688,6 +724,7 @@
},
"passwordDigitsOnly": "上限為 {{number}} 位數",
"passwordSetRemotely": "由其他參與者設定",
"pinnedParticipant": "參與者被釘選",
"polls": {
"answer": {
"skip": "跳過",
@@ -801,7 +838,20 @@
"setEmailLabel": "設定您的 Gravatar 電子信箱",
"title": "簡介"
},
"raisedHand": "請求發言",
"raisedHand": "想要發言",
"raisedHandsLabel": "舉手人數",
"record": {
"already": {
"linked": "會議已連結至此 Salesforce 物件。"
},
"type": {
"account": "帳號",
"contact": "聯絡",
"lead": "淺在客戶",
"opportunity": "機會",
"owner": "擁有者"
}
},
"recording": {
"authDropboxText": "上傳至 Dropbox",
"availableSpace": "可用空間:{{spaceLeft}} MB (錄影時間大約 {{duration}} 分鐘)",
@@ -816,6 +866,11 @@
"expandedPending": "錄影正在啟動...",
"failedToStart": "錄影啟動失敗",
"fileSharingdescription": "分享錄影給會議參與者",
"highlight": "精選",
"highlightMoment": "精選時刻",
"highlightMomentDisabled": "您可以在錄製開始後精選時刻",
"highlightMomentSuccess": "已精選的時刻",
"highlightMomentSucessDescription": "您的精選時刻將新增至會議摘要。",
"inProgress": "正在錄製或直播",
"limitNotificationDescriptionNative": "由於目前流量過大,您的錄影時間被限制在 {{limit}} 分鐘。若要無限制的錄影,請試試 <3>{{app}}</3>。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的錄影時間被限制在 {{limit}} 分鐘。若要無限制的錄影,請試試 <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
@@ -830,6 +885,7 @@
"rec": "錄影",
"serviceDescription": "您的錄影會由錄影服務儲存",
"serviceDescriptionCloud": "雲端錄製",
"serviceDescriptionCloudInfo": "已錄製的會議將在 24 小時後自動清除。",
"serviceName": "錄影服務",
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
"signIn": "登入",
@@ -838,8 +894,9 @@
"unavailableTitle": "錄影無法使用",
"uploadToCloud": "上傳至雲端"
},
"screenshareDisplayName": "{{name}} 的畫面",
"sectionList": {
"pullToRefresh": "下以重新整理"
"pullToRefresh": "下以重新整理"
},
"security": {
"about": "您可以新增 $t(lockRoomPassword) 至您的會議。參與者在加入會議前必須先輸入 $t(lockRoomPassword)。",
@@ -864,6 +921,7 @@
"incomingMessage": "新訊息",
"language": "語言",
"loggedIn": "以 {{name}} 登入",
"maxStageParticipants": "能夠被釘選至主舞台的參與者最大人數",
"microphones": "麥克風",
"moderator": "主持人",
"more": "更多",
@@ -877,6 +935,7 @@
"selectAudioOutput": "音訊輸出",
"selectCamera": "攝影裝置",
"selectMic": "麥克風",
"selfView": "自我檢視",
"sounds": "音效",
"speakers": "喇叭",
"startAudioMuted": "全部人啟動時處於靜音",
@@ -915,6 +974,7 @@
"speakerStats": {
"angry": "憤怒",
"disgusted": "作嘔",
"displayEmotions": "顯示表情",
"fearful": "害怕",
"happy": "開心",
"hours": "{{count}}時",
@@ -948,6 +1008,7 @@
"audioOnly": "切換僅聲音",
"audioRoute": "選擇音訊裝置",
"boo": "喝倒彩",
"breakoutRoom": "加入/離開分組討論室",
"callQuality": "管理影像品質",
"cc": "切換字幕",
"chat": "切換聊天視窗",
@@ -965,7 +1026,9 @@
"invite": "邀請人員",
"joy": "笑到流淚",
"kick": "踢出參與者",
"laugh": "大笑",
"like": "比讚",
"linkToSalesforce": "連結至 Salesforce",
"lobbyButton": "啟用/停用大廳模式",
"localRecording": "切換本地端錄影控制",
"lockRoom": "切換會議密碼",
@@ -1027,6 +1090,7 @@
"exitFullScreen": "離開全螢幕",
"exitTileView": "跳出格狀檢視",
"feedback": "回饋",
"giphy": "切換 GIPHY 選單",
"hangup": "離開",
"help": "說明",
"invite": "邀請人員",
@@ -1034,6 +1098,7 @@
"laugh": "大笑",
"leaveBreakoutRoom": "離開分組討論室",
"like": "比讚",
"linkToSalesforce": "連結至 Salesforce",
"lobbyButtonDisable": "停用大廳模式",
"lobbyButtonEnable": "啟用大廳模式",
"login": "登入",
@@ -1148,13 +1213,18 @@
"domuteVideoOfOthers": "停用其他人的攝影機",
"flip": "翻轉",
"grantModerator": "授予管理員",
"hideSelfView": "隱藏自我檢視",
"kick": "踢出",
"moderator": "主持人",
"mute": "參與者處於靜音",
"muted": "處於靜音",
"pinToStage": "釘選至舞台",
"remoteControl": "開始/停止遠端控制",
"screenSharing": "參與者正在分享他們的畫面",
"show": "顯示在台上",
"videoMuted": "已停用攝影機",
"showSelfView": "顯示自我檢視",
"unpinFromStage": "取消釘選",
"videoMuted": "已停用攝影裝置",
"videomute": "參與者已經停止攝影裝置"
},
"virtualBackground": {
@@ -1178,7 +1248,8 @@
"slightBlur": "稍微模糊",
"title": "虛擬背景",
"uploadedImage": "上傳圖片 {{index}}",
"webAssemblyWarning": "不支援 WebAssembly"
"webAssemblyWarning": "不支援 WebAssembly",
"webAssemblyWarningDescription": "WebAssembly 遭停用或不被此連覽器支援"
},
"volumeSlider": "音量滑條",
"welcomepage": {

View File

@@ -894,6 +894,7 @@
"unavailableTitle": "Recording unavailable",
"uploadToCloud": "Upload to the cloud"
},
"screenshareDisplayName": "{{name}}'s screen",
"sectionList": {
"pullToRefresh": "Pull to refresh"
},

View File

@@ -87,6 +87,8 @@ import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { toggleRequestingSubtitles, setRequestingSubtitles } from '../../react/features/subtitles/actions';
import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/functions';
import { toggleTileView, setTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality';
@@ -371,6 +373,12 @@ function initCommands() {
sendAnalytics(createApiEvent('screen.sharing.toggled'));
toggleScreenSharing(options.enable);
},
'toggle-subtitles': () => {
APP.store.dispatch(toggleRequestingSubtitles());
},
'set-subtitles': enabled => {
APP.store.dispatch(setRequestingSubtitles(enabled));
},
'toggle-tile-view': () => {
sendAnalytics(createApiEvent('tile-view.toggled'));
@@ -693,6 +701,9 @@ function initCommands() {
case 'is-audio-muted':
callback(APP.conference.isLocalAudioMuted());
break;
case 'is-audio-disabled':
callback(isAudioMuteButtonDisabled(APP.store.getState()));
break;
case 'is-moderation-on': {
const { mediaType } = request;
const type = mediaType || MEDIA_TYPE.AUDIO;
@@ -725,6 +736,9 @@ function initCommands() {
case 'is-sharing-screen':
callback(Boolean(APP.conference.isSharingScreen));
break;
case 'is-start-silent':
callback(Boolean(APP.store.getState()['features/base/config'].startSilent));
break;
case 'get-content-sharing-participants': {
const tracks = getState()['features/base/tracks'];
const sharingParticipantIds = tracks.filter(tr => tr.videoType === 'desktop').map(t => t.participantId);

View File

@@ -59,6 +59,7 @@ const commands = {
setLargeVideoParticipant: 'set-large-video-participant',
setMediaEncryptionKey: 'set-media-encryption-key',
setParticipantVolume: 'set-participant-volume',
setSubtitles: 'set-subtitles',
setTileView: 'set-tile-view',
setVideoQuality: 'set-video-quality',
startRecording: 'start-recording',
@@ -79,6 +80,7 @@ const commands = {
toggleRaiseHand: 'toggle-raise-hand',
toggleShareAudio: 'toggle-share-audio',
toggleShareScreen: 'toggle-share-screen',
toggleSubtitles: 'toggle-subtitles',
toggleTileView: 'toggle-tile-view',
toggleVirtualBackgroundDialog: 'toggle-virtual-background',
toggleVideo: 'toggle-video'
@@ -925,6 +927,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Returns the audio disabled status.
*
* @returns {Promise} - Resolves with the audio disabled status and rejects on
* failure.
*/
isAudioDisabled() {
return this._transport.sendRequest({
name: 'is-audio-disabled'
});
}
/**
* Returns the moderation on status on the given mediaType.
*
@@ -978,6 +992,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Returns wether meeting is started silent.
*
* @returns {Promise} - Resolves with start silent status.
*/
isStartSilent() {
return this._transport.sendRequest({
name: 'is-start-silent'
});
}
/**
* Returns the avatar URL of a participant.
*

View File

@@ -11,13 +11,15 @@ import { Avatar } from '../../../react/features/base/avatar';
import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
getParticipantDisplayName
} from '../../../react/features/base/participants';
import {
getVideoTrackByParticipant
getVideoTrackByParticipant,
trackStreamingStatusChanged
} from '../../../react/features/base/tracks';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
@@ -116,6 +118,14 @@ export default class LargeVideoManager {
*/
this._videoAspectRatio = 0;
/**
* The video track in effect.
* This is used to add and remove listeners on track streaming status change.
*
* @type {Object}
*/
this.videoTrack = undefined;
this.$container = $('#largeVideoContainer');
this.$container.css({
@@ -242,6 +252,26 @@ export default class LargeVideoManager {
const tracks = state['features/base/tracks'];
const videoTrack = getVideoTrackByParticipant(tracks, participant);
// Remove track streaming status listener from the old track and add it to the new track,
// in order to stop updating track streaming status for the old track and start it for the new track.
// TODO: when this class is converted to a function react component,
// use a custom hook to update a local track streaming status.
if (this.videoTrack?.jitsiTrack?.getSourceName() !== videoTrack?.jitsiTrack?.getSourceName()) {
if (this.videoTrack) {
this.videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
APP.store.dispatch(trackStreamingStatusChanged(this.videoTrack.jitsiTrack,
this.videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
if (videoTrack && !videoTrack.local) {
this.videoTrack = videoTrack;
this.videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
APP.store.dispatch(trackStreamingStatusChanged(this.videoTrack.jitsiTrack,
this.videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
isVideoRenderable = !isVideoMuted && (
APP.conference.isLocalId(id)
|| participant?.isLocalScreenShare
@@ -340,6 +370,19 @@ export default class LargeVideoManager {
});
}
/**
* Handle track streaming status change event by
* by dispatching an action to update track streaming status for the given track in app state.
*
* @param {JitsiTrack} jitsiTrack the track with streaming status updated
* @param {JitsiTrackStreamingStatus} streamingStatus the updated track streaming status
*
* @private
*/
handleTrackStreamingStatusChanged(jitsiTrack, streamingStatus) {
APP.store.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
}
/**
* Shows/hides notification about participant's connectivity issues to be
* shown on the large video area.

View File

@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { FILMSTRIP_BREAKPOINT, shouldDisplayStageFilmstrip } from '../../../react/features/filmstrip';
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip';
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
@@ -414,7 +414,7 @@ export class VideoContainer extends LargeContainer {
const verticalFilmstripWidth = state['features/filmstrip'].width?.current;
if (currentLayout === LAYOUTS.TILE_VIEW || shouldDisplayStageFilmstrip(state)) {
if (currentLayout === LAYOUTS.TILE_VIEW || currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW) {
// We don't need to resize the large video since it won't be displayed and we'll resize when returning back
// to stage view.
return;

50
package-lock.json generated
View File

@@ -72,7 +72,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1418.0.0+da6d04c2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1425.0.0+6b629a19/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.2",
@@ -6017,9 +6017,9 @@
}
},
"node_modules/async": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
"integrity": "sha1-rDYTsdqb7RtHUQu0ZRuJMeRxRsc="
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"node_modules/async-limiter": {
"version": "1.0.1",
@@ -11796,15 +11796,15 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1418.0.0+da6d04c2/lib-jitsi-meet.tgz",
"integrity": "sha512-btrR77ByMsfSikU4o8BXZ643eHsE7OqGtCS4kF8Q8/Ife+26kYa7eqccKwdtJHOWKMZNPTTb0HNssY+N1O2erA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1425.0.0+6b629a19/lib-jitsi-meet.tgz",
"integrity": "sha512-oqWGJv62jBTtGsAA1ZkBrkuzLYxAOpA/ppZ5kisy54boWbCh8/GVOfmf/OwkDrUj7iBkjlh/qRU3DUlz9l0pMw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "0.9.0",
"async": "3.2.3",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"lodash.clonedeep": "4.5.0",
@@ -12503,9 +12503,9 @@
}
},
"node_modules/metro/node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -13889,9 +13889,9 @@
}
},
"node_modules/portfinder/node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -24419,9 +24419,9 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
"integrity": "sha1-rDYTsdqb7RtHUQu0ZRuJMeRxRsc="
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"async-limiter": {
"version": "1.0.1",
@@ -28899,14 +28899,14 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1418.0.0+da6d04c2/lib-jitsi-meet.tgz",
"integrity": "sha512-btrR77ByMsfSikU4o8BXZ643eHsE7OqGtCS4kF8Q8/Ife+26kYa7eqccKwdtJHOWKMZNPTTb0HNssY+N1O2erA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1425.0.0+6b629a19/lib-jitsi-meet.tgz",
"integrity": "sha512-oqWGJv62jBTtGsAA1ZkBrkuzLYxAOpA/ppZ5kisy54boWbCh8/GVOfmf/OwkDrUj7iBkjlh/qRU3DUlz9l0pMw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "0.9.0",
"async": "3.2.3",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"lodash.clonedeep": "4.5.0",
@@ -29247,9 +29247,9 @@
}
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
@@ -30535,9 +30535,9 @@
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"

View File

@@ -77,7 +77,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1418.0.0+da6d04c2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1425.0.0+6b629a19/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.2",

View File

@@ -68,11 +68,12 @@ export default class AudioMuteButton extends Component<Props, State> {
Promise.all([
api.isAudioAvailable(),
api.isAudioMuted()
api.isAudioMuted(),
api.isAudioDisabled?.() || Promise.resolve(false)
])
.then(([ audioAvailable, audioMuted ]) =>
.then(([ audioAvailable, audioMuted, audioDisabled ]) =>
this.setState({
audioAvailable,
audioAvailable: audioAvailable && !audioDisabled,
audioMuted
}))
.catch(console.error);

View File

@@ -59,6 +59,7 @@ const styles = () => {
color: 'rgba(255, 255, 255, 1)',
fontWeight: '100',
objectFit: 'cover',
textAlign: 'center',
'&.avatar-small': {
height: '28px !important',

View File

@@ -49,15 +49,14 @@ const _updateLastN = debounce(({ dispatch, getState }) => {
const { appState } = state['features/background'] || {};
const { enabled: filmStripEnabled } = state['features/filmstrip'];
const config = state['features/base/config'];
const { lastNLimits, lastN } = state['features/base/lastn'];
const { lastNLimits } = state['features/base/lastn'];
const participantCount = getParticipantCount(state);
// Select the lastN value based on the following preference order.
// 1. The last-n value in redux.
// 2. The last-n value from 'startLastN' if it is specified in config.js
// 3. The last-n value from 'channelLastN' if specified in config.js.
// 4. -1 as the default value.
let lastNSelected = lastN || (config.startLastN ?? (config.channelLastN ?? -1));
// Select the (initial) lastN value based on the following preference order.
// 1. The last-n value from 'startLastN' if it is specified in config.js
// 2. The last-n value from 'channelLastN' if specified in config.js.
// 3. -1 as the default value.
let lastNSelected = config.startLastN ?? (config.channelLastN ?? -1);
// Apply last N limit based on the # of participants and config settings.
const limitedLastN = limitLastN(participantCount, lastNLimits);

View File

@@ -37,6 +37,11 @@ type Props = {
*/
hasTabNavigator?: boolean,
/**
* Insets for the SafeAreaView.
*/
safeAreaInsets?: Array,
/**
* Additional style to be appended to the KeyboardAvoidingView containing the content of the modal.
*/
@@ -49,6 +54,7 @@ const JitsiScreen = ({
footerComponent,
hasTabNavigator = false,
hasBottomTextInput = false,
safeAreaInsets = [ 'bottom', 'left', 'right' ],
style
}: Props) => (
<View
@@ -59,11 +65,7 @@ const JitsiScreen = ({
hasTabNavigator = { hasTabNavigator }
style = { style }>
<SafeAreaView
edges = { [
'bottom',
'left',
'right'
] }
edges = { safeAreaInsets }
style = { styles.safeArea }>
{ children }
</SafeAreaView>

View File

@@ -3,7 +3,8 @@
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { i18next } from '../../base/i18n';
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
@@ -299,8 +300,9 @@ export function getParticipantDisplayName(stateful: Object | Function, id: strin
*/
export function getScreenshareParticipantDisplayName(stateful: Object | Function, id: string) {
const owner = getParticipantById(stateful, getFakeScreenShareParticipantOwnerId(id));
const name = owner.name;
return `${owner.name}'s screen`;
return i18next.t('screenshareDisplayName', { name });
}
/**
@@ -370,7 +372,7 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
export function getPinnedParticipant(stateful: Object | Function) {
const state = toState(stateful);
const { pinnedParticipant } = state['features/base/participants'];
const stageFilmstrip = isStageFilmstripEnabled(state);
const stageFilmstrip = isStageFilmstripAvailable(state);
if (stageFilmstrip) {
const { activeParticipants } = state['features/filmstrip'];

View File

@@ -210,14 +210,22 @@ MiddlewareRegistry.register(store => next => action => {
}
case PARTICIPANT_JOINED: {
_maybePlaySounds(store, action);
const { isFakeScreenShareParticipant } = action.participant;
// Do not play sounds when a fake participant tile is created for screenshare.
!isFakeScreenShareParticipant && _maybePlaySounds(store, action);
return _participantJoinedOrUpdated(store, next, action);
}
case PARTICIPANT_LEFT:
_maybePlaySounds(store, action);
case PARTICIPANT_LEFT: {
const { isFakeScreenShareParticipant } = action.participant;
// Do not play sounds when a tile for screenshare is removed.
!isFakeScreenShareParticipant && _maybePlaySounds(store, action);
break;
}
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);

View File

@@ -6,6 +6,7 @@ import { connect } from '../../../../base/redux';
import DeviceStatus from '../../../../prejoin/components/preview/DeviceStatus';
import { Toolbox } from '../../../../toolbox/components/web';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
import { getToolbarButtons, isToolbarButtonEnabled } from '../../../config/functions.web';
import ConnectionStatus from './ConnectionStatus';
import Preview from './Preview';
@@ -147,14 +148,23 @@ class PreMeetingScreen extends PureComponent<Props> {
* @returns {Object}
*/
function mapStateToProps(state, ownProps): Object {
const hideButtons = state['features/base/config'].hiddenPremeetingButtons || [];
const premeetingButtons = ownProps.thirdParty
const { hiddenPremeetingButtons } = state['features/base/config'];
const toolbarButtons = getToolbarButtons(state);
const premeetingButtons = (ownProps.thirdParty
? THIRD_PARTY_PREJOIN_BUTTONS
: PREMEETING_BUTTONS;
: PREMEETING_BUTTONS).filter(b => !(hiddenPremeetingButtons || []).includes(b));
const { premeetingBackground } = state['features/dynamic-branding'];
return {
_buttons: premeetingButtons.filter(b => !hideButtons.includes(b)),
// For keeping backwards compat.: if we pass an empty hiddenPremeetingButtons
// array through external api, we have all prejoin buttons present on premeeting
// screen regardless of passed values into toolbarButtons config overwrite.
// If hiddenPremeetingButtons is missing, we hide the buttons according to
// toolbarButtons config overwrite.
_buttons: hiddenPremeetingButtons
? premeetingButtons
: premeetingButtons.filter(b => isToolbarButtonEnabled(b, toolbarButtons)),
_premeetingBackground: premeetingBackground
};
}

View File

@@ -5,6 +5,7 @@ import { batch } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
@@ -331,6 +332,7 @@ StateListenerRegistry.register(
for (const track of remoteTracks) {
dispatch(trackRemoved(track.jitsiTrack));
}
dispatch({ type: _RESET_BREAKOUT_ROOMS });
});
}
});

View File

@@ -183,7 +183,7 @@ class Conference extends AbstractConference<Props, State> {
const { _showLobby } = this.props;
if (!prevProps._showLobby && _showLobby) {
navigate(screen.lobby);
navigate(screen.lobby.root);
}
if (prevProps._showLobby && !_showLobby) {

View File

@@ -1,6 +1,5 @@
// @flow
import clsx from 'clsx';
import _ from 'lodash';
import React from 'react';
@@ -12,7 +11,7 @@ import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { MainFilmstrip, StageFilmstrip, shouldDisplayStageFilmstrip } from '../../../filmstrip';
import { MainFilmstrip, StageFilmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
@@ -59,7 +58,8 @@ const FULL_SCREEN_EVENTS = [
export const LAYOUT_CLASSNAMES = {
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
[LAYOUTS.TILE_VIEW]: 'tile-view',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip',
[LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip'
};
/**
@@ -103,11 +103,6 @@ type Props = AbstractProps & {
*/
_showPrejoin: boolean,
/**
* Whether or not the stage filmstrip should be displayed.
*/
_showStageFilmstrip: boolean,
dispatch: Function,
t: Function
}
@@ -220,8 +215,7 @@ class Conference extends AbstractConference<Props, *> {
_notificationsVisible,
_overflowDrawer,
_showLobby,
_showPrejoin,
_showStageFilmstrip
_showPrejoin
} = this.props;
return (
@@ -233,7 +227,7 @@ class Conference extends AbstractConference<Props, *> {
ref = { this._setBackground }>
<Chat />
<div
className = { clsx(_layoutClassName, _showStageFilmstrip && 'stage-filmstrip') }
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
@@ -242,7 +236,7 @@ class Conference extends AbstractConference<Props, *> {
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo />
{_showStageFilmstrip && <StageFilmstrip />}
<StageFilmstrip />
<MainFilmstrip />
</div>
@@ -402,8 +396,7 @@ function _mapStateToProps(state) {
_overflowDrawer: overflowDrawer,
_roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state),
_showStageFilmstrip: shouldDisplayStageFilmstrip(state)
_showPrejoin: isPrejoinPageVisible(state)
};
}

View File

@@ -52,12 +52,14 @@ export const ConnectionIndicatorIcon = ({
}: Props) => {
const sourceNameSignalingEnabled = useSelector(state => getSourceNameSignalingFeatureFlag(state));
const dispatch = useDispatch();
const sourceName = track?.jitsiTrack?.getSourceName?.();
const sourceName = track?.jitsiTrack?.getSourceName();
const handleTrackStreamingStatusChanged = streamingStatus => {
dispatch(trackStreamingStatusChanged(track.jitsiTrack, streamingStatus));
};
// TODO: replace this with a custom hook to be reused where track streaming status is needed.
// TODO: In the hood the listener should updates a local track streaming status instead of that in redux store.
useEffect(() => {
if (track && !track.local && sourceNameSignalingEnabled) {
track.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED, handleTrackStreamingStatusChanged);

View File

@@ -5,6 +5,8 @@ import {
setAudioOutputDeviceId,
setVideoInputDevice
} from '../base/devices';
import { isIosMobileBrowser } from '../base/environment/utils';
import { browser } from '../base/lib-jitsi-meet';
import { updateSettings } from '../base/settings';
import { getDeviceSelectionDialogProps } from './functions';
@@ -17,33 +19,34 @@ import logger from './logger';
* @returns {Function}
*/
export function submitDeviceSelectionTab(newState) {
// Always use the new track for mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30. The
// old track is stopped by the browser when a new track is created for preview so it needs to be replaced even if
// the device selection doesn't change.
const replaceTrackAlways = isIosMobileBrowser() && browser.isVersionGreaterThan('15.3');
return (dispatch, getState) => {
const currentState = getDeviceSelectionDialogProps(getState());
if (newState.selectedVideoInputId
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
if ((newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId))
|| replaceTrackAlways) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(
setVideoInputDevice(newState.selectedVideoInputId));
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
}
if (newState.selectedAudioInputId
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
if ((newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId)
|| replaceTrackAlways) {
dispatch(updateSettings({
userSelectedMicDeviceId: newState.selectedAudioInputId,
userSelectedMicDeviceLabel:
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
}));
dispatch(
setAudioInputDevice(newState.selectedAudioInputId));
dispatch(setAudioInputDevice(newState.selectedAudioInputId));
}
if (newState.selectedAudioOutputId

View File

@@ -13,7 +13,6 @@ import {
setAudioOutputDevice,
setVideoInputDeviceAndUpdateSettings
} from '../base/devices';
import { isIosMobileBrowser } from '../base/environment/utils';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux';
import {
@@ -34,16 +33,15 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
const settings = state['features/base/settings'];
const { conference } = state['features/base/conference'];
const { permissions } = state['features/base/devices'];
const isMobileSafari = isIosMobileBrowser();
const cameraChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
const userSelectedMic = getUserSelectedMicDeviceId(state);
let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
let disableVideoInputSelect = !cameraChangeSupported;
let selectedAudioInputId = isMobileSafari ? userSelectedMic : settings.micDeviceId;
let selectedAudioInputId = settings.micDeviceId;
let selectedAudioOutputId = getAudioOutputDeviceId();
let selectedVideoInputId = isMobileSafari ? userSelectedCamera : settings.cameraDeviceId;
let selectedVideoInputId = settings.cameraDeviceId;
// audio input change will be a problem only when we are in a
// conference and this is not supported, when we open device selection on

View File

@@ -36,11 +36,6 @@ type Props = {
*/
allowEditing: boolean,
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Invoked to update the participant's display name.
*/
@@ -70,7 +65,12 @@ type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function
t: Function,
/**
* The type of thumbnail.
*/
thumbnailType: string
};
/**
@@ -183,11 +183,11 @@ class DisplayName extends Component<Props, State> {
const {
_nameToDisplay,
allowEditing,
currentLayout,
displayNameSuffix,
classes,
elementID,
t
t,
thumbnailType
} = this.props;
if (allowEditing && this.state.isEditing) {
@@ -211,7 +211,7 @@ class DisplayName extends Component<Props, State> {
return (
<Tooltip
content = { appendSuffix(_nameToDisplay, displayNameSuffix) }
position = { getIndicatorsTooltipPosition(currentLayout) }>
position = { getIndicatorsTooltipPosition(thumbnailType) }>
<span
className = { `displayname ${classes.displayName}` }
id = { elementID }

View File

@@ -171,7 +171,6 @@ export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS';
*/
export const SET_MAX_STAGE_PARTICIPANTS = 'SET_MAX_STAGE_PARTICIPANTS';
/**
* The type of Redux action which toggles the pin state of stage participants.
* {
@@ -180,3 +179,11 @@ export const SET_MAX_STAGE_PARTICIPANTS = 'SET_MAX_STAGE_PARTICIPANTS';
* }
*/
export const TOGGLE_PIN_STAGE_PARTICIPANT = 'TOGGLE_PIN_STAGE_PARTICIPANT';
/**
* The type of Redux action which clears the list of stage participants.
* {
* type: CLEAR_STAGE_PARTICIPANTS
* }
*/
export const CLEAR_STAGE_PARTICIPANTS = 'CLEAR_STAGE_PARTICIPANTS';

View File

@@ -24,7 +24,8 @@ import {
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
TOGGLE_PIN_STAGE_PARTICIPANT
TOGGLE_PIN_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
@@ -83,14 +84,15 @@ export function setTileViewDimensions() {
disableTileEnlargement,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles
desiredNumberOfVisibleTiles: numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
const hasScroll = availableHeight < thumbnailsTotalHeight;
const filmstripWidth
= Math.min(clientWidth - TILE_VIEW_GRID_HORIZONTAL_MARGIN, columns * (TILE_HORIZONTAL_MARGIN + width))
+ (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN, thumbnailsTotalHeight);
const filmstripHeight = Math.min(availableHeight, thumbnailsTotalHeight);
dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
@@ -139,7 +141,11 @@ export function setVerticalViewDimensions() {
const { tileView = {} } = state['features/base/config'];
const { numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES } = tileView;
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
const maxColumns = getMaxColumnCount(state);
const maxColumns = getMaxColumnCount(state, {
width: filmstripWidth.current,
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const {
height,
width,
@@ -152,7 +158,7 @@ export function setVerticalViewDimensions() {
maxColumns,
noHorizontalContainerMargin: true,
numberOfParticipants,
numberOfVisibleTiles
desiredNumberOfVisibleTiles: numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
@@ -261,32 +267,33 @@ export function setStageFilmstripViewDimensions() {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const {
disableResponsiveTiles,
disableTileEnlargement,
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
const maxColumns = getMaxColumnCount(state);
const availableWidth = clientWidth - verticalWidth;
const maxColumns = getMaxColumnCount(state, {
width: availableWidth,
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const {
height,
width,
columns,
rows
} = disableResponsiveTiles
? calculateNonResponsiveTileViewDimensions(state, true)
: calculateResponsiveTileViewDimensions({
clientWidth: clientWidth - verticalWidth,
clientHeight,
disableTileEnlargement,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
} = calculateResponsiveTileViewDimensions({
clientWidth: availableWidth,
clientHeight,
disableTileEnlargement: false,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth
@@ -463,3 +470,14 @@ export function togglePinStageParticipant(participantId) {
participantId
};
}
/**
* Clears the stage participants list.
*
* @returns {Object}
*/
export function clearStageParticipants() {
return {
type: CLEAR_STAGE_PARTICIPANTS
};
}

View File

@@ -20,7 +20,7 @@ import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS } from '../../../video-layout';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
setFilmstripVisible,
setVisibleRemoteParticipants,
@@ -110,7 +110,7 @@ type Props = {
/**
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
*/
_localScreenShare: Object,
_localScreenShare: Object,
/**
* The maximum width of the vertical filmstrip.
@@ -318,32 +318,29 @@ class Filmstrip extends PureComponent <Props, State> {
const { isMouseDown } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && _stageFilmstrip) {
if (_visible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && !_stageFilmstrip)) {
filmstripStyle.maxWidth = _verticalViewMaxWidth;
if (!_visible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
break;
}
case LAYOUTS.TILE_VIEW: {
if (_stageFilmstrip && _visible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
break;
}
}
let toolbar = null;
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled && _currentLayout !== LAYOUTS.TILE_VIEW) {
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
&& _currentLayout !== LAYOUTS.TILE_VIEW && !_stageFilmstrip) {
toolbar = this._renderToggleButton();
}
const filmstrip = (<>
<div
className = { clsx(this.props._videosClassName,
!tileViewActive && !_resizableFilmstrip && 'filmstrip-hover',
!tileViewActive && !_stageFilmstrip && !_resizableFilmstrip && 'filmstrip-hover',
_verticalViewGrid && 'vertical-view-grid') }
id = 'remoteVideos'>
{!_disableSelfView && !_verticalViewGrid && (
@@ -351,7 +348,7 @@ class Filmstrip extends PureComponent <Props, State> {
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
{
!tileViewActive && <div id = 'filmstripLocalVideoThumbnail'>
!tileViewActive && !_stageFilmstrip && <div id = 'filmstripLocalVideoThumbnail'>
<Thumbnail
key = 'local' />
</div>
@@ -364,7 +361,7 @@ class Filmstrip extends PureComponent <Props, State> {
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && <Thumbnail
!tileViewActive && !_stageFilmstrip && <Thumbnail
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
@@ -604,6 +601,7 @@ class Filmstrip extends PureComponent <Props, State> {
_filmstripHeight,
_filmstripWidth,
_hasScroll,
_isVerticalFilmstrip,
_remoteParticipantsLength,
_resizableFilmstrip,
_rows,
@@ -619,7 +617,7 @@ class Filmstrip extends PureComponent <Props, State> {
return null;
}
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || _stageFilmstrip) {
return (
<FixedSizeGrid
className = 'filmstrip__videos remote-videos'
@@ -669,7 +667,7 @@ class Filmstrip extends PureComponent <Props, State> {
props.className += ' is-not-overflowing';
}
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
} else if (_isVerticalFilmstrip) {
const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
const isNotOverflowing = !_hasScroll;
@@ -820,15 +818,20 @@ function _mapStateToProps(state, ownProps) {
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
const _currentLayout = getCurrentLayout(state);
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (!ownProps._stageFilmstrip && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
return {
_className: className,
_chatOpen: state['features/chat'].isOpen,
_currentLayout,
_disableSelfView: disableSelfView,
_hasScroll,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_isVerticalFilmstrip,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_thumbnailsReordered: enableThumbnailReordering,

View File

@@ -17,11 +17,6 @@ import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
@@ -143,7 +138,8 @@ function _mapStateToProps(state) {
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && (
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
isMobileBrowser() || (_currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW
&& _currentLayout !== LAYOUTS.STAGE_FILMSTRIP_VIEW));
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
@@ -154,7 +150,8 @@ function _mapStateToProps(state) {
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.STAGE_FILMSTRIP_VIEW: {
const {
remote,
remoteVideosContainer,
@@ -189,7 +186,6 @@ function _mapStateToProps(state) {
return {
_columns: gridDimensions.columns,
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,

View File

@@ -92,11 +92,10 @@ type Props = {
_visible: boolean
};
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW && (
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
_currentLayout = { LAYOUTS.TILE_VIEW }
_stageFilmstrip = { true } />
</span>
);

View File

@@ -6,7 +6,6 @@ import { MEDIA_TYPE } from '../../../base/media';
import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
import { getCurrentLayout } from '../../../video-layout';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import AudioMutedIndicator from './AudioMutedIndicator';
@@ -20,11 +19,6 @@ declare var interfaceConfig: Object;
*/
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* Indicates if the audio muted indicator should be visible or not.
*/
@@ -43,7 +37,12 @@ type Props = {
/**
* The ID of the participant for which the status bar is rendered.
*/
participantID: String
participantID: String,
/**
* The type of thumbnail.
*/
thumbnailType: string
};
/**
@@ -60,12 +59,12 @@ class StatusIndicators extends Component<Props> {
*/
render() {
const {
_currentLayout,
_showAudioMutedIndicator,
_showModeratorIndicator,
_showScreenShareIndicator
_showScreenShareIndicator,
thumbnailType
} = this.props;
const tooltipPosition = getIndicatorsTooltipPosition(_currentLayout);
const tooltipPosition = getIndicatorsTooltipPosition(thumbnailType);
return (
<>
@@ -111,7 +110,6 @@ function _mapStateToProps(state, ownProps) {
const { disableModeratorIndicator } = state['features/base/config'];
return {
_currentLayout: getCurrentLayout(state),
_showAudioMutedIndicator: isAudioMuted && audio,
_showModeratorIndicator:
!disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR && moderator,

View File

@@ -9,6 +9,7 @@ import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics
import { Avatar } from '../../../base/avatar';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
getLocalParticipant,
@@ -24,7 +25,8 @@ import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack,
updateLastTrackVideoMediaEvent
updateLastTrackVideoMediaEvent,
trackStreamingStatusChanged
} from '../../../base/tracks';
import { getVideoObjectPosition } from '../../../face-landmarks/functions';
import { hideGif, showGif } from '../../../gifs/actions';
@@ -36,16 +38,17 @@ import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER,
THUMBNAIL_TYPE,
VIDEO_TEST_EVENTS
} from '../../constants';
import {
computeDisplayModeFromInput,
getActiveParticipantsIds,
getDisplayModeInput,
getThumbnailTypeFromLayout,
isVideoPlayable,
showGridInVerticalView,
isStageFilmstripEnabled,
shouldDisplayStageFilmstrip
isStageFilmstripAvailable,
showGridInVerticalView
} from '../../functions';
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
@@ -53,6 +56,7 @@ import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
declare var interfaceConfig: Object;
/**
@@ -91,11 +95,6 @@ export type Props = {|
*/
_audioTrack: ?Object,
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* Indicates whether the local video flip feature is disabled or not.
*/
@@ -189,9 +188,9 @@ export type Props = {|
_raisedHand: boolean,
/**
* Whether or not the stage filmstrip is disabled.
* Whether or not the current layout is stage filmstrip layout.
*/
_stageFilmstripDisabled: boolean,
_stageFilmstripLayout: boolean,
/**
* Whether or not the participants are displayed on stage.
@@ -200,6 +199,11 @@ export type Props = {|
*/
_stageParticipantsVisible: boolean,
/**
* The type of thumbnail to display.
*/
_thumbnailType: string,
/**
* The video object position for the participant.
*/
@@ -243,7 +247,12 @@ export type Props = {|
/**
* Styles that will be set to the Thumbnail's main span element.
*/
style?: ?Object
style?: ?Object,
/**
* Whether source name signaling is enabled.
*/
_sourceNameSignalingEnabled: boolean
|};
const defaultStyles = theme => {
@@ -403,6 +412,7 @@ class Thumbnail extends Component<Props, State> {
this._hidePopover = this._hidePopover.bind(this);
this._onGifMouseEnter = this._onGifMouseEnter.bind(this);
this._onGifMouseLeave = this._onGifMouseLeave.bind(this);
this.handleTrackStreamingStatusChanged = this.handleTrackStreamingStatusChanged.bind(this);
}
/**
@@ -413,6 +423,38 @@ class Thumbnail extends Component<Props, State> {
*/
componentDidMount() {
this._onDisplayModeChanged();
// Listen to track streaming status changed event to keep it updated.
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
/**
* Remove listeners for track streaming status update.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
/**
@@ -426,6 +468,38 @@ class Thumbnail extends Component<Props, State> {
if (prevState.displayMode !== this.state.displayMode) {
this._onDisplayModeChanged();
}
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
if (_sourceNameSignalingEnabled
&& prevProps._videoTrack?.jitsiTrack?.getSourceName() !== _videoTrack?.jitsiTrack?.getSourceName()) {
if (prevProps._videoTrack && !prevProps._videoTrack.local) {
prevProps._videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(prevProps._videoTrack.jitsiTrack,
prevProps._videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
if (_videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
}
/**
* Handle track streaming status change event by
* by dispatching an action to update track streaming status for the given track in app state.
*
* @param {JitsiTrack} jitsiTrack - The track with streaming status updated.
* @param {JitsiTrackStreamingStatus} streamingStatus - The updated track streaming status.
* @returns {void}
*/
handleTrackStreamingStatusChanged(jitsiTrack, streamingStatus) {
this.props.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
}
/**
@@ -447,15 +521,15 @@ class Thumbnail extends Component<Props, State> {
*/
_maybeSendScreenSharingIssueEvents(input) {
const {
_currentLayout,
_isAudioOnly,
_isScreenSharing
_isScreenSharing,
_thumbnailType
} = this.props;
const { displayMode } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const isTileType = _thumbnailType === THUMBNAIL_TYPE.TILE;
if (!(DISPLAY_VIDEO === displayMode)
&& tileViewActive
&& isTileType
&& _isScreenSharing
&& !_isAudioOnly) {
sendAnalytics(createScreenSharingIssueEvent({
@@ -530,9 +604,9 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_hidePopover() {
const { _currentLayout } = this.props;
const { _thumbnailType } = this.props;
if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
if (_thumbnailType === THUMBNAIL_TYPE.VERTICAL) {
this.setState({
isHovered: false
});
@@ -550,13 +624,13 @@ class Thumbnail extends Component<Props, State> {
_getStyles(): Object {
const { canPlayEventReceived } = this.state;
const {
_currentLayout,
_disableTileEnlargement,
_height,
_isFakeScreenShareParticipant,
_isHidden,
_isScreenSharing,
_participant,
_thumbnailType,
_videoObjectPosition,
_videoTrack,
_width,
@@ -564,7 +638,7 @@ class Thumbnail extends Component<Props, State> {
style
} = this.props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const isTileType = _thumbnailType === THUMBNAIL_TYPE.TILE;
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
const track = jitsiVideoTrack?.track;
const isPortraitVideo = ((track && track.getSettings()?.aspectRatio) || 1) < 1;
@@ -587,7 +661,7 @@ class Thumbnail extends Component<Props, State> {
}
let videoStyles = null;
const doNotStretchVideo = (isPortraitVideo && tileViewActive)
const doNotStretchVideo = (isPortraitVideo && isTileType)
|| _disableTileEnlargement
|| _isScreenSharing;
@@ -636,13 +710,13 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_onClick() {
const { _participant, dispatch, _stageFilmstripDisabled } = this.props;
const { _participant, dispatch, _stageFilmstripLayout } = this.props;
const { id, pinned } = _participant;
if (_stageFilmstripDisabled) {
dispatch(pinParticipant(pinned ? null : id));
} else {
if (_stageFilmstripLayout) {
dispatch(togglePinStageParticipant(id));
} else {
dispatch(pinParticipant(pinned ? null : id));
}
}
@@ -790,8 +864,8 @@ class Thumbnail extends Component<Props, State> {
const {
_isDominantSpeakerDisabled,
_participant,
_currentLayout,
_raisedHand,
_thumbnailType,
classes
} = this.props;
@@ -804,7 +878,7 @@ class Thumbnail extends Component<Props, State> {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
if (_currentLayout !== LAYOUTS.TILE_VIEW && _participant?.pinned) {
if (_thumbnailType !== THUMBNAIL_TYPE.TILE && _participant?.pinned) {
className += ' videoContainerFocused';
}
@@ -902,16 +976,16 @@ class Thumbnail extends Component<Props, State> {
_renderParticipant(local = false) {
const {
_audioTrack,
_currentLayout,
_disableLocalVideoFlip,
_gifSrc,
_isMobile,
_isMobilePortrait,
_isScreenSharing,
_isTestModeEnabled,
_localFlipX,
_participant,
_thumbnailType,
_videoTrack,
_gifSrc,
classes,
stageFilmstrip
} = this.props;
@@ -975,28 +1049,28 @@ class Thumbnail extends Component<Props, State> {
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsTopContainer,
_currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
) }>
<ThumbnailTopIndicators
currentLayout = { _currentLayout }
hidePopover = { this._hidePopover }
indicatorsClassName = { classes.indicatorsBackground }
isHovered = { isHovered }
local = { local }
participantId = { id }
popoverVisible = { popoverVisible }
showPopover = { this._showPopover } />
showPopover = { this._showPopover }
thumbnailType = { _thumbnailType } />
</div>
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
_currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
currentLayout = { _currentLayout }
local = { local }
participantId = { id } />
participantId = { id }
thumbnailType = { _thumbnailType } />
</div>
{!_gifSrc && this._renderAvatar(styles.avatar) }
{ !local && (
@@ -1103,7 +1177,7 @@ function _mapStateToProps(state, ownProps): Object {
}
const _audioTrack = isLocal
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state);
const _currentLayout = getCurrentLayout(state);
let size = {};
let _isMobilePortrait = false;
const {
@@ -1116,10 +1190,12 @@ function _mapStateToProps(state, ownProps): Object {
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
const activeParticipants = getActiveParticipantsIds(state);
const tileType = getThumbnailTypeFromLayout(_currentLayout, stageFilmstrip);
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
switch (tileType) {
case THUMBNAIL_TYPE.VERTICAL:
case THUMBNAIL_TYPE.HORIZONTAL: {
const {
horizontalViewDimensions = {
local: {},
@@ -1133,7 +1209,7 @@ function _mapStateToProps(state, ownProps): Object {
} = state['features/filmstrip'];
const _verticalViewGrid = showGridInVerticalView(state);
const { local, remote }
= _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
= tileType === THUMBNAIL_TYPE.VERTICAL
? verticalViewDimensions : horizontalViewDimensions;
const { width, height } = (isLocal ? local : remote) ?? {};
@@ -1155,7 +1231,7 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.TILE_VIEW: {
case THUMBNAIL_TYPE.TILE: {
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const {
stageFilmstripDimensions = {
@@ -1186,7 +1262,6 @@ function _mapStateToProps(state, ownProps): Object {
return {
_audioTrack,
_currentLayout,
_defaultLocalDisplayName: defaultLocalDisplayName,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
_disableTileEnlargement: Boolean(disableTileEnlargement),
@@ -1204,12 +1279,14 @@ function _mapStateToProps(state, ownProps): Object {
_localFlipX: Boolean(localFlipX),
_participant: participant,
_raisedHand: hasRaisedHand(participant),
_stageFilmstripDisabled: !isStageFilmstripEnabled(state),
_stageParticipantsVisible: shouldDisplayStageFilmstrip(state, 1),
_stageFilmstripLayout: isStageFilmstripAvailable(state),
_stageParticipantsVisible: _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW,
_thumbnailType: tileType,
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
_videoTrack,
...size,
_gifSrc: mode === 'chat' ? null : gifSrc
_gifSrc: mode === 'chat' ? null : gifSrc,
_sourceNameSignalingEnabled: sourceNameSignalingEnabled
};
}

View File

@@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
import { isDisplayNameVisible, isNameReadOnly } from '../../../base/config/functions.any';
import DisplayName from '../../../display-name/components/web/DisplayName';
import { LAYOUTS } from '../../../video-layout';
import { THUMBNAIL_TYPE } from '../../constants';
import StatusIndicators from './StatusIndicators';
@@ -14,11 +14,6 @@ declare var interfaceConfig: Object;
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Class name for indicators container.
*/
@@ -37,7 +32,12 @@ type Props = {
/**
* Whether or not to show the status indicators.
*/
showStatusIndicators: string
showStatusIndicators: string,
/**
* The type of thumbnail.
*/
thumbnailType: string
}
const useStyles = makeStyles(() => {
@@ -61,10 +61,10 @@ const useStyles = makeStyles(() => {
const ThumbnailBottomIndicators = ({
className,
currentLayout,
local,
participantId,
showStatusIndicators = true
showStatusIndicators = true,
thumbnailType
}: Props) => {
const styles = useStyles();
const _allowEditing = !useSelector(isNameReadOnly);
@@ -77,17 +77,18 @@ const ThumbnailBottomIndicators = ({
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
screenshare = { thumbnailType === THUMBNAIL_TYPE.TILE }
thumbnailType = { thumbnailType } />
}
{
_showDisplayName && (
<span className = { styles.nameContainer }>
<DisplayName
allowEditing = { local ? _allowEditing : false }
currentLayout = { currentLayout }
displayNameSuffix = { local ? _defaultLocalDisplayName : '' }
elementID = { local ? 'localDisplayName' : `participant_${participantId}_name` }
participantID = { participantId } />
participantID = { participantId }
thumbnailType = { thumbnailType } />
</span>
)
}

View File

@@ -8,8 +8,7 @@ import { useSelector } from 'react-redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
import { LAYOUTS } from '../../../video-layout';
import { STATS_POPOVER_POSITION } from '../../constants';
import { STATS_POPOVER_POSITION, THUMBNAIL_TYPE } from '../../constants';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import PinnedIndicator from './PinnedIndicator';
@@ -21,11 +20,6 @@ declare var interfaceConfig: Object;
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Hide popover callback.
*/
@@ -64,7 +58,12 @@ type Props = {
/**
* Show popover callback.
*/
showPopover: Function
showPopover: Function,
/**
* The type of thumbnail.
*/
thumbnailType: string
}
const useStyles = makeStyles(() => {
@@ -80,7 +79,6 @@ const useStyles = makeStyles(() => {
});
const ThumbnailTopIndicators = ({
currentLayout,
hidePopover,
indicatorsClassName,
isFakeScreenShareParticipant,
@@ -88,7 +86,8 @@ const ThumbnailTopIndicators = ({
local,
participantId,
popoverVisible,
showPopover
showPopover,
thumbnailType
}: Props) => {
const styles = useStyles();
@@ -111,32 +110,34 @@ const ThumbnailTopIndicators = ({
enableStatsDisplay = { true }
iconSize = { _indicatorIconSize }
participantId = { participantId }
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
statsPopoverPosition = { STATS_POPOVER_POSITION[thumbnailType] } />
}
</div>
);
}
const tooltipPosition = getIndicatorsTooltipPosition(thumbnailType);
return (
<>
<div className = { styles.container }>
<PinnedIndicator
iconSize = { _indicatorIconSize }
participantId = { participantId }
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
tooltipPosition = { tooltipPosition } />
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { _indicatorIconSize }
participantId = { participantId }
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
statsPopoverPosition = { STATS_POPOVER_POSITION[thumbnailType] } />
}
<RaisedHandIndicator
iconSize = { _indicatorIconSize }
participantId = { participantId }
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
{currentLayout !== LAYOUTS.TILE_VIEW && (
tooltipPosition = { tooltipPosition } />
{thumbnailType !== THUMBNAIL_TYPE.TILE && (
<div className = { clsx(indicatorsClassName, 'top-indicators') }>
<StatusIndicators
participantID = { participantId }
@@ -146,12 +147,12 @@ const ThumbnailTopIndicators = ({
</div>
<div className = { styles.container }>
<VideoMenuTriggerButton
currentLayout = { currentLayout }
hidePopover = { hidePopover }
local = { local }
participantId = { participantId }
popoverVisible = { popoverVisible }
showPopover = { showPopover }
thumbnailType = { thumbnailType }
visible = { isHovered } />
</div>
</>);

View File

@@ -6,11 +6,6 @@ import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../..
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Hide popover callback.
*/
@@ -36,6 +31,11 @@ type Props = {
*/
showPopover: Function,
/**
* The type of thumbnail.
*/
thumbnailType: string,
/**
* Whether or not the component is visible.
*/
@@ -44,33 +44,33 @@ type Props = {
// eslint-disable-next-line no-confusing-arrow
const VideoMenuTriggerButton = ({
currentLayout,
hidePopover,
local,
participantId,
popoverVisible,
showPopover,
thumbnailType,
visible
}: Props) => local
? (
<span id = 'localvideomenu'>
<LocalVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
showPopover = { showPopover }
thumbnailType = { thumbnailType } />
</span>
)
: (
<span id = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
participantID = { participantId }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
showPopover = { showPopover }
thumbnailType = { thumbnailType } />
</span>
);

View File

@@ -1,7 +1,6 @@
// @flow
import { BoxModel } from '../base/styles';
import { LAYOUTS } from '../video-layout/constants';
/**
* The size (height and width) of the small (not tile view) thumbnails.
@@ -228,22 +227,31 @@ export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
*/
export const TILE_MARGIN = 10;
/**
* The types of thumbnails for filmstrip.
*/
export const THUMBNAIL_TYPE = {
TILE: 'TILE',
VERTICAL: 'VERTICAL',
HORIZONTAL: 'HORIZONTAL'
};
/**
* The popover position for the connection stats table.
*/
export const STATS_POPOVER_POSITION = {
[LAYOUTS.TILE_VIEW]: 'right-start',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left-start',
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top-end'
[THUMBNAIL_TYPE.TILE]: 'right-start',
[THUMBNAIL_TYPE.VERTICAL]: 'left-start',
[THUMBNAIL_TYPE.HORIZONTAL]: 'top-end'
};
/**
* The tooltip position for the indicators on the thumbnail.
*/
export const INDICATORS_TOOLTIP_POSITION = {
[LAYOUTS.TILE_VIEW]: 'right',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left',
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top'
[THUMBNAIL_TYPE.TILE]: 'right',
[THUMBNAIL_TYPE.VERTICAL]: 'left',
[THUMBNAIL_TYPE.HORIZONTAL]: 'top'
};
/**

View File

@@ -103,11 +103,20 @@ export function isReorderingEnabled(state) {
}
/**
* Whether the stage filmstrip is disabled or not.
* Whether the stage filmstrip is available or not.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isStageFilmstripAvailable() {
return false;
}
/**
* Whether the stage filmstrip is enabled.
*
* @returns {boolean}
*/
export function isStageFilmstripEnabled() {
return false;
}

View File

@@ -35,6 +35,7 @@ import {
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
THUMBNAIL_TYPE,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_LARGE,
@@ -243,18 +244,16 @@ export function getNumberOfPartipantsForTileView(state) {
* disabled.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} - The dimensions.
*/
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
export function calculateNonResponsiveTileViewDimensions(state) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableTileEnlargement } = state['features/base/config'];
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
const filmstripWidth = getVerticalViewMaxWidth(state);
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
const size = calculateThumbnailSizeForTileView({
columns: c,
minVisibleRows,
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
clientWidth,
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: true
@@ -297,7 +296,7 @@ export function calculateResponsiveTileViewDimensions({
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
}) {
let height, width;
let columns, rows;
@@ -311,12 +310,12 @@ export function calculateResponsiveTileViewDimensions({
maxArea: 0
};
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants); c++) {
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants, desiredNumberOfVisibleTiles); c++) {
const r = Math.ceil(numberOfParticipants / c);
// we want to display as much as possible tumbnails up to numberOfVisibleTiles
// we want to display as much as possible tumbnails up to desiredNumberOfVisibleTiles
const visibleRows
= numberOfParticipants <= numberOfVisibleTiles ? r : Math.floor(numberOfVisibleTiles / c);
= numberOfParticipants <= desiredNumberOfVisibleTiles ? r : Math.floor(desiredNumberOfVisibleTiles / c);
const size = calculateThumbnailSizeForTileView({
columns: c,
@@ -330,18 +329,38 @@ export function calculateResponsiveTileViewDimensions({
if (size) {
const { height: currentHeight, width: currentWidth, minHeightEnforced, maxVisibleRows } = size;
let area = currentHeight * currentWidth * Math.min(c * maxVisibleRows, numberOfParticipants);
const numberOfVisibleParticipants = Math.min(c * maxVisibleRows, numberOfParticipants);
let area = Math.round(
(currentHeight + TILE_VERTICAL_MARGIN)
* (currentWidth + TILE_HORIZONTAL_MARGIN)
* numberOfVisibleParticipants);
const currentDimensions = {
maxArea: area,
height: currentHeight,
width: currentWidth,
columns: c,
rows: r
rows: r,
numberOfVisibleParticipants
};
const { numberOfVisibleParticipants: oldNumberOfVisibleParticipants = 0 } = dimensions;
if (!minHeightEnforced && area > dimensions.maxArea) {
dimensions = currentDimensions;
} else if (minHeightEnforced && area > minHeightEnforcedDimensions.maxArea) {
if (!minHeightEnforced) {
if (area > dimensions.maxArea) {
dimensions = currentDimensions;
} else if ((area === dimensions.maxArea)
&& ((oldNumberOfVisibleParticipants > desiredNumberOfVisibleTiles
&& oldNumberOfVisibleParticipants >= numberOfParticipants)
|| (oldNumberOfVisibleParticipants < numberOfParticipants
&& numberOfVisibleParticipants <= desiredNumberOfVisibleTiles))
) { // If the area of the new candidates and the old ones are equal we preffer the one that will have
// closer number of visible participants to desiredNumberOfVisibleTiles config.
dimensions = currentDimensions;
}
} else if (minHeightEnforced && area >= minHeightEnforcedDimensions.maxArea) {
// If we choose configuration with minHeightEnforced there will be less than desiredNumberOfVisibleTiles
// visible tiles, that's why we prefer more columns when the area is the same.
minHeightEnforcedDimensions = currentDimensions;
} else if (minHeightEnforced && maxVisibleRows === 0) {
area = currentHeight * currentWidth * Math.min(c, numberOfParticipants);
@@ -400,7 +419,8 @@ export function calculateThumbnailSizeForTileView({
const minHeight = getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
const viewHeight = availableHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
const initialWidth = viewWidth / columns;
let initialHeight = viewHeight / minVisibleRows;
let minHeightEnforced = false;
@@ -417,52 +437,47 @@ export function calculateThumbnailSizeForTileView({
return;
}
const height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
const height = Math.min(aspectRatioHeight, initialHeight);
return {
height,
width: Math.floor(aspectRatio * height),
width: aspectRatio * height,
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
};
}
const initialRatio = initialWidth / initialHeight;
let height = Math.floor(initialHeight);
let height = initialHeight;
let width;
// The biggest area of the grid will be when the grid's height is equal to clientHeight or when the grid's width is
// equal to clientWidth.
if (initialRatio > aspectRatio) {
return {
height,
width: Math.floor(initialHeight * aspectRatio),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialHeight * aspectRatio;
} else if (initialRatio >= TILE_PORTRAIT_ASPECT_RATIO) {
return {
height,
width: Math.floor(initialWidth),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialWidth;
// eslint-disable-next-line no-negated-condition
} else if (!minHeightEnforced) {
height = Math.floor(initialWidth / TILE_PORTRAIT_ASPECT_RATIO);
height = initialWidth / TILE_PORTRAIT_ASPECT_RATIO;
if (height >= minHeight) {
return {
height,
width: Math.floor(initialWidth),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialWidth;
} else { // The width is so small that we can't reach the minimum height with portrait aspect ratio.
return;
}
} else {
// We can't fit that number of columns with the desired min height and aspect ratio.
return;
}
// else
// We can't fit that number of columns with the desired min height and aspect ratio.
return;
return {
height,
width,
minHeightEnforced,
maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
};
}
/**
@@ -499,6 +514,7 @@ export function computeDisplayModeFromInput(input: Object) {
canPlayEventReceived,
isRemoteParticipant,
stageParticipantsVisible,
stageFilmstrip,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
@@ -508,7 +524,7 @@ export function computeDisplayModeFromInput(input: Object) {
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant))) {
|| (stageParticipantsVisible && isActiveParticipant && !stageFilmstrip))) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@@ -540,7 +556,8 @@ export function getDisplayModeInput(props: Object, state: Object) {
_isVideoPlayable,
_participant,
_stageParticipantsVisible,
_videoTrack
_videoTrack,
stageFilmstrip
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived } = state;
@@ -558,6 +575,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
isScreenSharing: _isScreenSharing,
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
stageParticipantsVisible: _stageParticipantsVisible,
stageFilmstrip,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@@ -565,11 +583,11 @@ export function getDisplayModeInput(props: Object, state: Object) {
/**
* Gets the tooltip position for the thumbnail indicators.
*
* @param {string} currentLayout - The current layout of the app.
* @param {string} thumbnailType - The current thumbnail type.
* @returns {string}
*/
export function getIndicatorsTooltipPosition(currentLayout: string) {
return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top';
export function getIndicatorsTooltipPosition(thumbnailType: string) {
return INDICATORS_TOOLTIP_POSITION[thumbnailType] || 'top';
}
/**
@@ -583,7 +601,7 @@ export function isFilmstripResizable(state: Object) {
const _currentLayout = getCurrentLayout(state);
return !filmstrip?.disableResizable && !isMobileBrowser()
&& _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
&& (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
}
/**
@@ -649,7 +667,8 @@ export function isFilmstripScrollVisible(state) {
case LAYOUTS.TILE_VIEW:
({ hasScroll = false } = state['features/filmstrip'].tileViewDimensions);
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.STAGE_FILMSTRIP_VIEW: {
({ hasScroll = false } = state['features/filmstrip'].verticalViewDimensions);
break;
}
@@ -687,21 +706,20 @@ export function getPinnedActiveParticipants(state) {
}
/**
* Get whether or not the stage filmstrip should be displayed.
* Get whether or not the stage filmstrip is available (enabled & can be used).
*
* @param {Object} state - Redux state.
* @param {number} minParticipantCount - The min number of participants for the stage filmstrip
* to be displayed.
* @returns {boolean}
*/
export function shouldDisplayStageFilmstrip(state, minParticipantCount = 2) {
export function isStageFilmstripAvailable(state, minParticipantCount = 0) {
const { activeParticipants } = state['features/filmstrip'];
const { remoteScreenShares } = state['features/video-layout'];
const currentLayout = getCurrentLayout(state);
const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
return isStageFilmstripEnabled(state) && remoteScreenShares.length === 0 && !sharedVideo
&& activeParticipants.length >= minParticipantCount && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
&& activeParticipants.length >= minParticipantCount;
}
/**
@@ -713,5 +731,29 @@ export function shouldDisplayStageFilmstrip(state, minParticipantCount = 2) {
export function isStageFilmstripEnabled(state) {
const { filmstrip } = state['features/base/config'];
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
return !(filmstrip?.disableStageFilmstrip ?? true) && interfaceConfig.VERTICAL_FILMSTRIP;
}
/**
* Gets the thumbnail type by filmstrip type.
*
* @param {string} currentLayout - Current app layout.
* @param {boolean} isStageFilmstrip - Whether the filmstrip is stage filmstrip or not.
* @returns {string}
*/
export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = false) {
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
return THUMBNAIL_TYPE.TILE;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.VERTICAL;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.HORIZONTAL;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
if (isStageFilmstrip) {
return THUMBNAIL_TYPE.TILE;
}
return THUMBNAIL_TYPE.VERTICAL;
}
}

View File

@@ -22,6 +22,7 @@ import {
import {
ADD_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS,
REMOVE_STAGE_PARTICIPANT,
SET_MAX_STAGE_PARTICIPANTS,
SET_USER_FILMSTRIP_WIDTH,
@@ -42,10 +43,12 @@ import {
import {
isFilmstripResizable,
updateRemoteParticipants,
updateRemoteParticipantsOnLeave
updateRemoteParticipantsOnLeave,
getActiveParticipantsIds,
getPinnedActiveParticipants,
isStageFilmstripAvailable
} from './functions';
import './subscriber';
import { getActiveParticipantsIds, getPinnedActiveParticipants, isStageFilmstripEnabled } from './functions.web';
/**
* Map of timers.
@@ -202,15 +205,15 @@ MiddlewareRegistry.register(store => next => action => {
case DOMINANT_SPEAKER_CHANGED: {
const { id } = action.participant;
const state = store.getState();
const stageFilmstrip = isStageFilmstripEnabled(state);
const currentLayout = getCurrentLayout(state);
const stageFilmstrip = isStageFilmstripAvailable(state);
const local = getLocalParticipant(state);
const currentLayout = getCurrentLayout(state);
if (id === local.id) {
if (id === local.id || currentLayout === LAYOUTS.TILE_VIEW) {
break;
}
if (stageFilmstrip && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
if (stageFilmstrip) {
const isPinned = getPinnedActiveParticipants(state).some(p => p.participantId === id);
store.dispatch(addStageParticipant(id, Boolean(isPinned)));
@@ -276,7 +279,17 @@ MiddlewareRegistry.register(store => next => action => {
} else {
dispatch(addStageParticipant(participantId, true));
}
break;
}
case CLEAR_STAGE_PARTICIPANTS: {
const activeParticipants = getActiveParticipantsIds(store.getState());
activeParticipants.forEach(pId => {
const tid = timers.get(pId);
clearTimeout(tid);
timers.delete(pId);
});
}
}

View File

@@ -18,7 +18,8 @@ import {
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS
SET_MAX_STAGE_PARTICIPANTS,
CLEAR_STAGE_PARTICIPANTS
} from './actionTypes';
const DEFAULT_STATE = {
@@ -273,6 +274,12 @@ ReducerRegistry.register(
maxStageParticipants: action.maxParticipants
};
}
case CLEAR_STAGE_PARTICIPANTS: {
return {
...state,
activeParticipants: []
};
}
}
return state;

View File

@@ -1,7 +1,7 @@
// @flow
import { isMobileBrowser } from '../base/environment/utils';
import { getParticipantCountWithFake } from '../base/participants';
import { getParticipantCountWithFake, pinParticipant } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { clientResized } from '../base/responsive-ui';
import { shouldHideSelfView } from '../base/settings';
@@ -12,6 +12,7 @@ import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import {
clearStageParticipants,
setHorizontalViewDimensions,
setStageFilmstripViewDimensions,
setTileViewDimensions,
@@ -24,7 +25,6 @@ import {
import {
isFilmstripResizable,
isFilmstripScrollVisible,
shouldDisplayStageFilmstrip,
updateRemoteParticipants
} from './functions';
@@ -61,8 +61,13 @@ StateListenerRegistry.register(
*/
StateListenerRegistry.register(
/* selector */ state => {
return { layout: getCurrentLayout(state),
width: state['features/base/responsive-ui'].clientWidth };
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
return {
layout: getCurrentLayout(state),
height: clientHeight,
width: clientWidth
};
},
/* listener */ ({ layout }, store) => {
switch (layout) {
@@ -74,6 +79,12 @@ StateListenerRegistry.register(
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
if (store.getState()['features/filmstrip'].activeParticipants.length > 1) {
store.dispatch(clearStageParticipants());
}
break;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
store.dispatch(pinParticipant(null));
break;
}
}, {
@@ -177,7 +188,7 @@ StateListenerRegistry.register(
};
},
/* listener */(_, store) => {
if (shouldDisplayStageFilmstrip(store.getState())) {
if (getCurrentLayout(store.getState()) === LAYOUTS.STAGE_FILMSTRIP_VIEW) {
store.dispatch(setStageFilmstripViewDimensions());
}
}, {

View File

@@ -9,7 +9,7 @@ import {
getPinnedParticipant,
getRemoteParticipants
} from '../base/participants';
import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import {
SELECT_LARGE_VIDEO_PARTICIPANT,
@@ -30,6 +30,10 @@ export function selectParticipantInLargeVideo(participant: ?string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
if (isStageFilmstripAvailable(state, 2)) {
return;
}
// Keep Etherpad open.
if (state['features/etherpad'].editing) {
return;
@@ -103,7 +107,7 @@ function _electLastVisibleRemoteVideo(tracks) {
* @returns {(string|undefined)}
*/
function _electParticipantInLargeVideo(state) {
const stageFilmstrip = isStageFilmstripEnabled(state);
const stageFilmstrip = isStageFilmstripAvailable(state);
let participant;
if (!stageFilmstrip) {

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { translate } from '../../../base/i18n';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { connect } from '../../../base/redux';
import ChatInputBar from '../../../chat/components/native/ChatInputBar';
import MessageContainer from '../../../chat/components/native/MessageContainer';
import AbstractLobbyScreen, {
Props as AbstractProps,
_mapStateToProps as abstractMapStateToProps
} from '../AbstractLobbyScreen';
import styles from './styles';
/**
* Implements a chat screen that appears when communication is started
* between the moderator and the participant being in the lobby.
*/
class LobbyChatScreen extends
AbstractLobbyScreen<AbstractProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _lobbyChatMessages } = this.props;
return (
<JitsiScreen style = { styles.lobbyChatWrapper }>
<MessageContainer messages = { _lobbyChatMessages } />
<ChatInputBar onSend = { this._onSendMessage } />
</JitsiScreen>
);
}
_onSendMessage: () => void;
}
export default translate(connect(abstractMapStateToProps)(LobbyChatScreen));

View File

@@ -2,50 +2,50 @@
import React from 'react';
import { Text, View, TouchableOpacity, TextInput } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { Icon, IconClose, IconEdit } from '../../../base/icons';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import ChatInputBar from '../../../chat/components/native/ChatInputBar';
import MessageContainer from '../../../chat/components/native/MessageContainer';
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui';
import BaseTheme from '../../../base/ui/components/BaseTheme';
import InviteButton
from '../../../invite/components/add-people-dialog/native/InviteButton';
import { LargeVideo } from '../../../large-video/components';
import { navigate }
from '../../../mobile/navigation/components/lobby/LobbyNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import AudioMuteButton from '../../../toolbox/components/AudioMuteButton';
import VideoMuteButton from '../../../toolbox/components/VideoMuteButton';
import AbstractLobbyScreen, {
Props as AbstractProps,
_mapStateToProps as abstractMapStateToProps } from '../AbstractLobbyScreen';
import styles from './styles';
type Props = AbstractProps & {
/**
* The current aspect ratio of the screen.
*/
_aspectRatio: Symbol
}
/**
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen {
class LobbyScreen extends AbstractLobbyScreen<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _meetingName, t } = this.props;
return (
<JitsiScreen
style = { this.props._isLobbyChatActive && this.state.isChatOpen
? styles.lobbyChatWrapper
: styles.contentWrapper }>
{this.props._isLobbyChatActive && this.state.isChatOpen
? this._renderLobbyChat()
: <SafeAreaView>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent()}
</SafeAreaView> }
</JitsiScreen>
<>
{ this._renderLobby() }
</>
);
}
@@ -69,35 +69,58 @@ class LobbyScreen extends AbstractLobbyScreen {
_onSwitchToPasswordMode: () => void;
_onSendMessage: () => void;
_onToggleChat: () => void;
_renderContent: () => React$Element<*>;
_renderToolbarButtons: () => React$Element<*>;
_renderLobby: () => React$Element<*>;
_onNavigateToLobbyChat: () => void;
/**
* Renders the lobby chat.
* Navigates to the lobby chat screen.
*
* @private
* @returns {void}
*/
_onNavigateToLobbyChat() {
navigate(screen.lobby.chat);
}
/**
* Renders the lobby.
*
* @inheritdoc
*/
_renderLobbyChat() {
const { t } = this.props;
_renderLobby() {
const { _aspectRatio } = this.props;
let contentStyles;
let largeVideoContainerStyles;
let contentContainerStyles;
if (_aspectRatio === ASPECT_RATIO_NARROW) {
largeVideoContainerStyles = styles.largeVideoContainer;
contentContainerStyles = styles.contentContainer;
} else {
contentStyles = styles.contentWide;
largeVideoContainerStyles = styles.largeVideoContainerWide;
contentContainerStyles = styles.contentContainerWide;
}
return (
<>
<View style = { styles.lobbyChatHeader }>
<Text style = { styles.lobbyChatTitle }>
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
</Text>
<TouchableOpacity onPress = { this._onToggleChat }>
<Icon
src = { IconClose }
style = { styles.lobbyChatCloseButton } />
</TouchableOpacity>
<JitsiScreen
safeAreaInsets = { [ 'right' ] }
style = { styles.contentWrapper }>
<View style = { contentStyles }>
<View style = { largeVideoContainerStyles }>
<LargeVideo />
</View>
<View style = { contentContainerStyles }>
{ this._renderContent() }
{ this._renderToolbarButtons() }
</View>
</View>
<MessageContainer messages = { this.props._lobbyChatMessages } />
<ChatInputBar onSend = { this._onSendMessage } />
</>
</JitsiScreen>
);
}
@@ -108,15 +131,15 @@ class LobbyScreen extends AbstractLobbyScreen {
*/
_renderJoining() {
return (
<>
<View style = { styles.formWrapper }>
<LoadingIndicator
color = 'black'
color = { BaseTheme.palette.icon01 }
style = { styles.loadingIndicator } />
<Text style = { styles.joiningMessage }>
{ this.props.t('lobby.joiningMessage') }
</Text>
{ this._renderStandardButtons() }
</>
</View>
);
}
@@ -127,7 +150,7 @@ class LobbyScreen extends AbstractLobbyScreen {
*/
_renderParticipantForm() {
const { t } = this.props;
const { displayName, email } = this.state;
const { displayName } = this.state;
return (
<View style = { styles.formWrapper }>
@@ -138,13 +161,6 @@ class LobbyScreen extends AbstractLobbyScreen {
onChangeText = { this._onChangeDisplayName }
style = { styles.field }
value = { displayName } />
<Text style = { styles.fieldLabel }>
{ t('lobby.emailField') }
</Text>
<TextInput
onChangeText = { this._onChangeEmail }
style = { styles.field }
value = { email } />
</View>
);
}
@@ -155,28 +171,7 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderParticipantInfo() {
const { displayName, email } = this.state;
return (
<View style = { styles.participantBox }>
<TouchableOpacity
onPress = { this._onEnableEdit }
style = { styles.editButton }>
<Icon
src = { IconEdit }
style = { styles.editIcon } />
</TouchableOpacity>
<Avatar
participantId = { this.props._participantId }
size = { 64 } />
<Text style = { styles.displayNameText }>
{ displayName }
</Text>
{ Boolean(email) && <Text style = { styles.secondaryText }>
{ email }
</Text> }
</View>
);
return this._renderParticipantForm();
}
/**
@@ -215,7 +210,17 @@ class LobbyScreen extends AbstractLobbyScreen {
const { t } = this.props;
return (
<>
<View style = { styles.passwordJoinButtonsWrapper }>
<TouchableOpacity
onPress = { this._onSwitchToKnockMode }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.backToKnockModeButton') }
</Text>
</TouchableOpacity>
<TouchableOpacity
disabled = { !this.state.password }
onPress = { this._onJoinWithPassword }
@@ -227,17 +232,34 @@ class LobbyScreen extends AbstractLobbyScreen {
{ t('lobby.passwordJoinButton') }
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { this._onSwitchToKnockMode }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text>
{ t('lobby.backToKnockModeButton') }
</Text>
</TouchableOpacity>
</>
</View>
);
}
/**
* Renders the toolbar buttons menu.
*
* @inheritdoc
*/
_renderToolbarButtons() {
const { _aspectRatio } = this.props;
let toolboxContainerStyles;
if (_aspectRatio === ASPECT_RATIO_NARROW) {
toolboxContainerStyles = styles.toolboxContainer;
} else {
toolboxContainerStyles = styles.toolboxContainerWide;
}
return (
<View style = { toolboxContainerStyles }>
<AudioMuteButton
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
<InviteButton
styles = { styles.buttonStylesBorderless } />
</View>
);
}
@@ -248,50 +270,72 @@ class LobbyScreen extends AbstractLobbyScreen {
*/
_renderStandardButtons() {
const { _knocking, _renderPassword, _isLobbyChatActive, t } = this.props;
const { displayName } = this.state;
const askToJoinButtonStyles
= displayName ? styles.primaryButton : styles.primaryButtonDisabled;
return (
<>
{ _knocking || <TouchableOpacity
disabled = { !this.state.displayName }
onPress = { this._onAskToJoin }
<View style = { styles.standardButtonWrapper }>
{ _knocking && _isLobbyChatActive && <TouchableOpacity
onPress = { this._onNavigateToLobbyChat }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity> }
{ _knocking && _isLobbyChatActive && <TouchableOpacity
onPress = { this._onToggleChat }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text>
{ t('toolbar.openChat') }
</Text>
</TouchableOpacity>}
{ _knocking || <TouchableOpacity
disabled = { !displayName }
onPress = { this._onAskToJoin }
style = { [
styles.button,
askToJoinButtonStyles
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity> }
{ _renderPassword && <TouchableOpacity
onPress = { this._onSwitchToPasswordMode }
style = { [
styles.button,
styles.secondaryButton
styles.primaryButton
] }>
<Text>
<Text style = { styles.primaryButtonText }>
{ t('lobby.enterPasswordButton') }
</Text>
</TouchableOpacity> }
<TouchableOpacity
onPress = { this._onCancel }
style = { styles.cancelButton }>
<Text>
style = { [
styles.button,
styles.cancelButton
] }>
<Text style = { styles.cancelButtonText }>
{ t('dialog.Cancel') }
</Text>
</TouchableOpacity>
</>
</View>
);
}
}
/**
* 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 {{
* _aspectRatio: Symbol
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
return {
...abstractMapStateToProps(state, ownProps),
_aspectRatio: state['features/base/responsive-ui'].aspectRatio
};
}
export default translate(connect(_mapStateToProps)(LobbyScreen));

View File

@@ -2,3 +2,4 @@
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyScreen } from './LobbyScreen';
export { default as LobbyChatScreen } from './LobbyChatScreen';

View File

@@ -1,6 +1,6 @@
// @flow
import BaseTheme from '../../../base/ui/components/BaseTheme';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const SECONDARY_COLOR = BaseTheme.palette.border04;
@@ -8,8 +8,24 @@ export default {
button: {
alignItems: 'center',
borderRadius: 4,
marginVertical: BaseTheme.spacing[1],
paddingVertical: BaseTheme.spacing[2]
padding: BaseTheme.spacing[2],
width: '100%'
},
buttonStylesBorderless: {
iconStyle: {
backgroundColor: BaseTheme.palette.action02Active,
color: BaseTheme.palette.icon01,
fontSize: 24
},
style: {
backgroundColor: BaseTheme.palette.action02Active,
flexDirection: 'row',
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[3],
height: 24,
width: 24
}
},
lobbyChatWrapper: {
@@ -26,29 +42,66 @@ export default {
},
lobbyChatTitle: {
color: '#fff',
color: BaseTheme.palette.text01,
fontSize: 20,
fontWeight: 'bold',
flexShrink: 1
},
lobbyChatCloseButton: {
fontSize: 20,
marginLeft: 20,
color: '#fff'
fontSize: 24,
marginLeft: BaseTheme.spacing[3],
marginTop: BaseTheme.spacing[1],
color: BaseTheme.palette.icon01
},
contentWrapper: {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyItems: 'center',
height: '100%'
backgroundColor: BaseTheme.palette.ui02,
flex: 1
},
closeIcon: {
color: 'red',
fontSize: 20
contentWide: {
backgroundColor: BaseTheme.palette.ui02,
flex: 1,
flexDirection: 'row'
},
largeVideoContainer: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
minHeight: '50%'
},
largeVideoContainerWide: {
height: '100%',
width: '50%'
},
contentContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
contentContainerWide: {
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[2],
width: '50%'
},
toolboxContainer: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
},
toolboxContainerWide: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
},
dialogTitle: {
@@ -75,40 +128,55 @@ export default {
},
field: {
backgroundColor: BaseTheme.palette.field02,
borderColor: SECONDARY_COLOR,
borderRadius: 4,
borderWidth: 1,
marginVertical: 8,
padding: 8
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 2,
marginHorizontal: BaseTheme.spacing[3],
padding: BaseTheme.spacing[2]
},
fieldError: {
color: BaseTheme.palette.warning07,
fontSize: 10
},
fieldRow: {
paddingTop: 16
color: BaseTheme.palette.warning03,
marginLeft: BaseTheme.spacing[3],
fontSize: 16
},
fieldLabel: {
color: BaseTheme.palette.text01,
marginVertical: BaseTheme.spacing[4],
textAlign: 'center'
},
formWrapper: {
alignItems: 'stretch',
alignSelf: 'stretch'
},
standardButtonWrapper: {
alignSelf: 'stretch',
paddingVertical: 16
marginHorizontal: BaseTheme.spacing[3]
},
joiningContainer: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center'
},
joiningMessage: {
color: 'rgba(0, 0, 0, .7)',
paddingBottom: 36,
color: BaseTheme.palette.text01,
marginBottom: BaseTheme.spacing[2],
textAlign: 'center'
},
passwordJoinButtonsWrapper: {
alignItems: 'stretch',
alignSelf: 'stretch',
marginHorizontal: BaseTheme.spacing[3]
},
loadingIndicator: {
marginVertical: 36
marginVertical: BaseTheme.spacing[4]
},
participantBox: {
@@ -122,29 +190,33 @@ export default {
},
primaryButton: {
alignSelf: 'stretch',
backgroundColor: 'rgb(3, 118, 218)'
backgroundColor: BaseTheme.palette.action01,
marginTop: BaseTheme.spacing[4]
},
primaryButtonDisabled: {
backgroundColor: BaseTheme.palette.action03Disabled,
marginTop: BaseTheme.spacing[4]
},
primaryButtonText: {
color: 'white'
color: BaseTheme.palette.text01
},
secondaryButton: {
alignSelf: 'stretch',
backgroundColor: 'transparent'
},
secondaryText: {
color: 'rgba(0, 0, 0, .7)',
primaryText: {
color: BaseTheme.palette.text01,
margin: 'auto',
textAlign: 'center'
},
cancelButton: {
alignItems: 'center',
backgroundColor: 'transparent',
marginVertical: 4
backgroundColor: BaseTheme.palette.action02Disabled,
marginTop: BaseTheme.spacing[4]
},
cancelButtonText: {
color: BaseTheme.palette.text01
},
// KnockingParticipantList

View File

@@ -1,5 +1,3 @@
// @flow
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

View File

@@ -1,8 +1,5 @@
// @flow
import React from 'react';
// $FlowExpectedError
export const conferenceNavigationRef = React.createRef();
/**
@@ -13,7 +10,6 @@ export const conferenceNavigationRef = React.createRef();
* @returns {Function}
*/
export function navigate(name: string, params: Object) {
// $FlowExpectedError
return conferenceNavigationRef.current?.navigate(name, params);
}
@@ -23,7 +19,6 @@ export function navigate(name: string, params: Object) {
* @returns {Function}
*/
export function goBack() {
// $FlowExpectedError
return conferenceNavigationRef.current?.goBack();
}
@@ -34,7 +29,6 @@ export function goBack() {
* @returns {Function}
*/
export function setParams(params: Object) {
// $FlowExpectedError
return conferenceNavigationRef.current?.setParams(params);
}

View File

@@ -1,5 +1,3 @@
// @flow
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
@@ -13,7 +11,6 @@ import { SharedDocument } from '../../../../../etherpad';
import { GifsMenu } from '../../../../../gifs/components';
import AddPeopleDialog
from '../../../../../invite/components/add-people-dialog/native/AddPeopleDialog';
import LobbyScreen from '../../../../../lobby/components/native/LobbyScreen';
import { ParticipantsPane } from '../../../../../participants-pane/components/native';
import { StartLiveStreamDialog } from '../../../../../recording';
import { StartRecordingDialog }
@@ -31,7 +28,6 @@ import {
gifsMenuOptions,
inviteScreenOptions,
liveStreamScreenOptions,
lobbyScreenOptions,
navigationContainerTheme,
participantsScreenOptions,
recordingScreenOptions,
@@ -42,6 +38,8 @@ import {
} from '../../../screenOptions';
import ChatAndPollsNavigationContainer
from '../../chat/components/ChatAndPollsNavigationContainer';
import LobbyNavigationContainer
from '../../lobby/components/LobbyNavigationContainer';
import {
conferenceNavigationRef
} from '../ConferenceNavigationContainerRef';
@@ -134,9 +132,12 @@ const ConferenceNavigationContainer = () => {
title: t('notify.gifsMenu')
}} />
<ConferenceStack.Screen
component = { LobbyScreen }
name = { screen.lobby }
options = { lobbyScreenOptions } />
component = { LobbyNavigationContainer }
name = { screen.lobby.root }
options = {{
gestureEnabled: false,
headerShown: false
}} />
<ConferenceStack.Screen
component = { AddPeopleDialog }
name = { screen.conference.invite }

View File

@@ -0,0 +1,23 @@
import React from 'react';
export const lobbyNavigationContainerRef = React.createRef();
/**
* User defined navigation action included inside the reference to the container.
*
* @param {string} name - Destination name of the route that has been defined somewhere.
* @param {Object} params - Params to pass to the destination route.
* @returns {Function}
*/
export function navigate(name: string, params: Object) {
return lobbyNavigationContainerRef.current?.navigate(name, params);
}
/**
* User defined navigation action included inside the reference to the container.
*
* @returns {Function}
*/
export function goBack() {
return lobbyNavigationContainerRef.current?.goBack();
}

View File

@@ -0,0 +1,47 @@
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { useSelector } from 'react-redux';
import { LobbyChatScreen, LobbyScreen } from '../../../../../lobby';
import { screen } from '../../../routes';
import {
lobbyChatScreenOptions,
lobbyScreenOptions, navigationContainerTheme
} from '../../../screenOptions';
import { lobbyNavigationContainerRef } from '../LobbyNavigationContainerRef';
const LobbyStack = createStackNavigator();
const LobbyNavigationContainer = () => {
const { isLobbyChatActive }
= useSelector(state => state['features/chat']);
return (
<NavigationContainer
independent = { true }
ref = { lobbyNavigationContainerRef }
theme = { navigationContainerTheme }>
<LobbyStack.Navigator
screenOptions = {{
presentation: 'modal'
}}>
<LobbyStack.Screen
component = { LobbyScreen }
name = { screen.lobby.main }
options = { lobbyScreenOptions } />
{
isLobbyChatActive
&& <LobbyStack.Screen
component = { LobbyChatScreen }
name = { screen.lobby.chat }
options = { lobbyChatScreenOptions } />
}
</LobbyStack.Navigator>
</NavigationContainer>
);
};
export default LobbyNavigationContainer;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native';
import { IconClose } from '../../base/icons';
import HeaderNavigationButton from './components/HeaderNavigationButton';
/**
* Close icon/text button based on platform.
*
* @param {Function} goBack - Goes back to the previous screen function.
* @returns {React.Component}
*/
export function screenHeaderCloseButton(goBack: Function) {
const { t } = useTranslation();
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconClose } />
);
}

View File

@@ -1,8 +1,7 @@
// @flow
import debounce from 'lodash/debounce';
import { SET_ROOM } from '../../base/conference/actionTypes';
import { CONFERENCE_FAILED, SET_ROOM } from '../../base/conference/actionTypes';
import { JitsiConferenceErrors } from '../../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../../base/redux';
import { readyToClose } from '../external-api/actions';
@@ -16,6 +15,9 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_ROOM:
return _setRoom(store, next, action);
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
}
return next(action);
@@ -61,3 +63,31 @@ function _setRoom({ dispatch, getState }, next, action) {
return result;
}
/**
* Function to handle the conference failed event and navigate the user to the lobby screen
* based on the failure reason.
*
* @param {Object} store - The Redux store.
* @param {Function} next - The Redux next function.
* @param {Object} action - The Redux action.
* @returns {Object}
*/
function _conferenceFailed({ dispatch, getState }, next, action) {
const state = getState();
const isWelcomePageEnabled = isWelcomePageAppEnabled(state);
const { error } = action;
// We need to cover the case where knocking participant
// is rejected from entering the conference
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
if (isWelcomePageEnabled) {
navigateRoot(screen.root);
} else {
// For JitsiSDK, WelcomePage is not available
_sendReadyToClose(dispatch);
}
}
return next(action);
}

View File

@@ -34,5 +34,9 @@ export const screen = {
invite: 'Invite',
sharedDocument: 'Shared document'
},
lobby: 'Lobby'
lobby: {
root: 'Lobby root',
main: 'Lobby',
chat: 'Lobby chat'
}
};

View File

@@ -1,13 +1,9 @@
// @flow
import { TransitionPresets } from '@react-navigation/stack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native';
import {
Icon,
IconClose,
IconHelp,
IconHome,
IconInfo,
@@ -15,8 +11,9 @@ import {
} from '../../base/icons';
import BaseTheme from '../../base/ui/components/BaseTheme.native';
import HeaderNavigationButton from './components/HeaderNavigationButton';
import { goBack } from './components/conference/ConferenceNavigationContainerRef';
import { goBack as goBackToLobbyScreen } from './components/lobby/LobbyNavigationContainerRef';
import { screenHeaderCloseButton } from './functions';
/**
@@ -167,16 +164,12 @@ export const helpScreenOptions = {
/**
* Screen options for conference.
*/
export const conferenceScreenOptions = {
...fullScreenOptions
};
export const conferenceScreenOptions = fullScreenOptions;
/**
* Screen options for lobby modal.
*/
export const lobbyScreenOptions = {
...fullScreenOptions
};
export const lobbyScreenOptions = fullScreenOptions;
/**
* Tab bar options for chat screen.
@@ -198,23 +191,7 @@ export const chatTabBarOptions = {
export const presentationScreenOptions = {
...conferenceModalPresentation,
headerBackTitleVisible: false,
headerLeft: () => {
const { t } = useTranslation();
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconClose } />
);
},
headerLeft: () => screenHeaderCloseButton(goBack),
headerStatusBarHeight: 0,
headerStyle: {
backgroundColor: BaseTheme.palette.screen01Header
@@ -227,50 +204,44 @@ export const presentationScreenOptions = {
/**
* Screen options for chat.
*/
export const chatScreenOptions = {
...presentationScreenOptions
};
export const chatScreenOptions = presentationScreenOptions;
/**
* Screen options for invite modal.
*/
export const inviteScreenOptions = {
...presentationScreenOptions
};
export const inviteScreenOptions = presentationScreenOptions;
/**
* Screen options for participants modal.
*/
export const participantsScreenOptions = {
...presentationScreenOptions
};
export const participantsScreenOptions = presentationScreenOptions;
/**
* Screen options for speaker stats modal.
*/
export const speakerStatsScreenOptions = {
...presentationScreenOptions
};
export const speakerStatsScreenOptions = presentationScreenOptions;
/**
* Screen options for security options modal.
*/
export const securityScreenOptions = {
...presentationScreenOptions
};
export const securityScreenOptions = presentationScreenOptions;
/**
* Screen options for recording modal.
*/
export const recordingScreenOptions = {
...presentationScreenOptions
};
export const recordingScreenOptions = presentationScreenOptions;
/**
* Screen options for live stream modal.
*/
export const liveStreamScreenOptions = {
...presentationScreenOptions
export const liveStreamScreenOptions = presentationScreenOptions;
/**
* Screen options for lobby chat modal.
*/
export const lobbyChatScreenOptions = {
...presentationScreenOptions,
headerLeft: () => screenHeaderCloseButton(goBackToLobbyScreen)
};
/**

View File

@@ -128,7 +128,12 @@ MiddlewareRegistry.register(store => next => action => {
const { participant: p } = action;
const { conference } = state['features/base/conference'];
if (conference && !p.local && !joinLeaveNotificationsDisabled() && !p.isReplacing) {
// Do not display notifications for the fake screenshare tiles.
if (conference
&& !p.local
&& !p.isFakeScreenShareParticipant
&& !joinLeaveNotificationsDisabled()
&& !p.isReplacing) {
dispatch(showParticipantJoinedNotification(
getParticipantDisplayName(state, p.id)
));
@@ -143,7 +148,11 @@ MiddlewareRegistry.register(store => next => action => {
action.participant.id
);
if (participant && !participant.local && !action.participant.isReplaced) {
// Do not display notifications for the fake screenshare tiles.
if (participant
&& !participant.local
&& !participant.isFakeScreenShareParticipant
&& !action.participant.isReplaced) {
dispatch(showParticipantLeftNotification(
getParticipantDisplayName(state, participant.id)
));

View File

@@ -45,3 +45,15 @@ export const UPDATE_TRANSCRIPT_MESSAGE = 'UPDATE_TRANSCRIPT_MESSAGE';
*/
export const TOGGLE_REQUESTING_SUBTITLES
= 'TOGGLE_REQUESTING_SUBTITLES';
/**
* The type of (redux) action which indicates if the user set the state of
* the subtitles to enabled or disabled.
*
* {
* type: SET_REQUESTING_SUBTITLES
* enabled: boolean
* }
*/
export const SET_REQUESTING_SUBTITLES
= 'SET_REQUESTING_SUBTITLES';

View File

@@ -4,6 +4,7 @@ import {
ENDPOINT_MESSAGE_RECEIVED,
REMOVE_TRANSCRIPT_MESSAGE,
TOGGLE_REQUESTING_SUBTITLES,
SET_REQUESTING_SUBTITLES,
UPDATE_TRANSCRIPT_MESSAGE
} from './actionTypes';
@@ -75,3 +76,19 @@ export function toggleRequestingSubtitles() {
type: TOGGLE_REQUESTING_SUBTITLES
};
}
/**
* Signals that the local user has enabled or disabled the subtitles.
*
* @param {boolean} enabled - The new state of the subtitles.
* @returns {{
* type: SET_REQUESTING_SUBTITLES,
* enabled: boolean
* }}
*/
export function setRequestingSubtitles(enabled: boolean) {
return {
type: SET_REQUESTING_SUBTITLES,
enabled
};
}

View File

@@ -4,7 +4,8 @@ import { MiddlewareRegistry } from '../base/redux';
import {
ENDPOINT_MESSAGE_RECEIVED,
TOGGLE_REQUESTING_SUBTITLES
TOGGLE_REQUESTING_SUBTITLES,
SET_REQUESTING_SUBTITLES
} from './actionTypes';
import {
removeTranscriptMessage,
@@ -56,6 +57,9 @@ MiddlewareRegistry.register(store => next => action => {
case TOGGLE_REQUESTING_SUBTITLES:
_requestingSubtitlesToggled(store);
break;
case SET_REQUESTING_SUBTITLES:
_requestingSubtitlesSet(store, action.enabled);
break;
}
return next(action);
@@ -177,6 +181,24 @@ function _requestingSubtitlesToggled({ getState }) {
!_requestingSubtitles);
}
/**
* Set the local property 'requestingTranscription'. This will cause Jicofo
* and Jigasi to decide whether the transcriber needs to be in the room.
*
* @param {Store} store - The redux store.
* @param {boolean} enabled - The new state of the subtitles.
* @private
* @returns {void}
*/
function _requestingSubtitlesSet({ getState }, enabled: boolean) {
const state = getState();
const { conference } = state['features/base/conference'];
conference.setLocalParticipantProperty(
P_NAME_REQUESTING_TRANSCRIPTION,
enabled);
}
/**
* Set a timeout on a TranscriptMessage object so it clears itself when it's not
* updated.

View File

@@ -2,7 +2,7 @@ import { ReducerRegistry } from '../base/redux';
import {
REMOVE_TRANSCRIPT_MESSAGE, TOGGLE_REQUESTING_SUBTITLES,
UPDATE_TRANSCRIPT_MESSAGE
SET_REQUESTING_SUBTITLES, UPDATE_TRANSCRIPT_MESSAGE
} from './actionTypes';
/**
@@ -30,6 +30,11 @@ ReducerRegistry.register('features/subtitles', (
...state,
_requestingSubtitles: !state._requestingSubtitles
};
case SET_REQUESTING_SUBTITLES:
return {
...state,
_requestingSubtitles: action.enabled
};
}
return state;

View File

@@ -157,7 +157,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
*/
function _mapStateToProps(state): Object {
const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const _disabled = state['features/base/config'].startSilent || isAudioMuteButtonDisabled(state);
const _disabled = isAudioMuteButtonDisabled(state);
const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true);
return {

View File

@@ -863,6 +863,27 @@ class Toolbox extends Component<Props> {
};
}
/**
* Returns the notify mode of the given toolbox button.
*
* @param {string} btnName - The toolbar button's name.
* @returns {string|undefined} - The button's notify mode.
*/
_getButtonNotifyMode(btnName) {
const notify = this.props._buttonsWithNotifyClick?.find(
(btn: string | Object) =>
(typeof btn === 'string' && btn === btnName)
|| (typeof btn === 'object' && btn.key === btnName)
);
if (notify) {
return typeof notify === 'string' || notify.preventExecution
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
}
}
/**
* Sets the notify click mode for the buttons.
*
@@ -876,19 +897,7 @@ class Toolbox extends Component<Props> {
Object.values(buttons).forEach((button: any) => {
if (typeof button === 'object') {
const notify = this.props._buttonsWithNotifyClick.find(
(btn: string | Object) =>
(typeof btn === 'string' && btn === button.key)
|| (typeof btn === 'object' && btn.key === button.key)
);
if (notify) {
const notifyMode = typeof notify === 'string' || notify.preventExecution
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
button.notifyMode = notifyMode;
}
button.notifyMode = this._getButtonNotifyMode(button.key);
}
});
}
@@ -1369,8 +1378,10 @@ class Toolbox extends Component<Props> {
)}
<HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { this._getButtonNotifyMode('hangup') }
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
</div>
</div>
@@ -1419,14 +1430,7 @@ function _mapStateToProps(state, ownProps) {
}
}
let { toolbarButtons } = ownProps;
const stateToolbarButtons = getToolbarButtons(state);
if (toolbarButtons) {
toolbarButtons = toolbarButtons.filter(name => isToolbarButtonEnabled(name, stateToolbarButtons));
} else {
toolbarButtons = stateToolbarButtons;
}
const toolbarButtons = ownProps.toolbarButtons || getToolbarButtons(state);
return {
_backgroundType: state['features/virtual-background'].backgroundType,

View File

@@ -8,6 +8,7 @@
*/
export function isAudioMuteButtonDisabled(state: Object) {
const { available, muted, unmuteBlocked } = state['features/base/media'].audio;
const { startSilent } = state['features/base/config'];
return !available || (muted && unmuteBlocked);
return Boolean(!available || startSilent || (muted && unmuteBlocked));
}

View File

@@ -6,5 +6,6 @@
export const LAYOUTS = {
HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view',
TILE_VIEW: 'tile-view',
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view'
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view',
STAGE_FILMSTRIP_VIEW: 'stage-filmstrip-view'
};

View File

@@ -7,11 +7,7 @@ import {
getParticipantCount,
pinParticipant
} from '../base/participants';
import {
DEFAULT_MAX_COLUMNS,
ABSOLUTE_MAX_COLUMNS
} from '../filmstrip/constants';
import { getNumberOfPartipantsForTileView } from '../filmstrip/functions.web';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { isVideoPlaying } from '../shared-video/functions';
import { VIDEO_QUALITY_LEVELS } from '../video-quality/constants';
@@ -45,54 +41,16 @@ export function getCurrentLayout(state: Object) {
if (shouldDisplayTileView(state)) {
return LAYOUTS.TILE_VIEW;
} else if (interfaceConfig.VERTICAL_FILMSTRIP) {
if (isStageFilmstripAvailable(state, 2)) {
return LAYOUTS.STAGE_FILMSTRIP_VIEW;
}
return LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
}
return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
}
/**
* Returns how many columns should be displayed in tile view. The number
* returned will be between 1 and 7, inclusive.
*
* @param {Object} state - The redux store state.
* @param {number} width - Custom width to use for calculation.
* @returns {number}
*/
export function getMaxColumnCount() {
const configuredMax = (typeof interfaceConfig === 'undefined'
? DEFAULT_MAX_COLUMNS
: interfaceConfig.TILE_VIEW_MAX_COLUMNS) || DEFAULT_MAX_COLUMNS;
return Math.min(Math.max(configuredMax, 1), ABSOLUTE_MAX_COLUMNS);
}
/**
* Returns the cell count dimensions for tile view. Tile view tries to uphold
* equal count of tiles for height and width, until maxColumn is reached in
* which rows will be added but no more columns.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} An object is return with the desired number of columns,
* rows, and visible rows (the rest should overflow) for the tile view layout.
*/
export function getNotResponsiveTileViewGridDimensions(state: Object, stageFilmstrip: boolean = false) {
const maxColumns = getMaxColumnCount(state);
const { activeParticipants } = state['features/filmstrip'];
const numberOfParticipants = stageFilmstrip ? activeParticipants.length : getNumberOfPartipantsForTileView(state);
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
const minVisibleRows = Math.min(maxColumns, rows);
return {
columns,
minVisibleRows,
rows
};
}
/**
* Selector for determining if the UI layout should be in tile view. Tile view
* is determined by more than just having the tile view setting enabled, as

View File

@@ -0,0 +1 @@
export * from './functions.any';

View File

@@ -0,0 +1,86 @@
import {
DEFAULT_MAX_COLUMNS,
ABSOLUTE_MAX_COLUMNS,
TILE_PORTRAIT_ASPECT_RATIO
} from '../filmstrip/constants';
import {
getNumberOfPartipantsForTileView,
getThumbnailMinHeight,
getTileDefaultAspectRatio
} from '../filmstrip/functions';
export * from './functions.any';
declare var interfaceConfig: Object;
/**
* Returns how many columns should be displayed in tile view. The number
* returned will be between 1 and 7, inclusive.
*
* @param {Object} state - The redux store state.
* @param {Object} options - Object with custom values used to override the values that we get from redux by default.
* @param {number} options.width - Custom width to be used.
* @param {boolean} options.disableResponsiveTiles - Custom value to be used instead of config.disableResponsiveTiles.
* @param {boolean} options.disableTileEnlargement - Custom value to be used instead of config.disableTileEnlargement.
* @returns {number}
*/
export function getMaxColumnCount(state, options = {}) {
if (typeof interfaceConfig === 'undefined') {
return DEFAULT_MAX_COLUMNS;
}
const {
disableResponsiveTiles: configDisableResponsiveTiles,
disableTileEnlargement: configDisableTileEnlargement
} = state['features/base/config'];
const {
width,
disableResponsiveTiles = configDisableResponsiveTiles,
disableTileEnlargement = configDisableTileEnlargement
} = options;
const { clientWidth } = state['features/base/responsive-ui'];
const widthToUse = width || clientWidth;
const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS;
if (disableResponsiveTiles) {
return Math.min(Math.max(configuredMax || DEFAULT_MAX_COLUMNS, 1), ABSOLUTE_MAX_COLUMNS);
}
if (typeof interfaceConfig.TILE_VIEW_MAX_COLUMNS !== 'undefined' && interfaceConfig.TILE_VIEW_MAX_COLUMNS > 0) {
return Math.max(configuredMax, 1);
}
const aspectRatio = disableTileEnlargement
? getTileDefaultAspectRatio(true, disableTileEnlargement, widthToUse)
: TILE_PORTRAIT_ASPECT_RATIO;
const minHeight = getThumbnailMinHeight(widthToUse);
const minWidth = aspectRatio * minHeight;
return Math.floor(widthToUse / minWidth);
}
/**
* Returns the cell count dimensions for tile view. Tile view tries to uphold
* equal count of tiles for height and width, until maxColumn is reached in
* which rows will be added but no more columns.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} An object is return with the desired number of columns,
* rows, and visible rows (the rest should overflow) for the tile view layout.
*/
export function getNotResponsiveTileViewGridDimensions(state: Object, stageFilmstrip: boolean = false) {
const maxColumns = getMaxColumnCount(state);
const { activeParticipants } = state['features/filmstrip'];
const numberOfParticipants = stageFilmstrip ? activeParticipants.length : getNumberOfPartipantsForTileView(state);
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
const minVisibleRows = Math.min(maxColumns, rows);
return {
columns,
minVisibleRows,
rows
};
}

View File

@@ -18,8 +18,8 @@ import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actio
import { getHideSelfView } from '../../../base/settings';
import { getLocalVideoTrack } from '../../../base/tracks';
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
import { THUMBNAIL_TYPE } from '../../../filmstrip';
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
import { LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ConnectionStatusButton from './ConnectionStatusButton';
@@ -274,7 +274,7 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { currentLayout } = ownProps;
const { thumbnailType } = ownProps;
const localParticipant = getLocalParticipant(state);
const { disableLocalVideoFlip, disableSelfViewSettings } = state['features/base/config'];
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
@@ -284,14 +284,14 @@ function _mapStateToProps(state, ownProps) {
let _menuPosition;
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
switch (thumbnailType) {
case THUMBNAIL_TYPE.TILE:
_menuPosition = 'left-start';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case THUMBNAIL_TYPE.VERTICAL:
_menuPosition = 'left-start';
break;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
case THUMBNAIL_TYPE.HORIZONTAL:
_menuPosition = 'top-start';
break;
default:

View File

@@ -14,7 +14,7 @@ import { getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
import { LAYOUTS } from '../../../video-layout';
import { THUMBNAIL_TYPE } from '../../../filmstrip';
import { renderConnectionStatus } from '../../actions.web';
import ParticipantContextMenu from './ParticipantContextMenu';
@@ -265,7 +265,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantID, currentLayout } = ownProps;
const { participantID, thumbnailType } = ownProps;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _participantDisplayName = participant?.name;
@@ -291,14 +291,14 @@ function _mapStateToProps(state, ownProps) {
let _menuPosition;
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
switch (thumbnailType) {
case THUMBNAIL_TYPE.TILE:
_menuPosition = 'left-start';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case THUMBNAIL_TYPE.VERTICAL:
_menuPosition = 'left-end';
break;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
case THUMBNAIL_TYPE.HORIZONTAL:
_menuPosition = 'top';
break;
default:

View File

@@ -23,33 +23,30 @@ echo "by providing an email address for important account notifications"
echo -n "Enter your email and press [ENTER]: "
read EMAIL
CERTBOT="$(command -v certbot)"
CERTBOT="$(command -v certbot || true)"
if [ ! -x "$CERTBOT" ] ; then
DISTRO=$(lsb_release -is)
DISTRO_VERSION=$(lsb_release -rs)
if [ "$DISTRO" = "Debian" ]; then
apt-get update
apt-get -y install certbot
elif [ "$DISTRO" = "Ubuntu" ]; then
if [ "$DISTRO_VERSION" = "20.04" ] || [ "$DISTRO_VERSION" = "19.10" ]; then
apt-get update
apt-get -y install software-properties-common
add-apt-repository -y universe
apt-get update
apt-get -y install certbot
elif [ "$DISTRO_VERSION" = "18.04" ]; then
apt-get update
apt-get -y install software-properties-common
add-apt-repository -y universe
add-apt-repository -y ppa:certbot/certbot
apt-get update
apt-get -y install certbot
fi
else
if [ "$DISTRO" != "Debian" ] && [ "$DISTRO" != "Ubuntu" ]; then
echo "$DISTRO $DISTRO_VERSION is not supported"
echo "Only Debian 9,10 and Ubuntu 18.04,19.10,20.04 are supported"
echo "Only Debian and Ubuntu 18.04+ are supported"
exit 1
fi
if [ "$DISTRO" = "Ubuntu" ]; then
apt-get update
apt-get -y install software-properties-common
add-apt-repository -y universe
if [ "$DISTRO_VERSION" = "18.04" ]; then
add-apt-repository -y ppa:certbot/certbot
fi
fi
apt-get update
apt-get -y install certbot
CERTBOT="$(command -v certbot)"
fi
CRON_FILE="/etc/cron.weekly/letsencrypt-renew"

View File

@@ -33,6 +33,7 @@ function Util.new(module)
self.appId = module:get_option_string("app_id");
self.appSecret = module:get_option_string("app_secret");
self.asapKeyServer = module:get_option_string("asap_key_server");
self.signatureAlgorithm = module:get_option_string("signature_algorithm");
self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
self.cache = require"util.cache".new(cacheSize);
@@ -85,6 +86,15 @@ function Util.new(module)
return nil;
end
-- Set defaults for signature algorithm
if self.signatureAlgorithm == nil then
if self.asapKeyServer ~= nil then
self.signatureAlgorithm = "RS256"
elseif self.appSecret ~= nil then
self.signatureAlgorithm = "HS256"
end
end
--array of accepted issuers: by default only includes our appId
self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId})
@@ -143,94 +153,6 @@ function Util:get_public_key(keyId)
end
end
--- Verifies issuer part of token
-- @param 'issClaim' claim from the token to verify
-- @param 'acceptedIssuers' list of issuers to check
-- @return nil and error string or true for accepted claim
function Util:verify_issuer(issClaim, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers
end
module:log("debug", "verify_issuer claim: %s against accepted: %s", issClaim, acceptedIssuers);
for i, iss in ipairs(acceptedIssuers) do
if iss == '*' then
-- "*" indicates to accept any issuer in the claims so return success
return true;
end
if issClaim == iss then
-- claim matches an accepted issuer so return success
return true;
end
end
-- if issClaim not found in acceptedIssuers, fail claim
return nil, "Invalid issuer ('iss' claim)";
end
--- Verifies audience part of token
-- @param 'audClaim' claim from the token to verify
-- @return nil and error string or true for accepted claim
function Util:verify_audience(audClaim)
module:log("debug", "verify_audience claim: %s against accepted: %s", audClaim, self.acceptedAudiences);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
-- "*" indicates to accept any audience in the claims so return success
return true;
end
if audClaim == aud then
-- claim matches an accepted audience so return success
return true;
end
end
-- if audClaim not found in acceptedAudiences, fail claim
return nil, "Invalid audience ('aud' claim)";
end
--- Verifies token
-- @param token the token to verify
-- @param secret the secret to use to verify token
-- @param acceptedIssuers the list of accepted issuers to check
-- @return nil and error or the extracted claims from the token
function Util:verify_token(token, secret, acceptedIssuers)
local claims, err = jwt.decode(token, secret, true);
if claims == nil then
return nil, err;
end
local alg = claims["alg"];
if alg ~= nil and (alg == "none" or alg == "") then
return nil, "'alg' claim must not be empty";
end
local issClaim = claims["iss"];
if issClaim == nil then
return nil, "'iss' claim is missing";
end
--check the issuer against the accepted list
local issCheck, issCheckErr = self:verify_issuer(issClaim, acceptedIssuers);
if issCheck == nil then
return nil, issCheckErr;
end
if self.requireRoomClaim then
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
end
end
local audClaim = claims["aud"];
if audClaim == nil then
return nil, "'aud' claim is missing";
end
--check the audience against the accepted list
local audCheck, audCheckErr = self:verify_audience(audClaim);
if audCheck == nil then
return nil, audCheckErr;
end
return claims;
end
--- Verifies token and process needed values to be stored in the session.
-- Token is obtained from session.auth_token.
-- Stores in session the following values:
@@ -255,11 +177,13 @@ function Util:process_and_verify_token(session, acceptedIssuers)
end
end
local pubKey;
local key;
if session.public_key then
-- We're using an public key stored in the session
module:log("debug","Public key was found on the session");
pubKey = session.public_key;
key = session.public_key;
elseif self.asapKeyServer and session.auth_token ~= nil then
-- We're fetching an public key from an ASAP server
local dotFirst = session.auth_token:find("%.");
if not dotFirst then return false, "not-allowed", "Invalid token" end
local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
@@ -274,23 +198,38 @@ function Util:process_and_verify_token(session, acceptedIssuers)
if alg == nil then
return false, "not-allowed", "'alg' claim is missing";
end
if alg.sub(alg,1,2) ~= "RS" then -- do not remove - needed to protect jwt.decode in verify_token
if alg.sub(alg,1,2) ~= "RS" then
return false, "not-allowed", "'kid' claim only support with RS family";
end
pubKey = self:get_public_key(kid);
if pubKey == nil then
key = self:get_public_key(kid);
if key == nil then
return false, "not-allowed", "could not obtain public key";
end
elseif self.appSecret ~= nil then
-- We're using a symmetric secret
key = self.appSecret
end
if key == nil then
return false, "not-allowed", "signature verification key is missing";
end
-- now verify the whole token
local claims, msg;
if self.asapKeyServer then
claims, msg = self:verify_token(session.auth_token, pubKey, acceptedIssuers);
else
claims, msg = self:verify_token(session.auth_token, self.appSecret, acceptedIssuers);
end
local claims, msg = jwt.verify(
session.auth_token,
self.signatureAlgorithm,
key,
acceptedIssuers,
self.acceptedAudiences
)
if claims ~= nil then
if self.requireRoomClaim then
local roomClaim = claims["room"];
if roomClaim == nil then
return false, "'room' claim is missing";
end
end
-- Binds room name to the session which is later checked on MUC join
session.jitsi_meet_room = claims["room"];
-- Binds domain name to the session