mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-25 04:47:48 +00:00
Compare commits
40 Commits
6088
...
saghul-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe9fd1c0bc | ||
|
|
e4704ae032 | ||
|
|
132b44a8b6 | ||
|
|
72111114b6 | ||
|
|
550c730ed4 | ||
|
|
2ac2138982 | ||
|
|
a84d7c17fa | ||
|
|
586ad30ed4 | ||
|
|
f1c5f314e5 | ||
|
|
64d7305598 | ||
|
|
c03d86e0e3 | ||
|
|
0ae2693116 | ||
|
|
20f6ba1736 | ||
|
|
eb64ea6aba | ||
|
|
3e004811e0 | ||
|
|
037b9202a6 | ||
|
|
8b8a42e0d1 | ||
|
|
4315e19780 | ||
|
|
4d2bd932a7 | ||
|
|
6e1f56fad1 | ||
|
|
7d2f62a614 | ||
|
|
97b958e9ea | ||
|
|
b00fc92ee6 | ||
|
|
f9d1003527 | ||
|
|
0abefa87aa | ||
|
|
dde8c586da | ||
|
|
aa944e76ad | ||
|
|
4153097cc9 | ||
|
|
2a5be074d0 | ||
|
|
2e0ae75774 | ||
|
|
a8017149a0 | ||
|
|
e99fc4394d | ||
|
|
46dd88c91b | ||
|
|
dbc8f21b01 | ||
|
|
5ebe308953 | ||
|
|
7420113079 | ||
|
|
221ecac12d | ||
|
|
744607a5cc | ||
|
|
8f641b7bb1 | ||
|
|
045bd44407 |
10
config.js
10
config.js
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
4
debian/control
vendored
@@ -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
|
||||
|
||||
15
debian/jitsi-meet-tokens.postinst
vendored
15
debian/jitsi-meet-tokens.postinst
vendored
@@ -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)
|
||||
|
||||
16
debian/jitsi-meet-turnserver.postinst
vendored
16
debian/jitsi-meet-turnserver.postinst
vendored
@@ -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"
|
||||
|
||||
|
||||
16
debian/jitsi-meet-turnserver.postrm
vendored
16
debian/jitsi-meet-turnserver.postrm
vendored
@@ -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)
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "بلوتوث",
|
||||
"car": "مسجل السيارة",
|
||||
"headphones": "سماعات رأس",
|
||||
"none": "لا يوجد أي أجهزة صوت",
|
||||
"phone": "هاتف",
|
||||
@@ -919,6 +920,7 @@
|
||||
"incomingMessage": "رسالة واردة",
|
||||
"language": "اللغة",
|
||||
"loggedIn": "الدخول باسم {{name}}",
|
||||
"maxStageParticipants": "الحد الأقصى لعدد المشاركين الذين يمكن تثبيتهم في المرحلة الرئيسية",
|
||||
"microphones": "المجهار (المايكروفون)",
|
||||
"moderator": "رئيس الجلسة",
|
||||
"more": "المزيد",
|
||||
|
||||
@@ -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": "L’integrazione del calendario con {{appName}} è usata per accedere in sicurezza al proprio calendario per poter leggere i prossimi appuntamenti ",
|
||||
"about": "L’integrazione 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 l’accesso 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": {
|
||||
|
||||
@@ -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 логотип",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -894,6 +894,7 @@
|
||||
"unavailableTitle": "Recording unavailable",
|
||||
"uploadToCloud": "Upload to the cloud"
|
||||
},
|
||||
"screenshareDisplayName": "{{name}}'s screen",
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Pull to refresh"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
25
modules/API/external/external_api.js
vendored
25
modules/API/external/external_api.js
vendored
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
50
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,6 +59,7 @@ const styles = () => {
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
fontWeight: '100',
|
||||
objectFit: 'cover',
|
||||
textAlign: 'center',
|
||||
|
||||
'&.avatar-small': {
|
||||
height: '28px !important',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
42
react/features/lobby/components/native/LobbyChatScreen.js
Normal file
42
react/features/lobby/components/native/LobbyChatScreen.js
Normal 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));
|
||||
@@ -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));
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
export { default as KnockingParticipantList } from './KnockingParticipantList';
|
||||
export { default as LobbyScreen } from './LobbyScreen';
|
||||
export { default as LobbyChatScreen } from './LobbyChatScreen';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import React from 'react';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
31
react/features/mobile/navigation/functions.js
Normal file
31
react/features/mobile/navigation/functions.js
Normal 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 } />
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -34,5 +34,9 @@ export const screen = {
|
||||
invite: 'Invite',
|
||||
sharedDocument: 'Shared document'
|
||||
},
|
||||
lobby: 'Lobby'
|
||||
lobby: {
|
||||
root: 'Lobby root',
|
||||
main: 'Lobby',
|
||||
chat: 'Lobby chat'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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
|
||||
1
react/features/video-layout/functions.native.js
Normal file
1
react/features/video-layout/functions.native.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './functions.any';
|
||||
86
react/features/video-layout/functions.web.js
Normal file
86
react/features/video-layout/functions.web.js
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user