Compare commits

...

58 Commits

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

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

* remove track streaming status handler from ConnectionIndicatorIcon and ConnectionIndicatorContent

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

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

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

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

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

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

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

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

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

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

* feat(external-api): add set subtitles command
2022-04-20 11:43:18 +03:00
Seda Çağlar
aa944e76ad fix(lang) update Turkish translation 2022-04-19 17:33:24 -05:00
Hristo Terezov
4153097cc9 fix(prejoin):Disable device selection on iosSafari
It seems that showing the device selection dialog on ios Safari will
leads to not working audio. This is temporary fix until we find out
better solution.
2022-04-19 15:32:45 -05:00
Hristo Terezov
2a5be074d0 fix(video-layout): functions imports. 2022-04-19 15:15:44 -05:00
Saúl Ibarra Corretgé
2e0ae75774 fix(debian) make sure we install the latest version of luajwtjitsi
Also on update, since we might start depending on a more recent version.
2022-04-19 14:59:51 +02:00
Saúl Ibarra Corretgé
a8017149a0 fix(debian) update Prosody related dependencies 2022-04-19 14:59:51 +02:00
Jaya Allamsetty
e99fc4394d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1418.0.0+da6d04c2...v1419.0.0+607646a1
2022-04-19 08:30:19 -04:00
philip-cc
46dd88c91b feat(jwt) refactor token authentication plugin to use new luajwtjitsi version 2022-04-19 13:06:20 +02:00
Horatiu Muresan
dbc8f21b01 fix(hangup-button) Add to notify toolbar button clicked 2022-04-19 13:26:07 +03:00
William Liang
5ebe308953 fix(multi-stream) add screenshare display name to i18n 2022-04-18 17:37:22 -05:00
Jaya Allamsetty
7420113079 fix(multi-stream) Do not show join notifications for SS tiles. 2022-04-18 13:19:17 -04:00
Hristo Terezov
221ecac12d feat(tile-vew): Calculate maxColumns dynamically 2022-04-18 11:19:58 -05:00
Jaya Allamsetty
744607a5cc fix(stageFilmstrip) Disable stage filmstrip by default. 2022-04-18 09:57:25 -05:00
Horatiu Muresan
8f641b7bb1 fix(start-silent) Disable AOT mic and unmute notif when start silent 2022-04-18 17:17:51 +03:00
chipechop
045bd44407 fix(lang) update Italian translation 2022-04-18 10:55:05 +02:00
Jaya Allamsetty
13cfc3ba66 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1415.0.0+fa916d41...v1418.0.0+da6d04c2
2022-04-15 10:56:26 -04:00
Дамян Минков
bbfe7b4f32 fix: Skips clearing tracks on conference failed.
In case of conference failed as max number of occupants reached, we should skip clearing the local tracks to keep state of pre-join screen. Otherwise, on join we will join muted even though on prejoin screen it was unmuted on the initial attempt.
2022-04-15 08:48:26 -05:00
William Liang
4a375aa2a4 fix(multi-stream) update screenshare display name (#11376) 2022-04-14 13:07:17 -04:00
Robert Pintilii
a6ad592d25 feat(reactions) Open reactions menu on hover instead of click (#11364)
Fixed issue on DialogPortal where the content would flash to the initial position then move to the correct position
2022-04-13 16:18:54 +03:00
Saúl Ibarra Corretgé
00bb013373 misc(rn,app) log navigation target 2022-04-12 17:12:05 +02:00
Calinteodor
95baf34ba6 feat(participants-pane/native) adjusted styles for when local p is not a moderator (#11349)
* feat(participants-pane/native) adjusted styles for when local p is not a moderator
2022-04-12 14:45:27 +03:00
Saúl Ibarra Corretgé
64385d48e9 fix(security) hide button if the enabled flag is set to false 2022-04-12 12:26:35 +02:00
Saúl Ibarra Corretgé
5d8c87eb76 fix(rn,config) fix loading config due to broken import 2022-04-12 10:53:47 +02:00
Robert Pintilii
8bf42e79a0 fix(stage-filmstrip) Fix issues (#11360)
Fix dominant speaker not removed on leave
Fix video not shown in vertical filmstrip when a remote screensharing was on
Refactor pin/ unpin. Add click to unpin
Remove from stage on unpin, except dominant (just change pin state)
Fix local shows video on both stage and vertical filmstrip
Don't reorder on stage base on queue (sort all by id)
2022-04-12 09:57:01 +03:00
Дамян Минков
930852cd88 fix: Adds testId for context menu items and ids to some components.
Needed to revive the lobby tests.
2022-04-11 13:21:48 -05:00
Дамян Минков
fcc8e98aad Revert "fix(avatar): add ZWNJ between initials of letter avatars"
This reverts commit 6085220bfc.
2022-04-11 11:17:49 -05:00
Jaya Allamsetty
c633929c58 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1413.0.0+474b2ec7...v1415.0.0+fa916d41
2022-04-11 11:33:16 -04:00
Shahab
6085220bfc fix(avatar): add ZWNJ between initials of letter avatars 2022-04-11 14:11:58 +02:00
Saúl Ibarra Corretgé
ed6759c6cf chore(rn,deps) react-native-default-preference@1.4.4
Upstream made a release in npm, we no longer need to link to a commit.
2022-04-11 12:18:59 +02:00
Gabriel Borlea
0259d1c260 feat(rtc-stats) add timestamp to face landmarks when sending to rtc stats 2022-04-11 11:26:31 +02:00
Christoph Settgast
537d3ae53a fix(lang) update German translation
Signed-off-by: Christoph Settgast <csett86@web.de>
2022-04-11 11:20:03 +02:00
dependabot[bot]
bf463e37ca chore(deps): bump moment from 2.29.1 to 2.29.2
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 09:41:40 +02:00
William Liang
c246174555 fix(multi-stream) use highest video quality for screenshare 2022-04-08 15:18:32 -04:00
107 changed files with 2307 additions and 1126 deletions

View File

@@ -54,7 +54,11 @@ import {
sendLocalParticipant,
nonParticipantMessageReceived
} from './react/features/base/conference';
import { getReplaceParticipant, getMultipleVideoSupportFeatureFlag } from './react/features/base/config/functions';
import {
getReplaceParticipant,
getMultipleVideoSupportFeatureFlag,
getSourceNameSignalingFeatureFlag
} from './react/features/base/config/functions';
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
@@ -93,6 +97,7 @@ import {
dominantSpeakerChanged,
getLocalParticipant,
getNormalizedDisplayName,
getScreenshareParticipantByOwnerId,
localParticipantAudioLevelChanged,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
@@ -102,6 +107,7 @@ import {
participantPresenceChanged,
participantRoleChanged,
participantUpdated,
screenshareParticipantDisplayNameChanged,
updateRemoteParticipantFeatures
} from './react/features/base/participants';
import {
@@ -2258,6 +2264,17 @@ export default {
id,
name: formattedDisplayName
}));
if (getSourceNameSignalingFeatureFlag(state)) {
const screenshareParticipantId = getScreenshareParticipantByOwnerId(state, id)?.id;
if (screenshareParticipantId) {
APP.store.dispatch(
screenshareParticipantDisplayNameChanged(screenshareParticipantId, formattedDisplayName)
);
}
}
APP.API.notifyDisplayNameChanged(id, {
displayName: formattedDisplayName,
formattedDisplayName:

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

@@ -46,18 +46,12 @@
}
.audio-preview > div:nth-child(2),
.video-preview > div:nth-child(2),
.reactions-menu-popup > div:nth-child(2) {
.video-preview > div:nth-child(2) {
margin-bottom: 4px;
outline: none;
padding: 0;
}
.reactions-menu-popup > div:nth-child(2) {
margin-bottom: 6px;
box-shadow: none;
}
/**
* The following selectors keep the chat modal full-size anywhere between 100px
* and 580px for desktop or 680px for mobile.

View File

@@ -104,6 +104,10 @@
}
}
.reactions-menu-container {
padding-bottom: 6px;
}
.reactions-animations-container {
position: absolute;
width: 20%;
@@ -112,8 +116,7 @@
height: 0;
}
.reactions-menu-popup-container,
.reactions-menu-popup {
.reactions-menu-popup-container {
display: inline-block;
position: relative;
}

View File

@@ -60,3 +60,15 @@
}
}
}
.settings-button-small-icon-container {
position: absolute;
right: -4px;
top: -3px;
& .settings-button-small-icon {
position: relative;
top: 0;
right: 0;
}
}

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

@@ -421,7 +421,7 @@ PODS:
- React-Core
- RNCMaskedView (0.2.6):
- React-Core
- RNDefaultPreference (1.4.3):
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (8.4.8):
- React-Core
@@ -723,7 +723,7 @@ SPEC CHECKSUMS:
RNCAsyncStorage: ea6b5c280997b2b32a587793163b1f10e580c4f7
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd
RNDefaultPreference: 326860d42a681bfd7338c8f6d061cf58745bd860
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
RNGestureHandler: e5c7cab5f214503dcefd6b2b0cefb050e1f51c4a
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c

View File

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

View File

@@ -31,6 +31,7 @@
},
"audioDevices": {
"bluetooth": "Bluetooth",
"car": "Auto",
"headphones": "Kopfhörer",
"none": "Keine Audiogeräte verfügbar",
"phone": "Hörer",
@@ -216,6 +217,8 @@
"liveStreaming": "Livestream"
},
"add": "Hinzufügen",
"addMeetingNote": "Notiz zu dieser Konferenz hinzufügen",
"addOptionalNote": "Notiz hinzufügen (optional):",
"allow": "Erlauben",
"alreadySharedVideoMsg": "Eine andere Person gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
@@ -267,6 +270,8 @@
"kickParticipantDialog": "Wollen Sie diese Person wirklich entfernen?",
"kickParticipantTitle": "Person entfernen?",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus dem Meeting geworfen",
"linkMeeting": "Konferenz verlinken",
"linkMeetingTitle": "Konferenz mit Salesforce verlinken",
"liveStreaming": "Livestreaming",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Während einer Aufnahme nicht möglich",
"liveStreamingDisabledTooltip": "Starten des Livestreams deaktiviert.",
@@ -303,11 +308,11 @@
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Person stummschalten?",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteParticipantsVideoBodyModerationOn": "Sie können die Kamera nicht wieder aktivieren und die Person selbst auch nicht.",
"muteParticipantsVideoButton": "Kamera ausschalten",
"muteParticipantsVideoDialog": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamera nicht wieder aktivieren, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantsVideoDialogModerationOn": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamea nicht wieder aktivieren und die Person selbst auch nicht.",
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
"noDropboxToken": "Kein gültiges Dropbox-Token",
"password": "Passwort",
@@ -321,6 +326,7 @@
"popupError": "Ihr Browser blockiert Pop-ups von dieser Website. Bitte aktivieren Sie Pop-ups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
"popupErrorTitle": "Pop-up blockiert",
"readMore": "mehr",
"recentlyUsedObjects": "Ihre zuletzt verwendeten Objekte",
"recording": "Aufnahme",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Während eines Livestreams nicht möglich",
"recordingDisabledTooltip": "Start der Aufzeichnung deaktiviert.",
@@ -343,6 +349,12 @@
"screenSharingFailed": "Ups! Beim Teilen des Bildschirms ist etwas schiefgegangen!",
"screenSharingFailedTitle": "Bildschirmfreigabe fehlgeschlagen!",
"screenSharingPermissionDeniedError": "Ups! Etwas stimmt nicht mit Ihren Berechtigungen zur Bildschirmfreigabe. Bitte neu laden und erneut versuchen.",
"searchInSalesforce": "In Salesforce suchen",
"searchResults": "Suchergebnisse({{count}})",
"searchResultsDetailsError": "Beim Abrufen der Daten des Besitzers ist ein Fehler aufgetreten.",
"searchResultsError": "Beim Abrufen der Daten ist ein Fehler aufgetreten.",
"searchResultsNotFound": "Keine Suchergebnisse.",
"searchResultsTryAgain": "Versuchen Sie es mit anderen Stichwörtern.",
"sendPrivateMessage": "Sie haben kürzlich eine private Nachricht erhalten. Hatten Sie die Absicht, darauf privat zu antworten, oder wollen Sie Ihre Nachricht an die Gruppe senden?",
"sendPrivateMessageCancel": "An die Gruppe senden",
"sendPrivateMessageOk": "Privat antworten",
@@ -410,6 +422,10 @@
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
},
"giphy": {
"noResults": "Keine Ergebnisse :(",
"search": "GIPHY durchsuchen"
},
"helpView": {
"header": "Hilfecenter"
},
@@ -476,6 +492,7 @@
"focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video einer anderen Person fokussieren",
"fullScreen": "Vollbildmodus aktivieren oder deaktivieren",
"giphyMenu": "GIPHY ein- oder ausblenden",
"keyboardShortcuts": "Tastenkürzel",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein- oder ausblenden",
"mute": "Stummschaltung aktivieren oder deaktivieren",
@@ -543,6 +560,7 @@
"errorMissingPassword": "Bitte das Konferenzpasswort eingeben",
"invalidPassword": "Ungültiges Passwort",
"joinRejectedMessage": "Ihre Beitrittsanfrage wurde von der Moderation abgelehnt.",
"joinRejectedTitle": "Beitrittsanfrage abgelehnt.",
"joinTitle": "Konferenz beitreten",
"joinWithPasswordMessage": "Beitrittsversuch mit Passwort, bitte warten …",
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt.",
@@ -615,6 +633,7 @@
"displayNotifications": "Benachrichtigungen anzeigen für",
"focus": "Konferenzleitung",
"focusFail": "{{component}} ist im Moment nicht verfügbar wiederholen in {{ms}} Sekunden",
"gifsMenu": "GIPHY",
"groupTitle": "Benachrichtigungen",
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
"invitedOneMember": "{{name}} wurde eingeladen",
@@ -624,6 +643,12 @@
"leftOneMember": "{{name}} hat die Konferenz verlassen",
"leftThreePlusMembers": "{{name}} und Weitere haben die Konferenz verlassen",
"leftTwoMembers": "{{first}} und {{second}} haben die Konferenz verlassen",
"linkToSalesforce": "Mit Salesforce verlinken",
"linkToSalesforceDescription": "Sie können die Zusammenfassung der Konferenz mit einem Objekt bei Salesforce verlinken.",
"linkToSalesforceError": "Konferenz konnte nicht mit Salesforce verlinkt werden",
"linkToSalesforceKey": "Konferenz verlinken",
"linkToSalesforceProgress": "Konferenz wird mit Salesforce verlinkt...",
"linkToSalesforceSuccess": "Die Konferenz wurde mit Salesforce verlinkt",
"me": "Ich",
"moderationInEffectCSDescription": "Bitte melden um ein Video zu teilen",
"moderationInEffectCSTitle": "Die Videofreigabe ist von der Moderation gesperrt",
@@ -702,6 +727,7 @@
},
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
"passwordSetRemotely": "von einer anderen Person gesetzt",
"pinnedParticipant": "Die Person ist angeheftet",
"polls": {
"answer": {
"skip": "Überspringen",
@@ -817,6 +843,18 @@
},
"raisedHand": "Ich möchte sprechen",
"raisedHandsLabel": "Anzahl gehobener Hände",
"record": {
"already": {
"linked": "Diese Konferenz ist bereits mit einem Objekt bei Salesforce verlinkt."
},
"type": {
"account": "Account",
"contact": "Contact",
"lead": "Lead",
"opportunity": "Opportunity",
"owner": "Owner"
}
},
"recording": {
"authDropboxText": "In Dropbox hochladen",
"availableSpace": "Verfügbarer Speicherplatz: {{spaceLeft}} MB (ca. {{duration}} Minuten Aufzeichnung)",
@@ -831,6 +869,11 @@
"expandedPending": "Aufzeichnung wird gestartet…",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
"highlight": "Highlight",
"highlightMoment": "Moment als Highlight festhalten",
"highlightMomentDisabled": "Sie können Momente als Highlights festhalten, sobald die Aufnahme startet",
"highlightMomentSuccess": "Highlight festgehalten",
"highlightMomentSucessDescription": "Ihr festgehaltener Moment wird zur Zusammenfassung des Meeting hinzugefügt.",
"inProgress": "Aufzeichnung gestartet",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
@@ -845,6 +888,7 @@
"rec": "AUFZ",
"serviceDescription": "Ihre Aufzeichnung wird vom Aufzeichnungsdienst gespeichert",
"serviceDescriptionCloud": "Cloud-Aufzeichnung",
"serviceDescriptionCloudInfo": "Aufzeichnungen werden 24 Stunden nach Aufzeichnungsende automatisch gelöscht.",
"serviceName": "Aufnahmedienst",
"sessionAlreadyActive": "Diese Konferenz wird bereits aufgezeichnet.",
"signIn": "Anmelden",
@@ -879,6 +923,7 @@
"incomingMessage": "Eingehende Nachricht",
"language": "Sprache",
"loggedIn": "Als {{name}} angemeldet",
"maxStageParticipants": "Maximale Anzahl an Personen, die zur Hauptansicht angeheftet werden können",
"microphones": "Mikrofon",
"moderator": "Moderation",
"more": "Mehr",
@@ -977,6 +1022,7 @@
"expand": "Ausklappen",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"giphy": "GIPHY ein-/ausschalten",
"grantModerator": "Moderationsrechte vergeben",
"hangup": "Konferenz verlassen",
"help": "Hilfe",
@@ -984,6 +1030,7 @@
"kick": "Person entfernen",
"laugh": "Lachen",
"like": "Daumen nach oben",
"linkToSalesforce": "Mit Salesforce verlinken",
"lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Konferenzpasswort ein-/ausschalten",
@@ -993,8 +1040,8 @@
"mute": "Mikrofon aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteEveryoneElsesVideoStream": "Alle anderen Kameras ausschalten",
"muteEveryonesVideoStream": "Alle Kameras ausschalten",
"participants": "Anwesende",
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
@@ -1045,6 +1092,7 @@
"exitFullScreen": "Vollbildmodus verlassen",
"exitTileView": "Kachelansicht ausschalten",
"feedback": "Feedback hinterlassen",
"giphy": "GIPHY ein-/ausschalten",
"hangup": "Konferenz verlassen",
"help": "Hilfe",
"invite": "Personen einladen",
@@ -1052,6 +1100,7 @@
"laugh": "Lachen",
"leaveBreakoutRoom": "Breakout-Raum verlassen",
"like": "Daumen hoch",
"linkToSalesforce": "Mit Salesforce verknüpfen",
"lobbyButtonDisable": "Lobbymodus deaktivieren",
"lobbyButtonEnable": "Lobbymodus aktivieren",
"login": "Anmelden",
@@ -1171,10 +1220,12 @@
"moderator": "Moderation",
"mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet",
"pinToStage": "Anheften",
"remoteControl": "Fernsteuerung",
"screenSharing": "Person teilt den Bildschirm",
"show": "Im Vordergrund anzeigen",
"showSelfView": "Eigene Ansicht anzeigen",
"unpinFromStage": "Lösen",
"videoMuted": "Kamera ausgeschaltet",
"videomute": "Person hat die Kamera angehalten"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

79
package-lock.json generated
View File

@@ -72,10 +72,10 @@
"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/v1413.0.0+474b2ec7/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.1",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
"optional-require": "1.0.3",
"promise.allsettled": "1.0.4",
@@ -91,7 +91,7 @@
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
"react-native-collapsible": "1.6.0",
"react-native-default-preference": "https://git@github.com/kevinresol/react-native-default-preference#11bff5eb05cb04fd8d35b5e761eeee80525e8c6c",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "9.2.1",
"react-native-gesture-handler": "2.1.0",
@@ -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/v1413.0.0+474b2ec7/lib-jitsi-meet.tgz",
"integrity": "sha512-7jVHp71Xo32NjLNX0OaqzXPMqVhAUKP+BxX8v3iskfmLGF9bvjIx0LntB6EJB8ApvS4J2ir5IJx7+EP1nBXt7Q==",
"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"
}
@@ -12781,9 +12781,9 @@
}
},
"node_modules/moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
"engines": {
"node": "*"
}
@@ -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"
@@ -15069,10 +15069,9 @@
}
},
"node_modules/react-native-default-preference": {
"version": "1.4.3",
"resolved": "git+https://git@github.com/kevinresol/react-native-default-preference.git#11bff5eb05cb04fd8d35b5e761eeee80525e8c6c",
"integrity": "sha512-vmUyt63mLc+xebOOWrZxTF7o7AdWQqzy6lUn7pgjnyUd93//AOpQ6iXGijL9KpNiOv8mDKWAPZKhLY1XVuzZwA==",
"license": "MIT",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/react-native-default-preference/-/react-native-default-preference-1.4.4.tgz",
"integrity": "sha512-h0vtgiSKws3UmMRJykXAVM4ne1SgfoocUcoBD19ewRpQd6wqurE0HJRQGrSxcHK5LdKE7QPSIB1VX3YGIVS8Jg==",
"peerDependencies": {
"react-native": ">=0.47.0"
}
@@ -24420,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",
@@ -28900,14 +28899,14 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1413.0.0+474b2ec7/lib-jitsi-meet.tgz",
"integrity": "sha512-7jVHp71Xo32NjLNX0OaqzXPMqVhAUKP+BxX8v3iskfmLGF9bvjIx0LntB6EJB8ApvS4J2ir5IJx7+EP1nBXt7Q==",
"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",
@@ -29248,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"
}
@@ -29728,9 +29727,9 @@
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
},
"moment-duration-format": {
"version": "2.2.2",
@@ -30536,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"
@@ -31505,9 +31504,9 @@
"integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg=="
},
"react-native-default-preference": {
"version": "git+https://git@github.com/kevinresol/react-native-default-preference.git#11bff5eb05cb04fd8d35b5e761eeee80525e8c6c",
"integrity": "sha512-vmUyt63mLc+xebOOWrZxTF7o7AdWQqzy6lUn7pgjnyUd93//AOpQ6iXGijL9KpNiOv8mDKWAPZKhLY1XVuzZwA==",
"from": "react-native-default-preference@https://git@github.com/kevinresol/react-native-default-preference#11bff5eb05cb04fd8d35b5e761eeee80525e8c6c"
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/react-native-default-preference/-/react-native-default-preference-1.4.4.tgz",
"integrity": "sha512-h0vtgiSKws3UmMRJykXAVM4ne1SgfoocUcoBD19ewRpQd6wqurE0HJRQGrSxcHK5LdKE7QPSIB1VX3YGIVS8Jg=="
},
"react-native-device-info": {
"version": "8.4.8",

View File

@@ -77,10 +77,10 @@
"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/v1413.0.0+474b2ec7/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.1",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
"optional-require": "1.0.3",
"promise.allsettled": "1.0.4",
@@ -96,7 +96,7 @@
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
"react-native-collapsible": "1.6.0",
"react-native-default-preference": "https://git@github.com/kevinresol/react-native-default-preference#11bff5eb05cb04fd8d35b5e761eeee80525e8c6c",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "9.2.1",
"react-native-gesture-handler": "2.1.0",

View File

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

View File

@@ -12,7 +12,7 @@ import {
storeConfig
} from '../base/config';
import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { loadConfig } from '../base/lib-jitsi-meet/functions.native';
import { createDesiredLocalTracks } from '../base/tracks';
import {
getBackendSafeRoomName,
@@ -39,6 +39,8 @@ export * from './actions.any';
* @returns {Function}
*/
export function appNavigate(uri: ?string) {
logger.info(`appNavigate to ${uri}`);
return async (dispatch: Dispatch<any>, getState: Function) => {
let location = parseURIString(uri);

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

@@ -56,6 +56,11 @@ export type Props = {
*/
onKeyPress?: Function,
/**
* TestId of the element, if any.
*/
testId?: string,
/**
* Action text.
*/
@@ -112,6 +117,7 @@ const ContextMenuItem = ({
onClick,
onKeyDown,
onKeyPress,
testId,
text,
textClassName }: Props) => {
const styles = useStyles();
@@ -126,6 +132,7 @@ const ContextMenuItem = ({
disabled && styles.contextMenuItemDisabled,
className
) }
data-testid = { testId }
id = { id }
key = { text }
onClick = { disabled ? undefined : onClick }

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

@@ -173,6 +173,17 @@ export const HIDDEN_PARTICIPANT_LEFT = 'HIDDEN_PARTICIPANT_LEFT';
*/
export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
/**
* The type of Redux action which notifies that the screenshare participant's display name has changed.
*
* {
* type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
* id: string,
* name: string
* }
*/
export const SCREENSHARE_PARTICIPANT_NAME_CHANGED = 'SCREENSHARE_PARTICIPANT_NAME_CHANGED';
/**
* Raises hand for the local participant.
* {

View File

@@ -16,6 +16,7 @@ import {
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
SET_LOADABLE_AVATAR_URL,
RAISE_HAND_UPDATED
} from './actionTypes';
@@ -432,6 +433,25 @@ export function participantRoleChanged(id, role) {
});
}
/**
* Action to signal that a participant's display name has changed.
*
* @param {string} id - Screenshare participant's ID.
* @param {name} name - The new display name of the screenshare participant's owner.
* @returns {{
* type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
* id: string,
* name: string
* }}
*/
export function screenshareParticipantDisplayNameChanged(id, name) {
return {
type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
id,
name
};
}
/**
* Action to signal that some of participant properties has been changed.
*

View File

@@ -3,7 +3,8 @@
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { i18next } from '../../base/i18n';
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
@@ -105,6 +106,22 @@ export function getLocalScreenShareParticipant(stateful: Object | Function) {
return state.localScreenShare;
}
/**
* Returns screenshare participant.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state features/base/participants.
* @param {string} id - The owner ID of the screenshare participant to retrieve.
* @returns {(Participant|undefined)}
*/
export function getScreenshareParticipantByOwnerId(stateful: Object | Function, id: string) {
const track = getTrackByMediaTypeAndParticipant(
toState(stateful)['features/base/tracks'], MEDIA_TYPE.SCREENSHARE, id
);
return getParticipantById(stateful, track?.jitsiTrack.getSourceName());
}
/**
* Normalizes a display name so then no invalid values (padding, length...etc)
* can be set.
@@ -244,14 +261,12 @@ export function getParticipantCountWithFake(stateful: Object | Function) {
/**
* Returns participant's display name.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @param {string} id - The ID of the participant's display name to retrieve.
* @returns {string}
*/
export function getParticipantDisplayName(
stateful: Object | Function,
id: string) {
export function getParticipantDisplayName(stateful: Object | Function, id: string) {
const participant = getParticipantById(stateful, id);
const {
defaultLocalDisplayName,
@@ -259,6 +274,10 @@ export function getParticipantDisplayName(
} = toState(stateful)['features/base/config'];
if (participant) {
if (participant.isFakeScreenShareParticipant) {
return getScreenshareParticipantDisplayName(stateful, id);
}
if (participant.name) {
return participant.name;
}
@@ -271,6 +290,21 @@ export function getParticipantDisplayName(
return defaultRemoteDisplayName;
}
/**
* Returns screenshare participant's display name.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @param {string} id - The ID of the screenshare participant's display name to retrieve.
* @returns {string}
*/
export function getScreenshareParticipantDisplayName(stateful: Object | Function, id: string) {
const owner = getParticipantById(stateful, getFakeScreenShareParticipantOwnerId(id));
const name = owner.name;
return i18next.t('screenshareDisplayName', { name });
}
/**
* Returns the presence status of a participant associated with the passed id.
*
@@ -338,7 +372,7 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
export function getPinnedParticipant(stateful: Object | Function) {
const state = toState(stateful);
const { pinnedParticipant } = state['features/base/participants'];
const stageFilmstrip = isStageFilmstripEnabled(state);
const stageFilmstrip = isStageFilmstripAvailable(state);
if (stageFilmstrip) {
const { activeParticipants } = state['features/filmstrip'];

View File

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

View File

@@ -13,6 +13,7 @@ import {
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
RAISE_HAND_UPDATED,
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
@@ -209,6 +210,23 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
...state
};
}
case SCREENSHARE_PARTICIPANT_NAME_CHANGED: {
const { id, name } = action;
if (state.sortedRemoteFakeScreenShareParticipants.has(id)) {
state.sortedRemoteFakeScreenShareParticipants.delete(id);
const sortedRemoteFakeScreenShareParticipants = [ ...state.sortedRemoteFakeScreenShareParticipants ];
sortedRemoteFakeScreenShareParticipants.push([ id, name ]);
sortedRemoteFakeScreenShareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
}
return { ...state };
}
case PARTICIPANT_JOINED: {
const participant = _participantJoined(action);
const { id, isFakeParticipant, isFakeScreenShareParticipant, isLocalScreenShare, name, pinned } = participant;

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

@@ -0,0 +1,138 @@
// @flow
import React from 'react';
import { Icon } from '../../../icons';
import { Popover } from '../../../popover';
type Props = {
/**
* Whether the element popup is expanded.
*/
ariaExpanded?: boolean,
/**
* The id of the element this button icon controls.
*/
ariaControls?: string,
/**
* Whether the element has a popup.
*/
ariaHasPopup?: boolean,
/**
* Aria label for the Icon.
*/
ariaLabel?: string,
/**
* The decorated component (ToolboxButton).
*/
children: React$Node,
/**
* Icon of the button.
*/
icon: Function,
/**
* Flag used for disabling the small icon.
*/
iconDisabled: boolean,
/**
* The ID of the icon button.
*/
iconId: string,
/**
* Popover close callback.
*/
onPopoverClose: Function,
/**
* Popover open callback.
*/
onPopoverOpen: Function,
/**
* The content that will be displayed inside the popover.
*/
popoverContent: React$Node,
/**
* Additional styles.
*/
styles?: Object,
/**
* Whether or not the popover is visible.
*/
visible: boolean
};
declare var APP: Object;
/**
* Displays the `ToolboxButtonWithIcon` component.
*
* @param {Object} props - Component's props.
* @returns {ReactElement}
*/
export default function ToolboxButtonWithIconPopup(props: Props) {
const {
ariaControls,
ariaExpanded,
ariaHasPopup,
ariaLabel,
children,
icon,
iconDisabled,
iconId,
onPopoverClose,
onPopoverOpen,
popoverContent,
styles,
visible
} = props;
const iconProps = {};
if (iconDisabled) {
iconProps.className
= 'settings-button-small-icon settings-button-small-icon--disabled';
} else {
iconProps.className = 'settings-button-small-icon';
iconProps.role = 'button';
iconProps.tabIndex = 0;
iconProps.ariaControls = ariaControls;
iconProps.ariaExpanded = ariaExpanded;
iconProps.containerId = iconId;
}
return (
<div
className = 'settings-button-container'
styles = { styles }>
{children}
<div className = 'settings-button-small-icon-container'>
<Popover
content = { popoverContent }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = 'top'
visible = { visible }>
<Icon
{ ...iconProps }
ariaHasPopup = { ariaHasPopup }
ariaLabel = { ariaLabel }
size = { 9 }
src = { icon } />
</Popover>
</div>
</div>
);
}

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';
@@ -318,9 +319,10 @@ MiddlewareRegistry.register(store => next => action => {
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch, getState }, prevConference) => {
const { authRequired, error } = getState()['features/base/conference'];
// conference keep flipping while we are authenticating, skip clearing while we are in that process
if (prevConference && !conference && !getState()['features/base/conference'].authRequired) {
if (prevConference && !conference && !authRequired && !error) {
// Clear all tracks.
const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
@@ -330,6 +332,7 @@ StateListenerRegistry.register(
for (const track of remoteTracks) {
dispatch(trackRemoved(track.jitsiTrack));
}
dispatch({ type: _RESET_BREAKOUT_ROOMS });
});
}
});
@@ -400,7 +403,7 @@ function createFakeScreenShareParticipant({ dispatch, getState }, { track }) {
id: track.jitsiTrack.getSourceName(),
isFakeScreenShareParticipant: true,
isLocalScreenShare: track?.jitsiTrack.isLocal(),
name: `${participant.name}'s screen`
name: participant.name
}));
} else {
logger.error(`Failed to create a screenshare participant for participantId: ${participantId}`);

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

@@ -6,7 +6,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { isDisplayNameVisible } from '../../../base/config/functions.any';
import { getLocalParticipant } from '../../../base/participants';
import { getLocalParticipant, getParticipantDisplayName } from '../../../base/participants';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { getLargeVideoParticipant } from '../../../large-video/functions';
import { isToolboxVisible } from '../../../toolbox/functions.web';
@@ -44,8 +44,8 @@ const useStyles = makeStyles(theme => {
const StageParticipantNameLabel = () => {
const classes = useStyles();
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
const nameToDisplay = largeVideoParticipant?.name;
const selectedId = largeVideoParticipant?.id;
const nameToDisplay = useSelector(state => getParticipantDisplayName(state, selectedId));
const localParticipant = useSelector(getLocalParticipant);
const localId = localParticipant?.id;

View File

@@ -170,3 +170,20 @@ 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.
* {
* type: TOGGLE_PIN_STAGE_PARTICIPANT,
* participantId: String
* }
*/
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

@@ -23,7 +23,9 @@ import {
SET_USER_IS_RESIZING,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS
SET_MAX_STAGE_PARTICIPANTS,
TOGGLE_PIN_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
@@ -82,14 +84,15 @@ export function setTileViewDimensions() {
disableTileEnlargement,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles
desiredNumberOfVisibleTiles: numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
const hasScroll = availableHeight < thumbnailsTotalHeight;
const filmstripWidth
= Math.min(clientWidth - TILE_VIEW_GRID_HORIZONTAL_MARGIN, columns * (TILE_HORIZONTAL_MARGIN + width))
+ (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN, thumbnailsTotalHeight);
const filmstripHeight = Math.min(availableHeight, thumbnailsTotalHeight);
dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
@@ -138,7 +141,11 @@ export function setVerticalViewDimensions() {
const { tileView = {} } = state['features/base/config'];
const { numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES } = tileView;
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
const maxColumns = getMaxColumnCount(state);
const maxColumns = getMaxColumnCount(state, {
width: filmstripWidth.current,
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const {
height,
width,
@@ -151,7 +158,7 @@ export function setVerticalViewDimensions() {
maxColumns,
noHorizontalContainerMargin: true,
numberOfParticipants,
numberOfVisibleTiles
desiredNumberOfVisibleTiles: numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
@@ -260,32 +267,33 @@ export function setStageFilmstripViewDimensions() {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const {
disableResponsiveTiles,
disableTileEnlargement,
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
const maxColumns = getMaxColumnCount(state);
const availableWidth = clientWidth - verticalWidth;
const maxColumns = getMaxColumnCount(state, {
width: availableWidth,
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const {
height,
width,
columns,
rows
} = disableResponsiveTiles
? calculateNonResponsiveTileViewDimensions(state, true)
: calculateResponsiveTileViewDimensions({
clientWidth: clientWidth - verticalWidth,
clientHeight,
disableTileEnlargement,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
} = calculateResponsiveTileViewDimensions({
clientWidth: availableWidth,
clientHeight,
disableTileEnlargement: false,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth
@@ -449,3 +457,27 @@ export function setMaxStageParticipants(maxParticipants) {
maxParticipants
};
}
/**
* Toggles the pin state of the given participant.
*
* @param {string} participantId - The id of the participant to be toggled.
* @returns {Object}
*/
export function togglePinStageParticipant(participantId) {
return {
type: TOGGLE_PIN_STAGE_PARTICIPANT,
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,8 +9,10 @@ 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,
getParticipantByIdOrUndefined,
hasRaisedHand,
pinParticipant
@@ -23,34 +25,38 @@ import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack,
updateLastTrackVideoMediaEvent
updateLastTrackVideoMediaEvent,
trackStreamingStatusChanged
} from '../../../base/tracks';
import { getVideoObjectPosition } from '../../../face-landmarks/functions';
import { hideGif, showGif } from '../../../gifs/actions';
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { addStageParticipant } from '../../actions.web';
import { togglePinStageParticipant } from '../../actions';
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,
isStageFilmstripAvailable,
showGridInVerticalView
} from '../../functions';
import { isStageFilmstripEnabled } from '../../functions.web';
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
declare var interfaceConfig: Object;
/**
@@ -89,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.
*/
@@ -187,9 +188,21 @@ 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.
* (and not screensharing or shared video; used to determine
* whether or not the display the participant video in the vertical filmstrip).
*/
_stageParticipantsVisible: boolean,
/**
* The type of thumbnail to display.
*/
_thumbnailType: string,
/**
* The video object position for the participant.
@@ -234,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 => {
@@ -394,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);
}
/**
@@ -404,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()));
}
}
/**
@@ -417,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));
}
/**
@@ -438,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({
@@ -521,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
});
@@ -541,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,
@@ -555,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;
@@ -578,7 +661,7 @@ class Thumbnail extends Component<Props, State> {
}
let videoStyles = null;
const doNotStretchVideo = (isPortraitVideo && tileViewActive)
const doNotStretchVideo = (isPortraitVideo && isTileType)
|| _disableTileEnlargement
|| _isScreenSharing;
@@ -627,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));
if (_stageFilmstripLayout) {
dispatch(togglePinStageParticipant(id));
} else {
dispatch(addStageParticipant(id, true));
dispatch(pinParticipant(pinned ? null : id));
}
}
@@ -781,8 +864,8 @@ class Thumbnail extends Component<Props, State> {
const {
_isDominantSpeakerDisabled,
_participant,
_currentLayout,
_raisedHand,
_thumbnailType,
classes
} = this.props;
@@ -795,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';
}
@@ -893,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;
@@ -966,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 && (
@@ -1094,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 {
@@ -1107,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: {},
@@ -1124,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) ?? {};
@@ -1146,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 = {
@@ -1173,14 +1258,14 @@ function _mapStateToProps(state, ownProps): Object {
const { gifUrl: gifSrc } = getGifForParticipant(state, id);
const mode = getGifDisplayMode(state);
const participantId = isLocal ? getLocalParticipant(state).id : participantID;
return {
_audioTrack,
_currentLayout,
_defaultLocalDisplayName: defaultLocalDisplayName,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
_disableTileEnlargement: Boolean(disableTileEnlargement),
_isActiveParticipant: activeParticipants.find(pId => pId === participantID),
_isActiveParticipant: activeParticipants.find(pId => pId === participantId),
_isHidden: isLocal && iAmRecorder && !iAmSipGateway,
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
@@ -1194,11 +1279,14 @@ function _mapStateToProps(state, ownProps): Object {
_localFlipX: Boolean(localFlipX),
_participant: participant,
_raisedHand: hasRaisedHand(participant),
_stageFilmstripDisabled: !isStageFilmstripEnabled(state),
_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

@@ -148,7 +148,8 @@ function _mapStateToProps(state, ownProps) {
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _verticalViewGrid = showGridInVerticalView(state);
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const remoteParticipants = stageFilmstrip ? activeParticipants : remote;
const sortedActiveParticipants = activeParticipants.sort();
const remoteParticipants = stageFilmstrip ? sortedActiveParticipants : remote;
const remoteParticipantsLength = remoteParticipants.length;
const localId = getLocalParticipant(state).id;

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ import {
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
THUMBNAIL_TYPE,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_LARGE,
@@ -243,18 +244,16 @@ export function getNumberOfPartipantsForTileView(state) {
* disabled.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} - The dimensions.
*/
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
export function calculateNonResponsiveTileViewDimensions(state) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableTileEnlargement } = state['features/base/config'];
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
const filmstripWidth = getVerticalViewMaxWidth(state);
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
const size = calculateThumbnailSizeForTileView({
columns: c,
minVisibleRows,
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
clientWidth,
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: true
@@ -297,7 +296,7 @@ export function calculateResponsiveTileViewDimensions({
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
}) {
let height, width;
let columns, rows;
@@ -311,12 +310,12 @@ export function calculateResponsiveTileViewDimensions({
maxArea: 0
};
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants); c++) {
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants, desiredNumberOfVisibleTiles); c++) {
const r = Math.ceil(numberOfParticipants / c);
// we want to display as much as possible tumbnails up to numberOfVisibleTiles
// we want to display as much as possible tumbnails up to desiredNumberOfVisibleTiles
const visibleRows
= numberOfParticipants <= numberOfVisibleTiles ? r : Math.floor(numberOfVisibleTiles / c);
= numberOfParticipants <= desiredNumberOfVisibleTiles ? r : Math.floor(desiredNumberOfVisibleTiles / c);
const size = calculateThumbnailSizeForTileView({
columns: c,
@@ -330,18 +329,38 @@ export function calculateResponsiveTileViewDimensions({
if (size) {
const { height: currentHeight, width: currentWidth, minHeightEnforced, maxVisibleRows } = size;
let area = currentHeight * currentWidth * Math.min(c * maxVisibleRows, numberOfParticipants);
const numberOfVisibleParticipants = Math.min(c * maxVisibleRows, numberOfParticipants);
let area = Math.round(
(currentHeight + TILE_VERTICAL_MARGIN)
* (currentWidth + TILE_HORIZONTAL_MARGIN)
* numberOfVisibleParticipants);
const currentDimensions = {
maxArea: area,
height: currentHeight,
width: currentWidth,
columns: c,
rows: r
rows: r,
numberOfVisibleParticipants
};
const { numberOfVisibleParticipants: oldNumberOfVisibleParticipants = 0 } = dimensions;
if (!minHeightEnforced && area > dimensions.maxArea) {
dimensions = currentDimensions;
} else if (minHeightEnforced && area > minHeightEnforcedDimensions.maxArea) {
if (!minHeightEnforced) {
if (area > dimensions.maxArea) {
dimensions = currentDimensions;
} else if ((area === dimensions.maxArea)
&& ((oldNumberOfVisibleParticipants > desiredNumberOfVisibleTiles
&& oldNumberOfVisibleParticipants >= numberOfParticipants)
|| (oldNumberOfVisibleParticipants < numberOfParticipants
&& numberOfVisibleParticipants <= desiredNumberOfVisibleTiles))
) { // If the area of the new candidates and the old ones are equal we preffer the one that will have
// closer number of visible participants to desiredNumberOfVisibleTiles config.
dimensions = currentDimensions;
}
} else if (minHeightEnforced && area >= minHeightEnforcedDimensions.maxArea) {
// If we choose configuration with minHeightEnforced there will be less than desiredNumberOfVisibleTiles
// visible tiles, that's why we prefer more columns when the area is the same.
minHeightEnforcedDimensions = currentDimensions;
} else if (minHeightEnforced && maxVisibleRows === 0) {
area = currentHeight * currentWidth * Math.min(c, numberOfParticipants);
@@ -400,7 +419,8 @@ export function calculateThumbnailSizeForTileView({
const minHeight = getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
const viewHeight = availableHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
const initialWidth = viewWidth / columns;
let initialHeight = viewHeight / minVisibleRows;
let minHeightEnforced = false;
@@ -417,52 +437,47 @@ export function calculateThumbnailSizeForTileView({
return;
}
const height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
const height = Math.min(aspectRatioHeight, initialHeight);
return {
height,
width: Math.floor(aspectRatio * height),
width: aspectRatio * height,
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
};
}
const initialRatio = initialWidth / initialHeight;
let height = Math.floor(initialHeight);
let height = initialHeight;
let width;
// The biggest area of the grid will be when the grid's height is equal to clientHeight or when the grid's width is
// equal to clientWidth.
if (initialRatio > aspectRatio) {
return {
height,
width: Math.floor(initialHeight * aspectRatio),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialHeight * aspectRatio;
} else if (initialRatio >= TILE_PORTRAIT_ASPECT_RATIO) {
return {
height,
width: Math.floor(initialWidth),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialWidth;
// eslint-disable-next-line no-negated-condition
} else if (!minHeightEnforced) {
height = Math.floor(initialWidth / TILE_PORTRAIT_ASPECT_RATIO);
height = initialWidth / TILE_PORTRAIT_ASPECT_RATIO;
if (height >= minHeight) {
return {
height,
width: Math.floor(initialWidth),
minHeightEnforced,
maxVisibleRows: Math.floor(viewHeight / height)
};
width = initialWidth;
} else { // The width is so small that we can't reach the minimum height with portrait aspect ratio.
return;
}
} else {
// We can't fit that number of columns with the desired min height and aspect ratio.
return;
}
// else
// We can't fit that number of columns with the desired min height and aspect ratio.
return;
return {
height,
width,
minHeightEnforced,
maxVisibleRows: Math.floor(availableHeight / (height + TILE_VERTICAL_MARGIN))
};
}
/**
@@ -498,6 +513,8 @@ export function computeDisplayModeFromInput(input: Object) {
isScreenSharing,
canPlayEventReceived,
isRemoteParticipant,
stageParticipantsVisible,
stageFilmstrip,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
@@ -506,7 +523,8 @@ export function computeDisplayModeFromInput(input: Object) {
return DISPLAY_VIDEO;
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant) || isActiveParticipant)) {
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant && !stageFilmstrip))) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@@ -537,7 +555,9 @@ export function getDisplayModeInput(props: Object, state: Object) {
_isScreenSharing,
_isVideoPlayable,
_participant,
_videoTrack
_stageParticipantsVisible,
_videoTrack,
stageFilmstrip
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived } = state;
@@ -554,6 +574,8 @@ export function getDisplayModeInput(props: Object, state: Object) {
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
stageParticipantsVisible: _stageParticipantsVisible,
stageFilmstrip,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@@ -561,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';
}
/**
@@ -579,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);
}
/**
@@ -645,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;
}
@@ -674,7 +697,7 @@ export function getActiveParticipantsIds(state) {
* Gets the ids of the active participants.
*
* @param {Object} state - Redux state.
* @returns {Array<string>}
* @returns {Array<Object>}
*/
export function getPinnedActiveParticipants(state) {
const { activeParticipants } = state['features/filmstrip'];
@@ -683,19 +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) {
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 > 1 && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
&& activeParticipants.length >= minParticipantCount;
}
/**
@@ -707,5 +731,29 @@ export function shouldDisplayStageFilmstrip(state) {
export function isStageFilmstripEnabled(state) {
const { filmstrip } = state['features/base/config'];
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
return !(filmstrip?.disableStageFilmstrip ?? true) && interfaceConfig.VERTICAL_FILMSTRIP;
}
/**
* Gets the thumbnail type by filmstrip type.
*
* @param {string} currentLayout - Current app layout.
* @param {boolean} isStageFilmstrip - Whether the filmstrip is stage filmstrip or not.
* @returns {string}
*/
export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = false) {
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
return THUMBNAIL_TYPE.TILE;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.VERTICAL;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.HORIZONTAL;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
if (isStageFilmstrip) {
return THUMBNAIL_TYPE.TILE;
}
return THUMBNAIL_TYPE.VERTICAL;
}
}

View File

@@ -22,9 +22,11 @@ import {
import {
ADD_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS,
REMOVE_STAGE_PARTICIPANT,
SET_MAX_STAGE_PARTICIPANTS,
SET_USER_FILMSTRIP_WIDTH
SET_USER_FILMSTRIP_WIDTH,
TOGGLE_PIN_STAGE_PARTICIPANT
} from './actionTypes';
import {
addStageParticipant,
@@ -41,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.
@@ -141,6 +145,7 @@ MiddlewareRegistry.register(store => next => action => {
const tid = timers.get(participantId);
clearTimeout(tid);
timers.delete(participantId);
} else if (activeParticipants.length < maxStageParticipants) {
queue = [ ...activeParticipants, {
participantId,
@@ -200,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)));
@@ -216,11 +221,17 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_LEFT: {
const state = store.getState();
const { id } = action.participant;
const activeParticipantsIds = getActiveParticipantsIds(store.getState());
const activeParticipantsIds = getActiveParticipantsIds(state);
if (activeParticipantsIds.find(pId => pId === id)) {
store.dispatch(removeStageParticipant(id));
const tid = timers.get(id);
const { activeParticipants } = state['features/filmstrip'];
clearTimeout(tid);
timers.delete(id);
store.dispatch(setStageParticipants(activeParticipants.filter(p => p.participantId !== id)));
}
break;
}
@@ -240,6 +251,46 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case TOGGLE_PIN_STAGE_PARTICIPANT: {
const { dispatch, getState } = store;
const state = getState();
const { participantId } = action;
const pinnedParticipants = getPinnedActiveParticipants(state);
const dominant = getDominantSpeakerParticipant(state);
if (pinnedParticipants.find(p => p.participantId === participantId)) {
if (dominant?.id === participantId) {
const { activeParticipants } = state['features/filmstrip'];
const queue = activeParticipants.map(p => {
if (p.participantId === participantId) {
return {
participantId,
pinned: false
};
}
return p;
});
dispatch(setStageParticipants(queue));
} else {
dispatch(removeStageParticipant(participantId));
}
} 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);
});
}
}
return result ?? next(action);

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

@@ -12,6 +12,13 @@ StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].remoteScreenShares,
/* listener */ (remoteScreenShares, store) => updateRemoteParticipants(store));
/**
* Listens for changes to the remote screenshare participants to recompute the reordered list of the remote endpoints.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].sortedRemoteFakeScreenShareParticipants,
/* listener */ (sortedRemoteFakeScreenShareParticipants, store) => updateRemoteParticipants(store));
/**
* Listens for changes to the dominant speaker to recompute the reordered list of the remote endpoints.
*/

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

@@ -60,7 +60,7 @@ class Notification extends AbstractNotification<Props> {
description = { this._renderDescription() }
icon = { this._mapAppearanceToIcon() }
id = { uid }
testId = { titleKey }
testId = { titleKey || this._getDescriptionKey() }
title = { title || t(titleKey, titleArguments) } />
);
}

View File

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

View File

@@ -60,6 +60,11 @@ type Props = {
*/
dispatch: Function,
/**
* Is the local participant moderator?
*/
isLocalModerator: boolean,
/**
* List of participants waiting in lobby.
*/
@@ -191,6 +196,7 @@ class MeetingParticipantList extends PureComponent<Props> {
_showInviteButton,
_sortedRemoteParticipants,
breakoutRooms,
isLocalModerator,
lobbyParticipants,
t
} = this.props;
@@ -208,11 +214,14 @@ class MeetingParticipantList extends PureComponent<Props> {
// we take the full container height
const onlyMeetingParticipants
= breakoutRooms?.length === 0 && lobbyParticipants?.length === 0;
const containerStyle
const containerStyleModerator
= onlyMeetingParticipants
? styles.meetingListFullContainer : styles.meetingListContainer;
const containerStyle
= isLocalModerator
? containerStyleModerator : styles.notLocalModeratorContainer;
const finalContainerStyle
= _participantsCount > 3 && containerStyle;
= _participantsCount > 6 && containerStyle;
return (
<CollapsibleList

View File

@@ -71,6 +71,7 @@ const ParticipantsPane = () => {
<LobbyParticipantList />
<MeetingParticipantList
breakoutRooms = { rooms }
isLocalModerator = { isLocalModerator }
lobbyParticipants = { lobbyParticipants }
searchString = { searchString }
setSearchString = { setSearchString } />

View File

@@ -216,8 +216,12 @@ export default {
width: '100%'
},
notLocalModeratorContainer: {
height: '100%'
},
meetingListContainer: {
height: '56%'
height: '58%'
},
meetingListFullContainer: {

View File

@@ -31,7 +31,7 @@ type Props = {
function LobbyParticipantItems({ openDrawerForParticipant, overflowDrawer, participants }: Props) {
return (
<div>
<div id = 'lobby-list'>
{participants.map(p => (
<LobbyParticipantItem
key = { p.id }

View File

@@ -1,12 +1,13 @@
// @flow
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconArrowUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import ToolboxButtonWithIconPopup from '../../../base/toolbox/components/web/ToolboxButtonWithIconPopup';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { type ReactionEmojiProps } from '../../constants';
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
@@ -14,7 +15,7 @@ import { getReactionsMenuVisibility } from '../../functions.web';
import RaiseHandButton from './RaiseHandButton';
import ReactionEmoji from './ReactionEmoji';
import ReactionsMenuPopup from './ReactionsMenuPopup';
import ReactionsMenu from './ReactionsMenu';
type Props = {
@@ -26,7 +27,7 @@ type Props = {
/**
* The button's key.
*/
buttonKey?: string,
buttonKey?: string,
/**
* Redux dispatch function.
@@ -84,38 +85,47 @@ function ReactionsMenuButton({
reactionsQueue,
t
}: Props) {
const visible = useSelector(getReactionsMenuVisibility);
const toggleReactionsMenu = useCallback(() => {
dispatch(toggleReactionsMenuVisibility());
}, [ dispatch ]);
const openReactionsMenu = useCallback(() => {
!visible && toggleReactionsMenu();
}, [ visible, toggleReactionsMenu ]);
const reactionsMenu = (<div className = 'reactions-menu-container'>
<ReactionsMenu />
</div>);
return (
<div className = 'reactions-menu-popup-container'>
<ReactionsMenuPopup>
{!_reactionsEnabled || isMobile ? (
<RaiseHandButton
{!_reactionsEnabled || isMobile ? (
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithIconPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithIcon
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
notifyMode = { notifyMode }
onPopoverClose = { toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
visible = { visible }>
<RaiseHandButton
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) }
notifyMode = { notifyMode }
onIconClick = { toggleReactionsMenu }>
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
)}
</ReactionsMenuPopup>
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithIconPopup>
)}
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }

View File

@@ -1,52 +0,0 @@
// @flow
import InlineDialog from '@atlaskit/inline-dialog';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { getReactionsMenuVisibility } from '../../functions.web';
import ReactionsMenu from './ReactionsMenu';
type Props = {
/**
* Component's children (the reactions menu button).
*/
children: React$Node
}
/**
* Popup with reactions menu.
*
* @returns {ReactElement}
*/
function ReactionsMenuPopup({
children
}: Props) {
/**
* Flag controlling the visibility of the popup.
*/
const isOpen = useSelector(state => getReactionsMenuVisibility(state));
const dispatch = useDispatch();
const onClose = useCallback(() => {
dispatch(toggleReactionsMenuVisibility());
});
return (
<div className = 'reactions-menu-popup'>
<InlineDialog
content = { <ReactionsMenu /> }
isOpen = { isOpen }
onClose = { onClose }
placement = 'top'>
{children}
</InlineDialog>
</div>
);
}
export default ReactionsMenuPopup;

View File

@@ -4,4 +4,3 @@ export { default as ReactionButton } from './ReactionButton';
export { default as ReactionEmoji } from './ReactionEmoji';
export { default as ReactionsMenu } from './ReactionsMenu';
export { default as ReactionsMenuButton } from './ReactionsMenuButton';
export { default as ReactionsMenuPopup } from './ReactionsMenuPopup';

View File

@@ -119,11 +119,12 @@ MiddlewareRegistry.register(store => next => action => {
}
case ADD_FACE_EXPRESSION: {
if (canSendRtcstatsData(state)) {
const { duration, faceExpression } = action;
const { duration, faceExpression, timestamp } = action;
RTCStats.sendFaceExpressionData({
duration,
faceExpression
faceExpression,
timestamp
});
}
break;

View File

@@ -92,6 +92,6 @@ export function _mapStateToProps(state: Object) {
return {
_locked: locked || lobbyEnabled,
visible: enabledFlag || (enabledLobbyModeFlag || enabledMeetingPassFlag)
visible: enabledFlag && (enabledLobbyModeFlag || enabledMeetingPassFlag)
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// @flow
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
type Props = {
@@ -41,8 +41,11 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
portalDiv.style.visibility = 'hidden';
return portalDiv;
});
const timerRef = useRef();
useEffect(() => {
if (style) {
@@ -74,6 +77,10 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
if (contentRect.width !== size.width || contentRect.height !== size.height) {
setSize && setSize(contentRect);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
portalTarget.style.visibility = 'visible';
}, 100);
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

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