Compare commits

...

33 Commits

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

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

* remove track streaming status handler from ConnectionIndicatorIcon and ConnectionIndicatorContent

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

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

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

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

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

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

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

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

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

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

* feat(external-api): add set subtitles command
2022-04-20 11:43:18 +03:00
Seda Çağlar
aa944e76ad fix(lang) update Turkish translation 2022-04-19 17:33:24 -05:00
Hristo Terezov
4153097cc9 fix(prejoin):Disable device selection on iosSafari
It seems that showing the device selection dialog on ios Safari will
leads to not working audio. This is temporary fix until we find out
better solution.
2022-04-19 15:32:45 -05:00
Hristo Terezov
2a5be074d0 fix(video-layout): functions imports. 2022-04-19 15:15:44 -05:00
Saúl Ibarra Corretgé
2e0ae75774 fix(debian) make sure we install the latest version of luajwtjitsi
Also on update, since we might start depending on a more recent version.
2022-04-19 14:59:51 +02:00
Saúl Ibarra Corretgé
a8017149a0 fix(debian) update Prosody related dependencies 2022-04-19 14:59:51 +02:00
Jaya Allamsetty
e99fc4394d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1418.0.0+da6d04c2...v1419.0.0+607646a1
2022-04-19 08:30:19 -04:00
philip-cc
46dd88c91b feat(jwt) refactor token authentication plugin to use new luajwtjitsi version 2022-04-19 13:06:20 +02:00
74 changed files with 1498 additions and 929 deletions

View File

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

View File

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

View File

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

4
debian/control vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

50
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { getGravatarURL } from '@jitsi/js-utils/avatar';
import type { Store } from 'redux';
import { i18next } from '../../base/i18n';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
@@ -372,7 +372,7 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
export function getPinnedParticipant(stateful: Object | Function) {
const state = toState(stateful);
const { pinnedParticipant } = state['features/base/participants'];
const stageFilmstrip = isStageFilmstripEnabled(state);
const stageFilmstrip = isStageFilmstripAvailable(state);
if (stageFilmstrip) {
const { activeParticipants } = state['features/filmstrip'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,8 @@ import {
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
TOGGLE_PIN_STAGE_PARTICIPANT
TOGGLE_PIN_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
@@ -266,8 +267,6 @@ 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'];
@@ -275,24 +274,26 @@ export function setStageFilmstripViewDimensions() {
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
const availableWidth = clientWidth - verticalWidth;
const maxColumns = getMaxColumnCount(state, { width: availableWidth });
const maxColumns = getMaxColumnCount(state, {
width: availableWidth,
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const {
height,
width,
columns,
rows
} = disableResponsiveTiles
? calculateNonResponsiveTileViewDimensions(state, true)
: calculateResponsiveTileViewDimensions({
clientWidth: availableWidth,
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
@@ -469,3 +470,14 @@ export function togglePinStageParticipant(participantId) {
participantId
};
}
/**
* Clears the stage participants list.
*
* @returns {Object}
*/
export function clearStageParticipants() {
return {
type: CLEAR_STAGE_PARTICIPANTS
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ import {
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
THUMBNAIL_TYPE,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_LARGE,
@@ -243,18 +244,16 @@ export function getNumberOfPartipantsForTileView(state) {
* disabled.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} - The dimensions.
*/
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
export function calculateNonResponsiveTileViewDimensions(state) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableTileEnlargement } = state['features/base/config'];
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
const filmstripWidth = getVerticalViewMaxWidth(state);
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
const size = calculateThumbnailSizeForTileView({
columns: c,
minVisibleRows,
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
clientWidth,
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: true
@@ -515,6 +514,7 @@ export function computeDisplayModeFromInput(input: Object) {
canPlayEventReceived,
isRemoteParticipant,
stageParticipantsVisible,
stageFilmstrip,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
@@ -524,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
@@ -556,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;
@@ -574,6 +575,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
isScreenSharing: _isScreenSharing,
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
stageParticipantsVisible: _stageParticipantsVisible,
stageFilmstrip,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@@ -581,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';
}
/**
@@ -599,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);
}
/**
@@ -665,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;
}
@@ -703,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;
}
/**
@@ -731,3 +733,27 @@ export function isStageFilmstripEnabled(state) {
return !(filmstrip?.disableStageFilmstrip ?? true) && interfaceConfig.VERTICAL_FILMSTRIP;
}
/**
* Gets the thumbnail type by filmstrip type.
*
* @param {string} currentLayout - Current app layout.
* @param {boolean} isStageFilmstrip - Whether the filmstrip is stage filmstrip or not.
* @returns {string}
*/
export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = false) {
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
return THUMBNAIL_TYPE.TILE;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.VERTICAL;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.HORIZONTAL;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
if (isStageFilmstrip) {
return THUMBNAIL_TYPE.TILE;
}
return THUMBNAIL_TYPE.VERTICAL;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1430,14 +1430,7 @@ function _mapStateToProps(state, ownProps) {
}
}
let { toolbarButtons } = ownProps;
const stateToolbarButtons = getToolbarButtons(state);
if (toolbarButtons) {
toolbarButtons = toolbarButtons.filter(name => isToolbarButtonEnabled(name, stateToolbarButtons));
} else {
toolbarButtons = stateToolbarButtons;
}
const toolbarButtons = ownProps.toolbarButtons || getToolbarButtons(state);
return {
_backgroundType: state['features/virtual-background'].backgroundType,

View File

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

View File

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

View File

@@ -7,16 +7,7 @@ import {
getParticipantCount,
pinParticipant
} from '../base/participants';
import {
DEFAULT_MAX_COLUMNS,
ABSOLUTE_MAX_COLUMNS,
TILE_PORTRAIT_ASPECT_RATIO
} from '../filmstrip/constants';
import {
getNumberOfPartipantsForTileView,
getThumbnailMinHeight,
getTileDefaultAspectRatio
} from '../filmstrip/functions.web';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { isVideoPlaying } from '../shared-video/functions';
import { VIDEO_QUALITY_LEVELS } from '../video-quality/constants';
@@ -50,84 +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 {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
};
}
/**
* Selector for determining if the UI layout should be in tile view. Tile view
* is determined by more than just having the tile view setting enabled, as

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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