Compare commits
30 Commits
jitihouse/
...
bloomberg-
Author | SHA1 | Date |
---|---|---|
Horatiu Muresan | 00cb0a80a1 | |
Saúl Ibarra Corretgé | 8e0ad97860 | |
Saúl Ibarra Corretgé | 09d686fcd3 | |
robertpin | f0a59934a5 | |
Saúl Ibarra Corretgé | 38a1354877 | |
Calin Chitu | aed23a8cd1 | |
Robert Pintilii | 5add987711 | |
Shahab | 636c16f269 | |
Shahab | f7e4d83a05 | |
Shahab | cca984048e | |
Shahab | 5baacaed7e | |
Shahab | 736667d0e9 | |
Robert Pintilii | e3c2823655 | |
Дамян Минков | b65428aa02 | |
Saúl Ibarra Corretgé | ddf90dd6f1 | |
Saúl Ibarra Corretgé | de5c83d53a | |
Avram Tudor | fce2ffc3e3 | |
gpatel-fr | 90d330921e | |
William Liang | c3938e8915 | |
William Liang | 75fcc01576 | |
William Liang | 733e703afd | |
William Liang | 6f56d8f9d8 | |
William Liang | 2e39df49fa | |
William Liang | 34b52cf26e | |
William Liang | 639f794a43 | |
William Liang | d2dab7e6b2 | |
William Liang | 4f21cca7d7 | |
William Liang | f87a1a0e3a | |
pangrr | 56ae2a56e4 | |
William Liang | 4608e60325 |
|
@ -1308,6 +1308,10 @@ var config = {
|
|||
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
|
||||
// // (width, tiles aspect ratios) through the interfaceConfig options.
|
||||
// disableResizable: false,
|
||||
|
||||
// // Disables the stage filmstrip
|
||||
// // (displaying multiple participants on stage besides the vertical filmstrip)
|
||||
// disableStageFilmstrip: false
|
||||
// },
|
||||
|
||||
// Tile view related config options.
|
||||
|
@ -1317,7 +1321,6 @@ var config = {
|
|||
// numberOfVisibleTiles: 25
|
||||
// },
|
||||
|
||||
|
||||
// Specifies whether the chat emoticons are disabled or not
|
||||
// disableChatSmileys: false,
|
||||
|
||||
|
|
|
@ -43,18 +43,6 @@ body {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* AtlasKit sets a default margin on the rendered modals, so
|
||||
* when the shift-right class is set when the chat opens, we
|
||||
* pad the modal container in order for the modals to be centered
|
||||
* while also taking the chat size into consideration.
|
||||
*/
|
||||
@media (min-width: 581px) {
|
||||
.shift-right .atlaskit-portal > div:not(.Tooltip) {
|
||||
padding-left: $sidebarWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
&-default svg {
|
||||
fill: white;
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
#sideToolbarContainer {
|
||||
background-color: $chatBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: width .16s ease-in-out;
|
||||
width: $sidebarWidth;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
@media (max-width: 580px) {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
%connection-info {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
td {
|
||||
padding: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.connection-info
|
||||
{
|
||||
@extend %connection-info;
|
||||
|
||||
> table {
|
||||
white-space: nowrap;
|
||||
@extend %connection-info;
|
||||
}
|
||||
|
||||
td:nth-child(n-1) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&__download
|
||||
{
|
||||
@extend .connection-info__icon;
|
||||
}
|
||||
|
||||
&__status
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__upload
|
||||
{
|
||||
@extend .connection-info__icon;
|
||||
}
|
||||
|
||||
&__mobile {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
margin: 10px auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -193,3 +193,17 @@
|
|||
@mixin transparentBg($color, $alpha) {
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the direction of the current element to LTR, but do not change the direction
|
||||
* of its children; Keep them RTL.
|
||||
*/
|
||||
@mixin ltr {
|
||||
body[dir=rtl] & {
|
||||
direction: ltr;
|
||||
|
||||
& > * {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
transition: width .16s ease-in-out;
|
||||
width: 315px;
|
||||
z-index: $zindex0;
|
||||
|
||||
&--closed {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.participants_pane-content {
|
||||
|
@ -34,11 +30,6 @@
|
|||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
|
||||
&--closed {
|
||||
display: none;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.participants_pane-content {
|
||||
|
|
|
@ -88,10 +88,6 @@
|
|||
max-width: calc(100% - 24px);
|
||||
}
|
||||
|
||||
.shift-right .details-container {
|
||||
margin-left: calc(#{$sidebarWidth} / 2);
|
||||
}
|
||||
|
||||
@keyframes hideSubject {
|
||||
0% {
|
||||
max-width: 100%;
|
||||
|
|
|
@ -39,13 +39,6 @@
|
|||
&.no-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-content {
|
||||
|
@ -99,6 +92,10 @@
|
|||
max-width: 100%;
|
||||
pointer-events: all;
|
||||
border-radius: 6px;
|
||||
|
||||
.toolbox-content-items {
|
||||
@include ltr;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-content-wrapper::after {
|
||||
|
@ -183,6 +180,7 @@
|
|||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
@include ltr;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
}
|
||||
|
||||
#layout_wrapper {
|
||||
@include ltr;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -44,15 +45,6 @@
|
|||
position: relative;
|
||||
text-align: center;
|
||||
overflow: 'hidden';
|
||||
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
&#largeVideoContainer {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
/**
|
||||
* The local video identifier.
|
||||
*/
|
||||
&#filmstripLocalVideo {
|
||||
&#filmstripLocalVideo,
|
||||
&#filmstripLocalScreenShare {
|
||||
align-self: flex-end;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
|
|
|
@ -41,17 +41,6 @@
|
|||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 581px) {
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
|
||||
.remote-videos {
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
#remoteVideos {
|
||||
height: calc(100% - #{$newToolbarSizeMobile}) !important;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Various overrides outside of the filmstrip to style the app to support a
|
||||
* tiled thumbnail experience.
|
||||
*/
|
||||
.tile-view {
|
||||
.tile-view, .stage-filmstrip {
|
||||
/**
|
||||
* Let the avatar grow with the tile.
|
||||
*/
|
||||
|
@ -15,9 +15,10 @@
|
|||
* Hide various features that should not be displayed while in tile view.
|
||||
*/
|
||||
#dominantSpeaker,
|
||||
#filmstripLocalVideoThumbnail,
|
||||
#filmstripLocalScreenShareThumbnail,
|
||||
#largeVideoElementsContainer,
|
||||
#sharedVideo {
|
||||
#sharedVideo,
|
||||
.stage-participant-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.vertical-filmstrip .filmstrip {
|
||||
.vertical-filmstrip span:not(.tile-view) .filmstrip {
|
||||
&.hide-videos {
|
||||
.remote-videos {
|
||||
& > div {
|
||||
|
@ -87,9 +87,27 @@
|
|||
.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%;
|
||||
|
||||
#filmstripLocalScreenShareThumbnail {
|
||||
width: calc(100% - 15px);
|
||||
|
||||
.videocontainer {
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,6 +115,7 @@
|
|||
* filmstrip from overlapping the left edge of the screen.
|
||||
*/
|
||||
#filmstripLocalVideo,
|
||||
#filmstripLocalScreenShare,
|
||||
.remote-videos {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -38,10 +38,8 @@ $flagsImagePath: "../images/";
|
|||
@import 'modals/embed-meeting/embed-meeting';
|
||||
@import 'modals/feedback/feedback';
|
||||
@import 'modals/invite/info';
|
||||
@import 'modals/settings/settings';
|
||||
@import 'modals/screen-share/share-audio';
|
||||
@import 'modals/screen-share/share-screen-warning';
|
||||
@import 'modals/virtual-background/virtual-background';
|
||||
@import 'modals/local-recording/local-recording';
|
||||
@import 'videolayout_default';
|
||||
@import 'notice';
|
||||
|
@ -61,7 +59,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'components/button-control';
|
||||
@import 'components/input-control';
|
||||
@import 'components/input-slider';
|
||||
@import "connection-info";
|
||||
@import '404';
|
||||
@import 'policy';
|
||||
@import 'popover';
|
||||
|
@ -89,7 +86,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'country-picker';
|
||||
@import 'modals/invite/invite_more';
|
||||
@import 'modals/security/security';
|
||||
@import 'modals/mute/mute-dialog';
|
||||
@import 'e2ee';
|
||||
@import 'responsive';
|
||||
@import 'drawer';
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
.mute-dialog {
|
||||
.separator-line {
|
||||
margin: 24px 0 24px -20px;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #5E6D7A;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
.settings-pane {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
&.profile-pane {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auth-name {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.calendar-tab,
|
||||
.device-selection {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mock-atlaskit-label {
|
||||
color: #b8c7e0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.33;
|
||||
padding: 20px 0px 4px 0px;
|
||||
}
|
||||
input[type="checkbox"]:checked + svg {
|
||||
--checkbox-background-color: #6492e7;
|
||||
--checkbox-border-color: #6492e7;
|
||||
}
|
||||
input[type="checkbox"] + svg + span {
|
||||
color: #b8c7e0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + svg + span {
|
||||
color: #9FB0CC;
|
||||
}
|
||||
|
||||
.calendar-tab,
|
||||
.more-tab,
|
||||
.box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-edit {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-edit-field {
|
||||
flex: .5;
|
||||
}
|
||||
.settings-sub-pane {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-sub-pane .right {
|
||||
flex: 1;
|
||||
}
|
||||
.settings-sub-pane .left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-sub-pane-element {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.moderator-settings-wrapper {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.profile-edit-field {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.calendar-tab {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
min-height: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-tab-sign-in {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sign-out-cta {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $smallScreen) {
|
||||
.device-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.more-tab {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
.virtual-background-dialog {
|
||||
margin-left: -10px;
|
||||
position: relative;
|
||||
max-height: 300px;
|
||||
color: white;
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto auto auto auto;
|
||||
column-gap: 9px;
|
||||
cursor: pointer;
|
||||
.desktop-share:hover,
|
||||
.thumbnail:hover,
|
||||
.blur:hover,
|
||||
.slight-blur:hover,
|
||||
.virtual-background-none:hover {
|
||||
opacity: 0.5;
|
||||
border: 2px solid #99bbf3;
|
||||
@media (max-width: 632px) {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
.background-option {
|
||||
margin-top: 8px;
|
||||
border-radius: 6px;
|
||||
height: 60px;
|
||||
width: 107px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.thumbnail {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbnail:hover ~ .delete-image-icon {
|
||||
display: block;
|
||||
}
|
||||
.thumbnail-selected {
|
||||
object-fit: cover;
|
||||
border: 2px solid #246fe5;
|
||||
}
|
||||
.blur {
|
||||
box-shadow: inset 0 0 12px #000000;
|
||||
background: #7e8287;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.blur-selected {
|
||||
box-shadow: inset 0 0 12px #000000;
|
||||
background: #7e8287;
|
||||
border: 2px solid #246fe5;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.slight-blur {
|
||||
box-shadow: inset 0 0 12px #000000;
|
||||
background: #a4a4a4;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.slight-blur-selected {
|
||||
box-shadow: inset 0 0 12px #000000;
|
||||
background: #a4a4a4;
|
||||
border: 2px solid #246fe5;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.virtual-background-none {
|
||||
background: #525252;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.none-selected {
|
||||
background: #525252;
|
||||
border: 2px solid #246fe5;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.desktop-share {
|
||||
background: #525252;
|
||||
}
|
||||
.desktop-share-selected {
|
||||
background: #525252;
|
||||
border: 2px solid #246fe5;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 632px) {
|
||||
font-size: 1.5vw;
|
||||
.desktop-share,
|
||||
.virtual-background-none,
|
||||
.thumbnail,
|
||||
.blur,
|
||||
.slight-blur {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
.desktop-share-selected,
|
||||
.thumbnail-selected,
|
||||
.none-selected,
|
||||
.blur-selected,
|
||||
.slight-blur-selected {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form .virtual-background-loading {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
margin-top: 10px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.modal-dialog-form .video-preview {
|
||||
height: 250px;
|
||||
}
|
||||
.file-upload-btn {
|
||||
display: none;
|
||||
}
|
||||
.file-upload-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
margin-left: -10px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: #669aec;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-image-icon {
|
||||
background: #3d3d3d;
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 96;
|
||||
bottom: 51;
|
||||
@media (max-width: 632px) {
|
||||
left: 51px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-image-icon:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail-container {
|
||||
position: relative;
|
||||
&:focus-within {
|
||||
.thumbnail ~ .delete-image-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-background {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.apply-background-btn {
|
||||
margin-top: 16px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.video-background-preview-entry {
|
||||
margin-left: -10px;
|
||||
height: 250px;
|
||||
width: 570px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 2;
|
||||
@media (max-width: 632px) {
|
||||
max-width: 336;
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-background-preview-video {
|
||||
margin-left: -10;
|
||||
border-radius: 6px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
.video-preview-loader {
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
height: 250px;
|
||||
margin-bottom: 8px;
|
||||
width: 572px;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
@media (min-width: 432px) and (max-width: 632px) {
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-preview-loader svg {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 45%;
|
||||
}
|
||||
|
||||
.dialog-margin-top{
|
||||
margin-top: 44px;
|
||||
}
|
|
@ -137,6 +137,7 @@
|
|||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
@include ltr;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
|
|
|
@ -33,7 +33,7 @@ Description: Configuration for web serving of Jitsi Meet
|
|||
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Depends: openssl, prosody (>= 0.11.0) | prosody-trunk | prosody-0.11, lua-sec
|
||||
Depends: openssl, prosody (>= 0.11.0) | prosody-trunk | prosody-0.12 | prosody-0.11, lua-sec
|
||||
Replaces: jitsi-meet-tokens
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
|
|
|
@ -14,6 +14,16 @@ cross_domain_bosh = false;
|
|||
consider_bosh_secure = true;
|
||||
-- https_ports = { }; -- Remove this line to prevent listening on port 5284
|
||||
|
||||
-- by default prosody 0.12 sends cors headers, if you want to disable it uncomment the following (the config is available on 0.12.1)
|
||||
--http_cors_override = {
|
||||
-- bosh = {
|
||||
-- enabled = false;
|
||||
-- };
|
||||
-- websocket = {
|
||||
-- enabled = false;
|
||||
-- };
|
||||
--}
|
||||
|
||||
-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
|
||||
ssl = {
|
||||
protocol = "tlsv1_2+";
|
||||
|
|
|
@ -39,9 +39,6 @@
|
|||
"audioOnly": {
|
||||
"audioOnly": "Bande passante faible"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "Réunion terminée."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"actions": {
|
||||
"add": "Ajouter salle annexe",
|
||||
|
@ -559,6 +556,7 @@
|
|||
"errorMissingPassword": "Veuillez saisir le mot de passe de la réunion",
|
||||
"invalidPassword": "Mot de passe invalide",
|
||||
"joinRejectedMessage": "Votre requête pour rejoindre une réunion a été refusée par un modérateur.",
|
||||
"joinRejectedTitle": "Demande d'accès rejetée.",
|
||||
"joinTitle": "Rejoindre une réunion",
|
||||
"joinWithPasswordMessage": "Tentative de rejoindre avec mot de passe, patientez s'il vous plait ...",
|
||||
"joiningMessage": "Vous allez rejoindre une réunion dès que quelqu'un aura accepté votre demande",
|
||||
|
@ -865,6 +863,11 @@
|
|||
"expandedPending": "Démarrage de l'enregistrement ...",
|
||||
"failedToStart": "L'enregistrement n'a pas réussi à démarrer",
|
||||
"fileSharingdescription": "Partager l'enregistrement avec les participants de la réunion",
|
||||
"highlight": "Souligner",
|
||||
"highlightMoment": "Souligner un moment",
|
||||
"highlightMomentDisabled": "Vous ne pouvez souligner des moments que pendant une réunion",
|
||||
"highlightMomentSuccess": "Moment souligné",
|
||||
"highlightMomentSucessDescription": "Votre moment souligné sera ajouté au résumé de la réunion.",
|
||||
"inProgress": "Enregistrement ou diffusion en cours",
|
||||
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
|
||||
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
|
||||
|
|
|
@ -722,6 +722,7 @@
|
|||
},
|
||||
"passwordDigitsOnly": "Up to {{number}} digits",
|
||||
"passwordSetRemotely": "Set by another participant",
|
||||
"pinnedParticipant": "The participant is pinned",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"skip": "Skip",
|
||||
|
@ -1213,10 +1214,12 @@
|
|||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
"pinToStage": "Pin to stage",
|
||||
"remoteControl": "Start / Stop remote control",
|
||||
"screenSharing": "Participant is sharing their screen",
|
||||
"show": "Show on stage",
|
||||
"showSelfView": "Show self view",
|
||||
"unpinFromStage": "Unpin",
|
||||
"videoMuted": "Camera disabled",
|
||||
"videomute": "Participant has stopped the camera"
|
||||
},
|
||||
|
|
|
@ -108,13 +108,13 @@ UI.start = function() {
|
|||
$('body').addClass('mobile-browser');
|
||||
} else {
|
||||
$('body').addClass('desktop-browser');
|
||||
}
|
||||
|
||||
if (config.backgroundAlpha !== undefined) {
|
||||
const backgroundColor = $('body').css('background-color');
|
||||
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
|
||||
if (config.backgroundAlpha !== undefined) {
|
||||
const backgroundColor = $('body').css('background-color');
|
||||
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
|
||||
|
||||
$('body').css('background-color', alphaColor);
|
||||
}
|
||||
$('body').css('background-color', alphaColor);
|
||||
}
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
|
|
|
@ -11,12 +11,14 @@ 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 { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import { VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import {
|
||||
getVideoTrackByParticipant
|
||||
} from '../../../react/features/base/tracks';
|
||||
import { CHAT_SIZE } from '../../../react/features/chat';
|
||||
import {
|
||||
isParticipantConnectionStatusActive,
|
||||
|
@ -237,11 +239,13 @@ export default class LargeVideoManager {
|
|||
let isVideoRenderable;
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
|
||||
|
||||
isVideoRenderable = !isVideoMuted
|
||||
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack));
|
||||
isVideoRenderable = !isVideoMuted && (
|
||||
APP.conference.isLocalId(id)
|
||||
|| participant?.isLocalScreenShare
|
||||
|| isTrackStreamingStatusActive(videoTrack)
|
||||
);
|
||||
} else {
|
||||
isVideoRenderable = !isVideoMuted
|
||||
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
|
||||
|
@ -268,8 +272,8 @@ export default class LargeVideoManager {
|
|||
|
||||
&& participant && !participant.local && !participant.isFakeParticipant) {
|
||||
// remote participant only
|
||||
const track = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
const track = getVideoTrackByParticipant(state['features/base/tracks'], participant);
|
||||
|
||||
const isScreenSharing = track?.videoType === 'desktop';
|
||||
|
||||
if (isScreenSharing) {
|
||||
|
@ -300,8 +304,7 @@ export default class LargeVideoManager {
|
|||
let messageKey;
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
|
||||
|
||||
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
|
||||
} else {
|
||||
|
@ -541,8 +544,7 @@ export default class LargeVideoManager {
|
|||
const state = APP.store.getState();
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
|
||||
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
show = !APP.conference.isLocalId(this.id)
|
||||
|
|
|
@ -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 } from '../../../react/features/filmstrip';
|
||||
import { FILMSTRIP_BREAKPOINT, shouldDisplayStageFilmstrip } 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) {
|
||||
if (currentLayout === LAYOUTS.TILE_VIEW || shouldDisplayStageFilmstrip(state)) {
|
||||
// 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;
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
import Logger from '@jitsi/logger';
|
||||
|
||||
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getPinnedParticipant,
|
||||
getParticipantById
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import {
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
getVideoTrackByParticipant
|
||||
} from '../../../react/features/base/tracks';
|
||||
|
||||
import LargeVideoManager from './LargeVideoManager';
|
||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||
|
@ -91,6 +95,10 @@ const VideoLayout = {
|
|||
return VIDEO_TYPE.CAMERA;
|
||||
}
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state) && participant?.isFakeScreenShareParticipant) {
|
||||
return VIDEO_TYPE.DESKTOP;
|
||||
}
|
||||
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
|
||||
return videoTrack?.videoType;
|
||||
|
@ -177,7 +185,8 @@ const VideoLayout = {
|
|||
const currentContainerType = largeVideo.getCurrentContainerType();
|
||||
const isOnLarge = this.isCurrentlyOnLarge(id);
|
||||
const state = APP.store.getState();
|
||||
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
||||
const participant = getParticipantById(state, id);
|
||||
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
|
||||
const videoStream = videoTrack?.jitsiTrack;
|
||||
|
||||
if (isOnLarge && !forceUpdate
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import '../authentication/middleware';
|
||||
import '../mobile/audio-mode/middleware';
|
||||
import '../mobile/back-button/middleware';
|
||||
import '../mobile/background/middleware';
|
||||
import '../mobile/call-integration/middleware';
|
||||
import '../mobile/external-api/middleware';
|
||||
|
|
|
@ -218,19 +218,9 @@ export default class BaseApp extends Component<*, State> {
|
|||
// additional 3rd party middleware:
|
||||
// - Thunk - allows us to dispatch async actions easily. For more info
|
||||
// @see https://github.com/gaearon/redux-thunk.
|
||||
let middleware = MiddlewareRegistry.applyMiddleware(Thunk);
|
||||
|
||||
// Try to enable Redux DevTools Chrome extension in order to make it
|
||||
// available for the purposes of facilitating development.
|
||||
let devToolsExtension;
|
||||
|
||||
if (typeof window === 'object'
|
||||
&& (devToolsExtension = window.devToolsExtension)) {
|
||||
middleware = compose(middleware, devToolsExtension());
|
||||
}
|
||||
|
||||
const store = createStore(
|
||||
reducer, PersistenceRegistry.getPersistedState(), middleware);
|
||||
const middleware = MiddlewareRegistry.applyMiddleware(Thunk);
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
const store = createStore(reducer, PersistenceRegistry.getPersistedState(), composeEnhancers(middleware));
|
||||
|
||||
// StateListenerRegistry
|
||||
StateListenerRegistry.subscribe(store);
|
||||
|
|
|
@ -93,6 +93,7 @@ export { default as IconOutlook } from './office365.svg';
|
|||
export { default as IconParticipants } from './participants.svg';
|
||||
export { default as IconPhone } from './phone.svg';
|
||||
export { default as IconPin } from './enlarge.svg';
|
||||
export { default as IconPinParticipant } from './pin.svg';
|
||||
export { default as IconPlane } from './paper-plane.svg';
|
||||
export { default as IconPresentation } from './presentation.svg';
|
||||
export { default as IconRaisedHand } from './raised-hand.svg';
|
||||
|
@ -128,6 +129,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
|
|||
export { default as IconTileView } from './tiles-many.svg';
|
||||
export { default as IconToggleRecording } from './camera-take-picture.svg';
|
||||
export { default as IconTrash } from './trash.svg';
|
||||
export { default as IconUnpin } from './unpin.svg';
|
||||
export { default as IconVideoOff } from './video-off.svg';
|
||||
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
|
||||
export { default as IconVideoQualityHD } from './HD.svg';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.69428 17.1265L5.98398 13.9439L7.92634 15.8862C8.57722 16.5371 9.63249 16.5371 10.2834 15.8862L10.8726 15.297C11.5235 14.6461 11.5235 13.5908 10.8726 12.9399L10.578 12.6453L11.2691 11.4935C11.396 11.2819 11.5684 11.1012 11.7737 10.9643L13.5832 9.75796L13.8189 9.99366C14.4698 10.6445 15.525 10.6445 16.1759 9.99366L16.7652 9.4044C17.416 8.75353 17.416 7.69825 16.7652 7.04738L10.8726 1.15482C10.2217 0.503949 9.16647 0.503949 8.5156 1.15482L7.92634 1.74408C7.27547 2.39495 7.27547 3.45023 7.92634 4.1011L8.27989 4.45465L7.3088 6.25811C7.13602 6.579 6.86281 6.83441 6.53102 6.98522L5.42201 7.48932L4.98006 7.04738C4.32919 6.39651 3.27391 6.39651 2.62304 7.04738L2.03379 7.63664C1.38291 8.28751 1.38291 9.34278 2.03379 9.99366L3.97615 11.936L0.793463 16.2257C0.603279 16.4821 0.629578 16.839 0.855274 17.0647C1.08097 17.2904 1.43794 17.3167 1.69428 17.1265ZM13.7956 7.6133L10.8492 9.57753C10.4386 9.8513 10.0938 10.2128 9.8399 10.636L8.47933 12.9037L9.69411 14.1184L9.10485 14.7077L3.2123 8.81515L3.80155 8.22589L5.0602 9.48454L7.2207 8.5025C7.88427 8.20088 8.43068 7.69006 8.77626 7.04828L10.3353 4.15299L9.10485 2.92259L9.69411 2.33333L15.5867 8.22589L14.9974 8.81515L13.7956 7.6133Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.36765 7.1488L8.36029 7.16248L3.11269 1.91488C2.78182 1.58401 2.24545 1.58392 1.91468 1.91469C1.58391 2.24546 1.58399 2.78183 1.91487 3.1127L7.01977 8.21761L6.422 8.48932L5.98005 8.04738C5.32918 7.39651 4.27391 7.39651 3.62303 8.04738L3.03378 8.63664C2.3829 9.28751 2.3829 10.3428 3.03378 10.9937L4.97614 12.936L1.79345 17.2257C1.60327 17.4821 1.62957 17.839 1.85526 18.0647C2.08096 18.2904 2.43793 18.3167 2.69427 18.1265L6.98397 14.9439L8.92633 16.8862C9.57721 17.5371 10.6325 17.5371 11.2834 16.8862L11.8726 16.297C12.5235 15.6461 12.5235 14.5908 11.8726 13.9399L11.578 13.6453L11.904 13.1019L16.8873 18.0851C17.2182 18.416 17.7545 18.4161 18.0853 18.0853C18.4161 17.7545 18.416 17.2182 18.0851 16.8873L13.0067 11.8089L13.0194 11.8005L11.8177 10.5988C11.8135 10.6017 11.8093 10.6045 11.8052 10.6074L9.57425 8.37644C9.57714 8.37231 9.58001 8.36817 9.58288 8.36403L8.36765 7.1488ZM13.2549 9.6404L14.7956 8.6133L15.9974 9.81515L16.5867 9.22589L10.6941 3.33333L10.1048 3.92259L11.3352 5.15299L10.4365 6.82203L9.20613 5.59163L9.27989 5.45465L8.92633 5.1011C8.27546 4.45023 8.27546 3.39495 8.92633 2.74408L9.51559 2.15482C10.1665 1.50395 11.2217 1.50395 11.8726 2.15482L17.7652 8.04738C18.416 8.69825 18.416 9.75353 17.7652 10.4044L17.1759 10.9937C16.525 11.6445 15.4698 11.6445 14.8189 10.9937L14.5832 10.758L14.4567 10.8422L13.2549 9.6404ZM9.47932 13.9037L10.6893 11.8871L8.27797 9.4758C8.25897 9.48488 8.23988 9.49378 8.22069 9.5025L6.06019 10.4845L4.80154 9.22589L4.21229 9.81515L10.1048 15.7077L10.6941 15.1184L9.47932 13.9037Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -5,7 +5,11 @@ import debounce from 'lodash/debounce';
|
|||
import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
|
||||
import { SELECT_LARGE_VIDEO_PARTICIPANT } from '../../large-video/actionTypes';
|
||||
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
|
||||
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-layout/actionTypes';
|
||||
import {
|
||||
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SET_TILE_VIEW
|
||||
} from '../../video-layout/actionTypes';
|
||||
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
|
||||
import { CONFERENCE_JOINED } from '../conference/actionTypes';
|
||||
import {
|
||||
|
@ -92,6 +96,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
switch (action.type) {
|
||||
case APP_STATE_CHANGED:
|
||||
case CONFERENCE_JOINED:
|
||||
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_KICKED:
|
||||
case PARTICIPANT_LEFT:
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
|
||||
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
|
||||
import { getSourceNameSignalingFeatureFlag } from '../config';
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
import { toState } from '../redux';
|
||||
|
@ -118,9 +120,11 @@ export function getNormalizedDisplayName(name: string) {
|
|||
export function getParticipantById(
|
||||
stateful: Object | Function, id: string): ?Object {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { local, remote } = state;
|
||||
const { local, localScreenShare, remote } = state;
|
||||
|
||||
return remote.get(id) || (local?.id === id ? local : undefined);
|
||||
return remote.get(id)
|
||||
|| (local?.id === id ? local : undefined)
|
||||
|| (localScreenShare?.id === id ? localScreenShare : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,10 +151,31 @@ export function getParticipantByIdOrUndefined(stateful: Object | Function, parti
|
|||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCount(stateful: Object | Function) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { local, remote, fakeParticipants } = state;
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
local,
|
||||
remote,
|
||||
fakeParticipants,
|
||||
sortedRemoteFakeScreenShareParticipants
|
||||
} = state['features/base/participants'];
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
return remote.size - fakeParticipants.size - sortedRemoteFakeScreenShareParticipants.size + (local ? 1 : 0);
|
||||
}
|
||||
|
||||
return remote.size - fakeParticipants.size + (local ? 1 : 0);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant ID of the owner of a fake screenshare participant.
|
||||
*
|
||||
* @param {string} id - The ID of the fake screenshare participant.
|
||||
* @private
|
||||
* @returns {(string|undefined)}
|
||||
*/
|
||||
export function getFakeScreenShareParticipantOwnerId(id: string) {
|
||||
return id.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,6 +201,10 @@ export function getFakeParticipants(stateful: Object | Function) {
|
|||
export function getRemoteParticipantCount(stateful: Object | Function) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
return state.remote.size - state.sortedRemoteFakeScreenShareParticipants.size;
|
||||
}
|
||||
|
||||
return state.remote.size;
|
||||
}
|
||||
|
||||
|
@ -189,8 +218,12 @@ export function getRemoteParticipantCount(stateful: Object | Function) {
|
|||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCountWithFake(stateful: Object | Function) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { local, remote } = state;
|
||||
const state = toState(stateful);
|
||||
const { local, localScreenShare, remote } = state['features/base/participants'];
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
|
||||
}
|
||||
|
||||
return remote.size + (local ? 1 : 0);
|
||||
}
|
||||
|
@ -290,8 +323,16 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
|
|||
* @returns {(Participant|undefined)}
|
||||
*/
|
||||
export function getPinnedParticipant(stateful: Object | Function) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { pinnedParticipant } = state;
|
||||
const state = toState(stateful);
|
||||
const { pinnedParticipant } = state['features/base/participants'];
|
||||
const stageFilmstrip = isStageFilmstripEnabled(state);
|
||||
|
||||
if (stageFilmstrip) {
|
||||
const { activeParticipants } = state['features/filmstrip'];
|
||||
const id = activeParticipants.find(p => p.pinned)?.participantId;
|
||||
|
||||
return id ? getParticipantById(stateful, id) : undefined;
|
||||
}
|
||||
|
||||
if (!pinnedParticipant) {
|
||||
return undefined;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED } from '../../video-layout/actionTypes';
|
||||
import {
|
||||
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
|
||||
} from '../../video-layout/actionTypes';
|
||||
import { ReducerRegistry, set } from '../redux';
|
||||
|
||||
import {
|
||||
|
@ -59,9 +61,11 @@ const DEFAULT_STATE = {
|
|||
fakeParticipants: new Map(),
|
||||
haveParticipantWithScreenSharingFeature: false,
|
||||
local: undefined,
|
||||
localScreenShare: undefined,
|
||||
pinnedParticipant: undefined,
|
||||
raisedHandsQueue: [],
|
||||
remote: new Map(),
|
||||
sortedRemoteFakeScreenShareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
sortedRemoteScreenshares: new Map(),
|
||||
speakersList: new Map()
|
||||
|
@ -207,7 +211,7 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
const participant = _participantJoined(action);
|
||||
const { id, isFakeParticipant, name, pinned } = participant;
|
||||
const { id, isFakeParticipant, isFakeScreenShareParticipant, isLocalScreenShare, name, pinned } = participant;
|
||||
const { pinnedParticipant, dominantSpeaker } = state;
|
||||
|
||||
if (pinned) {
|
||||
|
@ -241,6 +245,13 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
};
|
||||
}
|
||||
|
||||
if (isLocalScreenShare) {
|
||||
return {
|
||||
...state,
|
||||
localScreenShare: participant
|
||||
};
|
||||
}
|
||||
|
||||
state.remote.set(id, participant);
|
||||
|
||||
// Insert the new participant.
|
||||
|
@ -253,6 +264,14 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
|
||||
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
|
||||
|
||||
if (isFakeScreenShareParticipant) {
|
||||
const sortedRemoteFakeScreenShareParticipants = [ ...state.sortedRemoteFakeScreenShareParticipants ];
|
||||
|
||||
sortedRemoteFakeScreenShareParticipants.push([ id, name ]);
|
||||
sortedRemoteFakeScreenShareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
|
||||
}
|
||||
if (isFakeParticipant) {
|
||||
state.fakeParticipants.set(id, participant);
|
||||
}
|
||||
|
@ -267,7 +286,15 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
// (and the fact that the local participant "joins" at the beginning of
|
||||
// the app and "leaves" at the end of the app).
|
||||
const { conference, id } = action.participant;
|
||||
const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
|
||||
const {
|
||||
fakeParticipants,
|
||||
sortedRemoteFakeScreenShareParticipants,
|
||||
remote,
|
||||
local,
|
||||
localScreenShare,
|
||||
dominantSpeaker,
|
||||
pinnedParticipant
|
||||
} = state;
|
||||
let oldParticipant = remote.get(id);
|
||||
|
||||
if (oldParticipant && oldParticipant.conference === conference) {
|
||||
|
@ -275,6 +302,9 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
} else if (local?.id === id) {
|
||||
oldParticipant = state.local;
|
||||
delete state.local;
|
||||
} else if (localScreenShare?.id === id) {
|
||||
oldParticipant = state.local;
|
||||
delete state.localScreenShare;
|
||||
} else {
|
||||
// no participant found
|
||||
return state;
|
||||
|
@ -324,6 +354,11 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
|||
fakeParticipants.delete(id);
|
||||
}
|
||||
|
||||
if (sortedRemoteFakeScreenShareParticipants.has(id)) {
|
||||
sortedRemoteFakeScreenShareParticipants.delete(id);
|
||||
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
case RAISE_HAND_UPDATED: {
|
||||
|
@ -447,6 +482,8 @@ function _participantJoined({ participant }) {
|
|||
dominantSpeaker,
|
||||
email,
|
||||
isFakeParticipant,
|
||||
isFakeScreenShareParticipant,
|
||||
isLocalScreenShare,
|
||||
isReplacing,
|
||||
isJigasi,
|
||||
loadableAvatarUrl,
|
||||
|
@ -479,6 +516,8 @@ function _participantJoined({ participant }) {
|
|||
email,
|
||||
id,
|
||||
isFakeParticipant,
|
||||
isFakeScreenShareParticipant,
|
||||
isLocalScreenShare,
|
||||
isReplacing,
|
||||
isJigasi,
|
||||
loadableAvatarUrl,
|
||||
|
@ -500,7 +539,7 @@ function _participantJoined({ participant }) {
|
|||
* @returns {boolean} - True if a participant was updated and false otherwise.
|
||||
*/
|
||||
function _updateParticipantProperty(state, id, property, value) {
|
||||
const { remote, local } = state;
|
||||
const { remote, local, localScreenShare } = state;
|
||||
|
||||
if (remote.has(id)) {
|
||||
remote.set(id, set(remote.get(id), property, value));
|
||||
|
@ -511,6 +550,11 @@ function _updateParticipantProperty(state, id, property, value) {
|
|||
// not in a conference.
|
||||
state.local = set(local, property, value);
|
||||
|
||||
return true;
|
||||
|
||||
} else if (localScreenShare?.id === id) {
|
||||
state.localScreenShare = set(localScreenShare, property, value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
import React, { PureComponent, type Node } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { BackButtonRegistry } from '../../../../mobile/back-button';
|
||||
import { type StyleType } from '../../../styles';
|
||||
|
||||
import styles from './slidingviewstyles';
|
||||
|
@ -121,7 +121,7 @@ export default class SlidingView extends PureComponent<Props, State> {
|
|||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
BackButtonRegistry.addListener(this._onHardwareBackPress, true);
|
||||
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
|
||||
this._mounted = true;
|
||||
this._setShow(this.props.show);
|
||||
|
@ -146,7 +146,7 @@ export default class SlidingView extends PureComponent<Props, State> {
|
|||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
BackButtonRegistry.removeListener(this._onHardwareBackPress);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
|
||||
this._mounted = false;
|
||||
}
|
||||
|
@ -229,13 +229,9 @@ export default class SlidingView extends PureComponent<Props, State> {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
_onHardwareBackPress() {
|
||||
const { onHide } = this.props;
|
||||
this._onHide();
|
||||
|
||||
if (typeof onHide === 'function') {
|
||||
return onHide();
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
_onHide: () => void;
|
||||
|
|
|
@ -107,6 +107,18 @@ export const TRACK_STOPPED = 'TRACK_STOPPED';
|
|||
*/
|
||||
export const TRACK_UPDATED = 'TRACK_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when a screenshare track's muted property were updated.
|
||||
*
|
||||
* {
|
||||
* type: SCREENSHARE_TRACK_MUTED_UPDATED,
|
||||
* track: Track,
|
||||
* muted: Boolean
|
||||
*
|
||||
* }
|
||||
*/
|
||||
export const SCREENSHARE_TRACK_MUTED_UPDATED = 'SCREENSHARE_TRACK_MUTED_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when a local track starts being created
|
||||
* via a WebRTC {@code getUserMedia} call. The action's payload includes an
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from '../../analytics';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications';
|
||||
import { getCurrentConference } from '../conference';
|
||||
import { getMultipleVideoSupportFeatureFlag } from '../config';
|
||||
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
|
||||
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
|
@ -21,6 +21,7 @@ import { getLocalParticipant } from '../participants';
|
|||
import { updateSettings } from '../settings';
|
||||
|
||||
import {
|
||||
SCREENSHARE_TRACK_MUTED_UPDATED,
|
||||
SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
|
@ -395,7 +396,12 @@ export function trackAdded(track) {
|
|||
return async (dispatch, getState) => {
|
||||
track.on(
|
||||
JitsiTrackEvents.TRACK_MUTE_CHANGED,
|
||||
() => dispatch(trackMutedChanged(track)));
|
||||
() => {
|
||||
if (getSourceNameSignalingFeatureFlag(getState()) && track.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
dispatch(screenshareTrackMutedChanged(track));
|
||||
}
|
||||
dispatch(trackMutedChanged(track));
|
||||
});
|
||||
track.on(
|
||||
JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
|
||||
type => dispatch(trackVideoTypeChanged(track, type)));
|
||||
|
@ -491,6 +497,24 @@ export function trackMutedChanged(track) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for when a screenshare track's muted state has been signaled to be changed.
|
||||
*
|
||||
* @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
|
||||
* @returns {{
|
||||
* type: TRACK_UPDATED,
|
||||
* track: Track,
|
||||
* muted: boolean
|
||||
* }}
|
||||
*/
|
||||
export function screenshareTrackMutedChanged(track) {
|
||||
return {
|
||||
type: SCREENSHARE_TRACK_MUTED_UPDATED,
|
||||
track: { jitsiTrack: track },
|
||||
muted: track.isMuted()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for when a track's muted state change action has failed. This could happen because of
|
||||
* {@code getUserMedia} errors during unmute or replace track errors at the peerconnection level.
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any';
|
|||
import { isMobileBrowser } from '../environment/utils';
|
||||
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
|
||||
import { getFakeScreenShareParticipantOwnerId } from '../participants';
|
||||
import { toState } from '../redux';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
|
@ -410,6 +411,32 @@ export function getLocalJitsiAudioTrack(state) {
|
|||
return track?.jitsiTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track of specified media type for specified participant.
|
||||
*
|
||||
* @param {Track[]} tracks - List of all tracks.
|
||||
* @param {Object} participant - Participant Object.
|
||||
* @returns {(Track|undefined)}
|
||||
*/
|
||||
export function getVideoTrackByParticipant(tracks, participant) {
|
||||
if (!participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
let participantId;
|
||||
let mediaType;
|
||||
|
||||
if (participant?.isFakeScreenShareParticipant) {
|
||||
participantId = getFakeScreenShareParticipantOwnerId(participant.id);
|
||||
mediaType = MEDIA_TYPE.SCREENSHARE;
|
||||
} else {
|
||||
participantId = participant.id;
|
||||
mediaType = MEDIA_TYPE.VIDEO;
|
||||
}
|
||||
|
||||
return getTrackByMediaTypeAndParticipant(tracks, mediaType, participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track of specified media type for specified participant id.
|
||||
*
|
||||
|
@ -427,6 +454,19 @@ export function getTrackByMediaTypeAndParticipant(
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track of given fakeScreenshareParticipantId.
|
||||
*
|
||||
* @param {Track[]} tracks - List of all tracks.
|
||||
* @param {string} fakeScreenshareParticipantId - Fake Screenshare Participant ID.
|
||||
* @returns {(Track|undefined)}
|
||||
*/
|
||||
export function getFakeScreenshareParticipantTrack(tracks, fakeScreenshareParticipantId) {
|
||||
const participantId = getFakeScreenShareParticipantOwnerId(fakeScreenshareParticipantId);
|
||||
|
||||
return getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.SCREENSHARE, participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track source name of specified media type for specified participant id.
|
||||
*
|
||||
|
|
|
@ -8,7 +8,7 @@ import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
|||
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { getMultipleVideoSupportFeatureFlag } from '../config';
|
||||
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
|
||||
import { getAvailableDevices } from '../devices/actions';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
|
@ -24,9 +24,11 @@ import {
|
|||
setScreenshareMuted,
|
||||
SCREENSHARE_MUTISM_AUTHORITY
|
||||
} from '../media';
|
||||
import { participantLeft, participantJoined, getParticipantById } from '../participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
SCREENSHARE_TRACK_MUTED_UPDATED,
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_MUTE_UNMUTE_FAILED,
|
||||
|
@ -50,6 +52,7 @@ import {
|
|||
isUserInteractionRequiredForUnmute,
|
||||
setTrackMuted
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
|
@ -72,6 +75,13 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
store.dispatch(getAvailableDevices());
|
||||
}
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(store.getState())
|
||||
&& action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP
|
||||
&& !action.track.jitsiTrack.isMuted()
|
||||
) {
|
||||
createFakeScreenShareParticipant(store, action);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case TRACK_NO_DATA_FROM_SOURCE: {
|
||||
|
@ -81,7 +91,40 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
case SCREENSHARE_TRACK_MUTED_UPDATED: {
|
||||
const state = store.getState();
|
||||
|
||||
if (!getSourceNameSignalingFeatureFlag(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { track, muted } = action;
|
||||
|
||||
if (muted) {
|
||||
const conference = getCurrentConference(state);
|
||||
const participantId = track?.jitsiTrack.getSourceName();
|
||||
|
||||
store.dispatch(participantLeft(participantId, conference));
|
||||
}
|
||||
|
||||
if (!muted) {
|
||||
createFakeScreenShareParticipant(store, action);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case TRACK_REMOVED: {
|
||||
const state = store.getState();
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state) && action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
|
||||
const conference = getCurrentConference(state);
|
||||
const participantId = action.track.jitsiTrack.getSourceName();
|
||||
|
||||
store.dispatch(participantLeft(participantId, conference));
|
||||
}
|
||||
|
||||
_removeNoDataFromSourceNotification(store, action.track);
|
||||
break;
|
||||
}
|
||||
|
@ -326,6 +369,32 @@ function _handleNoDataFromSourceErrors(store, action) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fake participant for screen share using the track's source name as the participant id.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is dispatched.
|
||||
* @param {Action} action - The redux action dispatched in the specified store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function createFakeScreenShareParticipant({ dispatch, getState }, { track }) {
|
||||
const state = getState();
|
||||
const participantId = track.jitsiTrack?.getParticipantId?.();
|
||||
const participant = getParticipantById(state, participantId);
|
||||
|
||||
if (participant.name) {
|
||||
dispatch(participantJoined({
|
||||
conference: state['features/base/conference'].conference,
|
||||
id: track.jitsiTrack.getSourceName(),
|
||||
isFakeScreenShareParticipant: true,
|
||||
isLocalScreenShare: track?.jitsiTrack.isLocal(),
|
||||
name: `${participant.name}'s screen`
|
||||
}));
|
||||
} else {
|
||||
logger.error(`Failed to create a screenshare participant for participantId: ${participantId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
|
||||
* specific redux store.
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
export const commonClassName = {
|
||||
emptyList: 'empty-list',
|
||||
muteDialog: 'mute-dialog',
|
||||
overflowMenuItem: 'overflow-menu-item',
|
||||
overflowMenuItemIcon: 'overflow-menu-item-icon',
|
||||
participantAvatar: 'participant-avatar',
|
||||
|
@ -30,6 +31,25 @@ export const commonStyles = (theme: Object) => {
|
|||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
[commonClassName.muteDialog]: {
|
||||
'& .separator-line': {
|
||||
margin: `${theme.spacing(4)}px 0 ${theme.spacing(4)}px -20px`,
|
||||
padding: '0 20px',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: '#5E6D7A'
|
||||
},
|
||||
|
||||
'& .control-row': {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: `${theme.spacing(3)}px`,
|
||||
|
||||
'& label': {
|
||||
fontSize: '14px'
|
||||
}
|
||||
}
|
||||
},
|
||||
[commonClassName.overflowMenuItem]: {
|
||||
alignItems: 'center',
|
||||
color: theme.palette.text01,
|
||||
|
@ -98,7 +118,7 @@ export const commonStyles = (theme: Object) => {
|
|||
}
|
||||
},
|
||||
[commonClassName.participantAvatar]: {
|
||||
margin: `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(2)} 0`
|
||||
margin: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(2)}px 0`
|
||||
},
|
||||
[commonClassName.toolboxIcon]: {
|
||||
display: 'flex',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
|
||||
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
|
||||
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { appNavigate } from '../../../app/actions';
|
||||
|
@ -22,7 +22,6 @@ import { CalleeInfoContainer } from '../../../invite';
|
|||
import { LargeVideo } from '../../../large-video';
|
||||
import { KnockingParticipantList } from '../../../lobby/components/native';
|
||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { BackButtonRegistry } from '../../../mobile/back-button';
|
||||
import { navigate }
|
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
@ -166,7 +165,7 @@ class Conference extends AbstractConference<Props, State> {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
BackButtonRegistry.addListener(this._onHardwareBackPress);
|
||||
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,7 +195,7 @@ class Conference extends AbstractConference<Props, State> {
|
|||
*/
|
||||
componentWillUnmount() {
|
||||
// Tear handling any hardware button presses for back navigation down.
|
||||
BackButtonRegistry.removeListener(this._onHardwareBackPress);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
|
||||
clearTimeout(this._expandedLabelTimeout.current);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -11,7 +12,7 @@ import { translate } from '../../../base/i18n';
|
|||
import { connect as reactReduxConnect } from '../../../base/redux';
|
||||
import { setColorAlpha } from '../../../base/util';
|
||||
import { Chat } from '../../../chat';
|
||||
import { Filmstrip } from '../../../filmstrip';
|
||||
import { MainFilmstrip, StageFilmstrip, shouldDisplayStageFilmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { LobbyScreen } from '../../../lobby';
|
||||
|
@ -55,7 +56,7 @@ const FULL_SCREEN_EVENTS = [
|
|||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const LAYOUT_CLASSNAMES = {
|
||||
export const LAYOUT_CLASSNAMES = {
|
||||
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
|
||||
|
@ -95,13 +96,18 @@ type Props = AbstractProps & {
|
|||
/**
|
||||
* If lobby page is visible or not.
|
||||
*/
|
||||
_showLobby: boolean,
|
||||
_showLobby: boolean,
|
||||
|
||||
/**
|
||||
* If prejoin page is visible or not.
|
||||
*/
|
||||
_showPrejoin: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the stage filmstrip should be displayed.
|
||||
*/
|
||||
_showStageFilmstrip: boolean,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
|
@ -214,7 +220,8 @@ class Conference extends AbstractConference<Props, *> {
|
|||
_notificationsVisible,
|
||||
_overflowDrawer,
|
||||
_showLobby,
|
||||
_showPrejoin
|
||||
_showPrejoin,
|
||||
_showStageFilmstrip
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -222,24 +229,24 @@ class Conference extends AbstractConference<Props, *> {
|
|||
id = 'layout_wrapper'
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
onMouseMove = { this._onMouseMove } >
|
||||
onMouseMove = { this._onMouseMove }
|
||||
ref = { this._setBackground }>
|
||||
<Chat />
|
||||
<div
|
||||
className = { _layoutClassName }
|
||||
className = { clsx(_layoutClassName, _showStageFilmstrip && 'stage-filmstrip') }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
|
||||
ref = { this._setBackground }>
|
||||
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
|
||||
<ConferenceInfo />
|
||||
|
||||
<Notice />
|
||||
<div
|
||||
id = 'videospace'
|
||||
onTouchStart = { this._onVidespaceTouchStart }>
|
||||
<LargeVideo />
|
||||
<Filmstrip />
|
||||
{_showStageFilmstrip && <StageFilmstrip />}
|
||||
<MainFilmstrip />
|
||||
</div>
|
||||
|
||||
{ _showPrejoin || _showLobby || <Toolbox /> }
|
||||
<Chat />
|
||||
|
||||
{_notificationsVisible && (_overflowDrawer
|
||||
? <JitsiPortal className = 'notification-portal'>
|
||||
|
@ -395,7 +402,8 @@ function _mapStateToProps(state) {
|
|||
_overflowDrawer: overflowDrawer,
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showLobby: getIsLobbyVisible(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state)
|
||||
_showPrejoin: isPrejoinPageVisible(state),
|
||||
_showStageFilmstrip: shouldDisplayStageFilmstrip(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import { MEDIA_TYPE } from '../../../base/media';
|
|||
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
|
||||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import {
|
||||
getFakeScreenshareParticipantTrack,
|
||||
getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import {
|
||||
isParticipantConnectionStatusInactive,
|
||||
isParticipantConnectionStatusInterrupted,
|
||||
|
@ -366,12 +368,18 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
|||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { participantId } = ownProps;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
|
||||
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
|
||||
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
|
||||
|
||||
const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
|
||||
|
||||
let firstVideoTrack;
|
||||
|
||||
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
|
||||
firstVideoTrack = getFakeScreenshareParticipantTrack(tracks, participantId);
|
||||
} else {
|
||||
firstVideoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
|
||||
}
|
||||
|
||||
const _isConnectionStatusInactive = sourceNameSignalingEnabled
|
||||
? isTrackStreamingStatusInactive(firstVideoTrack)
|
||||
: isParticipantConnectionStatusInactive(participant);
|
||||
|
|
|
@ -87,6 +87,12 @@ type Props = AbstractProps & {
|
|||
*/
|
||||
_enableSaveLogs: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the displays stats are for screen share. This prop is behind the sourceNameSignaling feature
|
||||
* flag.
|
||||
*/
|
||||
_isFakeScreenShareParticipant: Boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the displays stats are for local video.
|
||||
*/
|
||||
|
@ -199,6 +205,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
|
|||
e2eRtt = { e2eRtt }
|
||||
enableSaveLogs = { this.props._enableSaveLogs }
|
||||
framerate = { framerate }
|
||||
isFakeScreenShareParticipant = { this.props._isFakeScreenShareParticipant }
|
||||
isLocalVideo = { this.props._isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
onSaveLogs = { this.props._onSaveLogs }
|
||||
|
@ -334,10 +341,11 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
|||
_connectionStatus: participant?.connectionStatus,
|
||||
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
|
||||
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
|
||||
_isLocalVideo: participant?.local,
|
||||
_region: participant?.region,
|
||||
_isConnectionStatusInactive,
|
||||
_isConnectionStatusInterrupted
|
||||
_isConnectionStatusInterrupted,
|
||||
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
|
||||
_isLocalVideo: participant?.local,
|
||||
_region: participant?.region
|
||||
};
|
||||
|
||||
if (conference) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* @flow */
|
||||
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isMobileBrowser } from '../../../features/base/environment/utils';
|
||||
|
@ -90,6 +91,11 @@ type Props = {
|
|||
*/
|
||||
isLocalVideo: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the statistics are for screen share.
|
||||
*/
|
||||
isFakeScreenShareParticipant: boolean,
|
||||
|
||||
/**
|
||||
* The send-side max enabled resolution (aka the highest layer that is not
|
||||
* suspended on the send-side).
|
||||
|
@ -172,15 +178,48 @@ function onClick(event) {
|
|||
|
||||
const styles = theme => {
|
||||
return {
|
||||
actions: {
|
||||
margin: '10px auto',
|
||||
textAlign: 'center'
|
||||
},
|
||||
connectionStatsTable: {
|
||||
'&, & > table': {
|
||||
fontSize: '12px',
|
||||
fontWeight: '400',
|
||||
|
||||
'& td': {
|
||||
padding: '2px 0'
|
||||
}
|
||||
},
|
||||
'& > table': {
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
'& td:nth-child(n-1)': {
|
||||
paddingLeft: '5px'
|
||||
},
|
||||
|
||||
'& $upload, & $download': {
|
||||
marginRight: '2px'
|
||||
}
|
||||
},
|
||||
contextMenu: {
|
||||
position: 'relative',
|
||||
marginTop: 0,
|
||||
right: 'auto',
|
||||
padding: `${theme.spacing(2)}px ${theme.spacing(1)}px`,
|
||||
marginLeft: '4px',
|
||||
marginRight: '4px',
|
||||
marginBottom: '4px'
|
||||
}
|
||||
marginLeft: `${theme.spacing(1)}px`,
|
||||
marginRight: `${theme.spacing(1)}px`,
|
||||
marginBottom: `${theme.spacing(1)}px`
|
||||
},
|
||||
download: {},
|
||||
mobile: {
|
||||
margin: `${theme.spacing(3)}px`
|
||||
},
|
||||
status: {
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
upload: {}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -197,8 +236,18 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props;
|
||||
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
|
||||
const {
|
||||
classes,
|
||||
disableShowMoreStats,
|
||||
enableSaveLogs,
|
||||
isFakeScreenShareParticipant,
|
||||
isLocalVideo
|
||||
} = this.props;
|
||||
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
|
||||
|
||||
if (isFakeScreenShareParticipant) {
|
||||
return this._renderScreenShareStatus();
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
|
@ -209,7 +258,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
className = { className }
|
||||
onClick = { onClick }>
|
||||
{ this._renderStatistics() }
|
||||
<div className = 'connection-actions'>
|
||||
<div className = { classes.actions }>
|
||||
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
|
||||
{ !disableShowMoreStats && this._renderShowMoreLink() }
|
||||
</div>
|
||||
|
@ -219,6 +268,34 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ReactElement that will display connection statistics for a screen share thumbnail.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderScreenShareStatus() {
|
||||
const { classes } = this.props;
|
||||
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
|
||||
|
||||
return (<ContextMenu
|
||||
className = { classes.contextMenu }
|
||||
hidden = { false }
|
||||
inDrawer = { true }>
|
||||
<div
|
||||
className = { className }
|
||||
onClick = { onClick }>
|
||||
{ <table className = 'connection-info__container'>
|
||||
<tbody>
|
||||
{ this._renderResolution() }
|
||||
{ this._renderFrameRate() }
|
||||
</tbody>
|
||||
</table> }
|
||||
|
||||
</div>
|
||||
</ContextMenu>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table as ReactElement that will display additional statistics
|
||||
* related to bandwidth and transport for the local user.
|
||||
|
@ -230,7 +307,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
const { isLocalVideo } = this.props;
|
||||
|
||||
return (
|
||||
<table className = 'connection-info__container'>
|
||||
<table>
|
||||
<tbody>
|
||||
{ isLocalVideo ? this._renderBandwidth() : null }
|
||||
{ isLocalVideo ? this._renderTransport() : null }
|
||||
|
@ -251,6 +328,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderBandwidth() {
|
||||
const { classes } = this.props;
|
||||
const { download, upload } = this.props.bandwidth || {};
|
||||
|
||||
return (
|
||||
|
@ -259,11 +337,11 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
{ this.props.t('connectionindicator.bandwidth') }
|
||||
</td>
|
||||
<td>
|
||||
<span className = 'connection-info__download'>
|
||||
<span className = { classes.download }>
|
||||
↓
|
||||
</span>
|
||||
{ download ? `${download} Kbps` : 'N/A' }
|
||||
<span className = 'connection-info__upload'>
|
||||
<span className = { classes.upload }>
|
||||
↑
|
||||
</span>
|
||||
{ upload ? `${upload} Kbps` : 'N/A' }
|
||||
|
@ -280,6 +358,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderBitrate() {
|
||||
const { classes } = this.props;
|
||||
const { download, upload } = this.props.bitrate || {};
|
||||
|
||||
return (
|
||||
|
@ -290,11 +369,11 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className = 'connection-info__download'>
|
||||
<span className = { classes.download }>
|
||||
↓
|
||||
</span>
|
||||
{ download ? `${download} Kbps` : 'N/A' }
|
||||
<span className = 'connection-info__upload'>
|
||||
<span className = { classes.upload }>
|
||||
↑
|
||||
</span>
|
||||
{ upload ? `${upload} Kbps` : 'N/A' }
|
||||
|
@ -410,8 +489,10 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderConnectionSummary() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<tr className = 'connection-info__status'>
|
||||
<tr className = { classes.status }>
|
||||
<td>
|
||||
<span>{ this.props.t('connectionindicator.status') }</span>
|
||||
</td>
|
||||
|
@ -527,7 +608,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderPacketLoss() {
|
||||
const { packetLoss, t } = this.props;
|
||||
const { classes, packetLoss, t } = this.props;
|
||||
let packetLossTableData;
|
||||
|
||||
if (packetLoss) {
|
||||
|
@ -535,11 +616,11 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
|
||||
packetLossTableData = (
|
||||
<td>
|
||||
<span className = 'connection-info__download'>
|
||||
<span className = { classes.download }>
|
||||
↓
|
||||
</span>
|
||||
{ download === null ? 'N/A' : `${download}%` }
|
||||
<span className = 'connection-info__upload'>
|
||||
<span className = { classes.upload }>
|
||||
↑
|
||||
</span>
|
||||
{ upload === null ? 'N/A' : `${upload}%` }
|
||||
|
@ -650,7 +731,7 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
const isRemoteVideo = !this.props.isLocalVideo;
|
||||
|
||||
return (
|
||||
<table className = 'connection-info__container'>
|
||||
<table>
|
||||
<tbody>
|
||||
{ this._renderConnectionSummary() }
|
||||
{ this._renderBitrate() }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
@ -40,7 +41,7 @@ const useStyles = makeStyles(theme => {
|
|||
*
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
const DominantSpeakerName = () => {
|
||||
const StageParticipantNameLabel = () => {
|
||||
const classes = useStyles();
|
||||
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
|
||||
const nameToDisplay = largeVideoParticipant?.name;
|
||||
|
@ -56,7 +57,11 @@ const DominantSpeakerName = () => {
|
|||
if (showDisplayName && nameToDisplay && selectedId !== localId && !isTileView) {
|
||||
return (
|
||||
<div
|
||||
className = { `${classes.badgeContainer}${toolboxVisible ? ` ${classes.containerElevated}` : ''}` }>
|
||||
className = { clsx(
|
||||
'stage-participant-label',
|
||||
classes.badgeContainer,
|
||||
toolboxVisible && classes.containerElevated
|
||||
) }>
|
||||
<DisplayNameBadge name = { nameToDisplay } />
|
||||
</div>
|
||||
);
|
||||
|
@ -65,4 +70,4 @@ const DominantSpeakerName = () => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export default DominantSpeakerName;
|
||||
export default StageParticipantNameLabel;
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
export { default as DisplayName } from './DisplayName';
|
||||
export { default as DisplayNamePrompt } from './DisplayNamePrompt';
|
||||
export { default as DominantSpeakerName } from './DominantSpeakerName';
|
||||
export { default as StageParticipantNameLabel } from './StageParticipantNameLabel';
|
||||
|
|
|
@ -35,7 +35,7 @@ const queue = [];
|
|||
let lastValidFaceBox;
|
||||
|
||||
const detect = async message => {
|
||||
const { baseUrl, imageBitmap, isHorizontallyFlipped, threshold } = message.data;
|
||||
const { baseUrl, image, isHorizontallyFlipped, threshold } = message.data;
|
||||
|
||||
if (initInProgress || initError) {
|
||||
return;
|
||||
|
@ -70,8 +70,8 @@ const detect = async message => {
|
|||
|
||||
tf.engine().startScope();
|
||||
|
||||
const image = tf.browser.fromPixels(imageBitmap);
|
||||
const detections = await model.estimateFaces(image, false, isHorizontallyFlipped, false);
|
||||
const imageTensor = tf.browser.fromPixels(image);
|
||||
const detections = await model.estimateFaces(imageTensor, false, isHorizontallyFlipped, false);
|
||||
|
||||
tf.engine().endScope();
|
||||
|
||||
|
@ -80,10 +80,10 @@ const detect = async message => {
|
|||
if (detections.length) {
|
||||
faceBox = {
|
||||
// normalize to percentage based
|
||||
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / imageBitmap.width),
|
||||
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / imageBitmap.width),
|
||||
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / imageBitmap.height),
|
||||
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / imageBitmap.height)
|
||||
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / image.width),
|
||||
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / image.width),
|
||||
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / image.height),
|
||||
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / image.height)
|
||||
};
|
||||
|
||||
if (lastValidFaceBox && Math.abs(lastValidFaceBox.left - faceBox.left) < threshold) {
|
||||
|
|
|
@ -44,6 +44,7 @@ export async function sendDataToWorker(
|
|||
}
|
||||
|
||||
let imageBitmap;
|
||||
let image;
|
||||
|
||||
try {
|
||||
imageBitmap = await imageCapture.grabFrame();
|
||||
|
@ -53,13 +54,28 @@ export async function sendDataToWorker(
|
|||
return;
|
||||
}
|
||||
|
||||
if (typeof OffscreenCanvas === 'undefined') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
context.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
image = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
|
||||
} else {
|
||||
image = imageBitmap;
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
id: DETECT_FACE_BOX,
|
||||
baseUrl: getBaseUrl(),
|
||||
imageBitmap,
|
||||
image,
|
||||
threshold,
|
||||
isHorizontallyFlipped
|
||||
});
|
||||
|
||||
imageBitmap.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -118,3 +118,44 @@ export const SET_USER_FILMSTRIP_WIDTH = 'SET_USER_FILMSTRIP_WIDTH';
|
|||
* }
|
||||
*/
|
||||
export const SET_USER_IS_RESIZING = 'SET_USER_IS_RESIZING';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the dimensions of the thumbnails in stage filmstrip view.
|
||||
*
|
||||
* {
|
||||
* type: SET_STAGE_FILMSTRIP_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }
|
||||
*/
|
||||
export const SET_STAGE_FILMSTRIP_DIMENSIONS = 'SET_STAGE_FILMSTRIP_DIMENSIONS';
|
||||
|
||||
/**
|
||||
* The type of Redux action which adds a participant to the active list
|
||||
* (the participants displayed on the stage filmstrip).
|
||||
* {
|
||||
* type: ADD_STAGE_PARTICIPANT,
|
||||
* participantId: string,
|
||||
* pinned: boolean
|
||||
* }
|
||||
*/
|
||||
export const ADD_STAGE_PARTICIPANT = 'ADD_STAGE_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* The type of Redux action which removes a participant from the active list
|
||||
* (the participants displayed on the stage filmstrip).
|
||||
* {
|
||||
* type: REMOVE_STAGE_PARTICIPANT,
|
||||
* participantId: string,
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_STAGE_PARTICIPANT = 'REMOVE_STAGE_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* The type of Redux action which sets the active participants list
|
||||
* (the participants displayed on the stage filmstrip).
|
||||
* {
|
||||
* type: SET_STAGE_PARTICIPANTS,
|
||||
* queue: Array<Object>
|
||||
* }
|
||||
*/
|
||||
export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getSourceNameSignalingFeatureFlag } from '../base/config';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
|
@ -11,8 +12,12 @@ import { shouldHideSelfView } from '../base/settings/functions.any';
|
|||
import { getMaxColumnCount } from '../video-layout';
|
||||
|
||||
import {
|
||||
ADD_STAGE_PARTICIPANT,
|
||||
REMOVE_STAGE_PARTICIPANT,
|
||||
SET_STAGE_PARTICIPANTS,
|
||||
SET_FILMSTRIP_WIDTH,
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_STAGE_FILMSTRIP_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_USER_FILMSTRIP_WIDTH,
|
||||
SET_USER_IS_RESIZING,
|
||||
|
@ -21,6 +26,7 @@ import {
|
|||
} from './actionTypes';
|
||||
import {
|
||||
HORIZONTAL_FILMSTRIP_MARGIN,
|
||||
MAX_ACTIVE_PARTICIPANTS,
|
||||
SCROLL_SIZE,
|
||||
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
|
@ -32,11 +38,12 @@ import {
|
|||
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
} from './constants';
|
||||
import {
|
||||
calculateNotResponsiveTileViewDimensions,
|
||||
calculateNonResponsiveTileViewDimensions,
|
||||
calculateResponsiveTileViewDimensions,
|
||||
calculateThumbnailSizeForHorizontalView,
|
||||
calculateThumbnailSizeForVerticalView,
|
||||
getNumberOfPartipantsForTileView,
|
||||
getVerticalViewMaxWidth,
|
||||
isFilmstripResizable,
|
||||
showGridInVerticalView
|
||||
} from './functions';
|
||||
|
@ -46,9 +53,6 @@ export * from './actions.any';
|
|||
/**
|
||||
* Sets the dimensions of the tile view grid.
|
||||
*
|
||||
* @param {Object} dimensions - Whether the filmstrip is visible.
|
||||
* @param {Object | Function} stateful - An object or function that can be
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setTileViewDimensions() {
|
||||
|
@ -70,7 +74,7 @@ export function setTileViewDimensions() {
|
|||
columns,
|
||||
rows
|
||||
} = disableResponsiveTiles
|
||||
? calculateNotResponsiveTileViewDimensions(state)
|
||||
? calculateNonResponsiveTileViewDimensions(state)
|
||||
: calculateResponsiveTileViewDimensions({
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
|
@ -119,6 +123,7 @@ export function setVerticalViewDimensions() {
|
|||
const resizableFilmstrip = isFilmstripResizable(state);
|
||||
const _verticalViewGrid = showGridInVerticalView(state);
|
||||
const numberOfRemoteParticipants = getRemoteParticipantCount(state);
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
|
||||
let gridView = {};
|
||||
let thumbnails = {};
|
||||
|
@ -142,8 +147,8 @@ export function setVerticalViewDimensions() {
|
|||
clientWidth: filmstripWidth.current,
|
||||
clientHeight,
|
||||
disableTileEnlargement: false,
|
||||
isVerticalFilmstrip: true,
|
||||
maxColumns,
|
||||
noHorizontalContainerMargin: true,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles
|
||||
});
|
||||
|
@ -176,6 +181,20 @@ export function setVerticalViewDimensions() {
|
|||
= thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE;
|
||||
remoteVideosContainerHeight
|
||||
= clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height) - VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
|
||||
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
// Account for the height of the local screen share thumbnail when calculating the height of the remote
|
||||
// videos container.
|
||||
const localCameraThumbnailHeight = thumbnails?.local?.height;
|
||||
const localScreenShareThumbnailHeight
|
||||
= localScreenShare && !disableSelfView ? thumbnails?.local?.height : 0;
|
||||
|
||||
remoteVideosContainerHeight = clientHeight
|
||||
- localCameraThumbnailHeight
|
||||
- localScreenShareThumbnailHeight
|
||||
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
|
||||
}
|
||||
|
||||
hasScroll
|
||||
= remoteVideosContainerHeight
|
||||
< (thumbnails?.remote.height + TILE_VERTICAL_MARGIN) * numberOfRemoteParticipants;
|
||||
|
@ -230,6 +249,68 @@ export function setHorizontalViewDimensions() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the stage filmstrip tile view grid.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setStageFilmstripViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
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 {
|
||||
height,
|
||||
width,
|
||||
columns,
|
||||
rows
|
||||
} = disableResponsiveTiles
|
||||
? calculateNonResponsiveTileViewDimensions(state, true)
|
||||
: calculateResponsiveTileViewDimensions({
|
||||
clientWidth: clientWidth - verticalWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
maxColumns,
|
||||
noHorizontalContainerMargin: verticalWidth > 0,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles
|
||||
});
|
||||
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
|
||||
const hasScroll = clientHeight < 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);
|
||||
|
||||
dispatch({
|
||||
type: SET_STAGE_FILMSTRIP_DIMENSIONS,
|
||||
dimensions: {
|
||||
gridDimensions: {
|
||||
columns,
|
||||
rows
|
||||
},
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
hasScroll
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a click on the n-th video.
|
||||
*
|
||||
|
@ -313,3 +394,44 @@ export function setUserIsResizing(resizing: boolean) {
|
|||
resizing
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add participant to the active participants list.
|
||||
*
|
||||
* @param {string} participantId - The Id of the participant to be added.
|
||||
* @param {boolean?} pinned - Whether the participant is pinned or not.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addStageParticipant(participantId, pinned = false) {
|
||||
return {
|
||||
type: ADD_STAGE_PARTICIPANT,
|
||||
participantId,
|
||||
pinned
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove participant from the active participants list.
|
||||
*
|
||||
* @param {string} participantId - The Id of the participant to be removed.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function removeStageParticipant(participantId) {
|
||||
return {
|
||||
type: REMOVE_STAGE_PARTICIPANT,
|
||||
participantId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active participants list.
|
||||
*
|
||||
* @param {Array<Object>} queue - The new list.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setStageParticipants(queue) {
|
||||
return {
|
||||
type: SET_STAGE_PARTICIPANTS,
|
||||
queue
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
// @flow
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { VideoTrack } from '../../../base/media';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
||||
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
|
||||
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: Object,
|
||||
|
||||
/**
|
||||
* The class name that will be used for the container.
|
||||
*/
|
||||
containerClassName: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the thumbnail is hovered or not.
|
||||
*/
|
||||
isHovered: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether we are currently running in a mobile browser.
|
||||
*/
|
||||
isMobile: boolean,
|
||||
|
||||
/**
|
||||
* Click handler.
|
||||
*/
|
||||
onClick: Function,
|
||||
|
||||
/**
|
||||
* Mouse enter handler.
|
||||
*/
|
||||
onMouseEnter: Function,
|
||||
|
||||
/**
|
||||
* Mouse leave handler.
|
||||
*/
|
||||
onMouseLeave: Function,
|
||||
|
||||
/**
|
||||
* Mouse move handler.
|
||||
*/
|
||||
onMouseMove: Function,
|
||||
|
||||
/**
|
||||
* Touch end handler.
|
||||
*/
|
||||
onTouchEnd: Function,
|
||||
|
||||
/**
|
||||
* Touch move handler.
|
||||
*/
|
||||
onTouchMove: Function,
|
||||
|
||||
/**
|
||||
* Touch start handler.
|
||||
*/
|
||||
onTouchStart: Function,
|
||||
|
||||
/**
|
||||
* The ID of the fake screen share participant.
|
||||
*/
|
||||
participantId: string,
|
||||
|
||||
/**
|
||||
* An object with the styles for thumbnail.
|
||||
*/
|
||||
styles: Object,
|
||||
|
||||
/**
|
||||
* JitsiTrack instance.
|
||||
*/
|
||||
videoTrack: Object
|
||||
}
|
||||
|
||||
const FakeScreenShareParticipant = ({
|
||||
classes,
|
||||
containerClassName,
|
||||
isHovered,
|
||||
isMobile,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onMouseMove,
|
||||
onTouchEnd,
|
||||
onTouchMove,
|
||||
onTouchStart,
|
||||
participantId,
|
||||
styles,
|
||||
videoTrack
|
||||
}: Props) => {
|
||||
const currentLayout = useSelector(getCurrentLayout);
|
||||
const videoTrackId = videoTrack?.jitsiTrack?.getId();
|
||||
const video = videoTrack && <VideoTrack
|
||||
id = { `remoteVideo_${videoTrackId || ''}` }
|
||||
muted = { true }
|
||||
style = { styles.video }
|
||||
videoTrack = { videoTrack } />;
|
||||
|
||||
|
||||
return (
|
||||
<span
|
||||
className = { containerClassName }
|
||||
id = { `participant_${participantId}` }
|
||||
{ ...(isMobile
|
||||
? {
|
||||
onTouchEnd,
|
||||
onTouchMove,
|
||||
onTouchStart
|
||||
}
|
||||
: {
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave
|
||||
}
|
||||
) }
|
||||
style = { styles.thumbnail }>
|
||||
{video}
|
||||
<div className = { classes.containerBackground } />
|
||||
<div
|
||||
className = { clsx(classes.indicatorsContainer,
|
||||
classes.indicatorsTopContainer,
|
||||
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
|
||||
) }>
|
||||
<ThumbnailTopIndicators
|
||||
currentLayout = { currentLayout }
|
||||
isFakeScreenShareParticipant = { true }
|
||||
isHovered = { isHovered }
|
||||
participantId = { participantId } />
|
||||
</div>
|
||||
<div
|
||||
className = { clsx(classes.indicatorsContainer,
|
||||
classes.indicatorsBottomContainer,
|
||||
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
|
||||
) }>
|
||||
<ThumbnailBottomIndicators
|
||||
className = { classes.indicatorsBackground }
|
||||
currentLayout = { currentLayout }
|
||||
local = { false }
|
||||
participantId = { participantId }
|
||||
showStatusIndicators = { false } />
|
||||
</div>
|
||||
</span>);
|
||||
};
|
||||
|
||||
export default FakeScreenShareParticipant;
|
|
@ -12,15 +12,16 @@ import {
|
|||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { getSourceNameSignalingFeatureFlag, getToolbarButtons } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { shouldHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { CHAT_SIZE } from '../../../chat';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { LAYOUTS } from '../../../video-layout';
|
||||
import {
|
||||
setFilmstripVisible,
|
||||
setVisibleRemoteParticipants,
|
||||
|
@ -30,19 +31,13 @@ import {
|
|||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DEFAULT_FILMSTRIP_WIDTH,
|
||||
FILMSTRIP_BREAKPOINT,
|
||||
FILMSTRIP_BREAKPOINT_OFFSET,
|
||||
MIN_STAGE_VIEW_WIDTH,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TOOLBAR_HEIGHT,
|
||||
TOOLBAR_HEIGHT_MOBILE
|
||||
TILE_VERTICAL_MARGIN
|
||||
} from '../../constants';
|
||||
import {
|
||||
getVerticalViewMaxWidth,
|
||||
isFilmstripResizable,
|
||||
shouldRemoteVideosBeVisible,
|
||||
showGridInVerticalView
|
||||
shouldRemoteVideosBeVisible
|
||||
} from '../../functions';
|
||||
|
||||
import AudioTracksContainer from './AudioTracksContainer';
|
||||
|
@ -63,6 +58,11 @@ type Props = {
|
|||
*/
|
||||
_className: string,
|
||||
|
||||
/**
|
||||
* Whether or not the chat is open.
|
||||
*/
|
||||
_chatOpen: boolean,
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
|
@ -113,6 +113,11 @@ type Props = {
|
|||
*/
|
||||
_isVerticalFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
|
||||
*/
|
||||
_localScreenShare: Object,
|
||||
|
||||
/**
|
||||
* The maximum width of the vertical filmstrip.
|
||||
*/
|
||||
|
@ -138,6 +143,11 @@ type Props = {
|
|||
*/
|
||||
_rows: number,
|
||||
|
||||
/**
|
||||
* Whether or not this is the stage filmstrip.
|
||||
*/
|
||||
_stageFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The height of the thumbnail.
|
||||
*/
|
||||
|
@ -158,6 +168,11 @@ type Props = {
|
|||
*/
|
||||
_verticalFilmstripWidth: ?number,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should have a background color.
|
||||
*/
|
||||
_verticalViewBackground: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should be displayed as grid.
|
||||
*/
|
||||
|
@ -295,11 +310,14 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
render() {
|
||||
const filmstripStyle = { };
|
||||
const {
|
||||
_chatOpen,
|
||||
_currentLayout,
|
||||
_disableSelfView,
|
||||
_localScreenShare,
|
||||
_resizableFilmstrip,
|
||||
_verticalFilmstripWidth,
|
||||
_stageFilmstrip,
|
||||
_visible,
|
||||
_verticalViewBackground,
|
||||
_verticalViewGrid,
|
||||
_verticalViewMaxWidth,
|
||||
classes
|
||||
|
@ -308,13 +326,20 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
filmstripStyle.maxWidth = _verticalViewMaxWidth;
|
||||
if (!_visible) {
|
||||
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
if (_stageFilmstrip && _visible) {
|
||||
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px - ${_chatOpen ? CHAT_SIZE : 0}px)`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let toolbar = null;
|
||||
|
||||
|
@ -332,10 +357,24 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalVideo'>
|
||||
<div id = 'filmstripLocalVideoThumbnail'>
|
||||
{
|
||||
!tileViewActive && <div id = 'filmstripLocalVideoThumbnail'>
|
||||
<Thumbnail
|
||||
key = 'local' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalScreenShare'>
|
||||
<div id = 'filmstripLocalScreenShareThumbnail'>
|
||||
{
|
||||
!tileViewActive && <Thumbnail
|
||||
key = 'local' />
|
||||
key = 'localScreenShare'
|
||||
participantID = { _localScreenShare.id } />
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -352,8 +391,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
this.props._className,
|
||||
classes.filmstrip,
|
||||
_verticalViewGrid && 'no-vertical-padding',
|
||||
_verticalFilmstripWidth + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
|
||||
&& classes.filmstripBackground) }
|
||||
_verticalViewBackground && classes.filmstripBackground) }
|
||||
style = { filmstripStyle }>
|
||||
{ toolbar }
|
||||
{_resizableFilmstrip
|
||||
|
@ -576,6 +614,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
_remoteParticipantsLength,
|
||||
_resizableFilmstrip,
|
||||
_rows,
|
||||
_stageFilmstrip,
|
||||
_thumbnailHeight,
|
||||
_thumbnailWidth,
|
||||
_verticalViewGrid
|
||||
|
@ -596,6 +635,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
height = { _filmstripHeight }
|
||||
initialScrollLeft = { 0 }
|
||||
initialScrollTop = { 0 }
|
||||
itemData = {{ stageFilmstrip: _stageFilmstrip }}
|
||||
itemKey = { this._gridItemKey }
|
||||
onItemsRendered = { this._onGridItemsRendered }
|
||||
overscanRowCount = { 1 }
|
||||
|
@ -759,129 +799,49 @@ class Filmstrip extends PureComponent <Props, State> {
|
|||
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { _hasScroll = false } = ownProps;
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { testing = {}, iAmRecorder } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
const { isOpen: shiftRight } = state['features/chat'];
|
||||
const {
|
||||
gridDimensions: dimensions = {},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
hasScroll: tileViewHasScroll,
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const _resizableFilmstrip = isFilmstripResizable(state);
|
||||
const _verticalViewGrid = showGridInVerticalView(state);
|
||||
let gridDimensions = dimensions;
|
||||
let _hasScroll = false;
|
||||
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const availableSpace = clientHeight - filmstripHeight;
|
||||
let filmstripPadding = 0;
|
||||
|
||||
if (availableSpace > 0) {
|
||||
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
|
||||
|
||||
if (paddingValue > 0) {
|
||||
filmstripPadding = paddingValue;
|
||||
}
|
||||
} else {
|
||||
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
|
||||
}
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
const collapseTileView = reduceHeight
|
||||
&& isMobileBrowser()
|
||||
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
|
||||
|
||||
const shouldReduceHeight = reduceHeight && (
|
||||
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
|
||||
const shouldReduceHeight = reduceHeight && isMobileBrowser();
|
||||
|
||||
let videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
const className = `${remoteVideosVisible || _verticalViewGrid ? '' : 'hide-videos'} ${
|
||||
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
|
||||
const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${
|
||||
shouldReduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
|
||||
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
_hasScroll = Boolean(tileViewHasScroll);
|
||||
if (_hasScroll) {
|
||||
videosClassName += ' has-scroll';
|
||||
}
|
||||
_thumbnailSize = tileViewThumbnailSize;
|
||||
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
|
||||
remoteFilmstripWidth = filmstripWidth;
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
const {
|
||||
remote,
|
||||
remoteVideosContainer,
|
||||
gridView,
|
||||
hasScroll
|
||||
} = state['features/filmstrip'].verticalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
|
||||
? TOOLBAR_HEIGHT : 0);
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
|
||||
if (_verticalViewGrid) {
|
||||
gridDimensions = gridView.gridDimensions;
|
||||
_thumbnailSize = gridView.thumbnailSize;
|
||||
|
||||
if (gridView.hasScroll) {
|
||||
videosClassName += ' has-scroll';
|
||||
}
|
||||
} else {
|
||||
_thumbnailSize = remote;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
_thumbnailSize = remote;
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height;
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
_columns: gridDimensions.columns,
|
||||
_currentLayout,
|
||||
_chatOpen: state['features/chat'].isOpen,
|
||||
_disableSelfView: disableSelfView,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_hasScroll,
|
||||
_iAmRecorder: Boolean(iAmRecorder),
|
||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_isToolboxVisible: isToolboxVisible(state),
|
||||
_isVerticalFilmstrip: _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
|
||||
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
|
||||
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
|
||||
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
|
||||
_remoteParticipantsLength: remoteParticipants.length,
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_resizableFilmstrip,
|
||||
_rows: gridDimensions.rows,
|
||||
_thumbnailWidth: _thumbnailSize?.width,
|
||||
_thumbnailHeight: _thumbnailSize?.height,
|
||||
_thumbnailsReordered: enableThumbnailReordering,
|
||||
_verticalFilmstripWidth: verticalFilmstripWidth.current,
|
||||
_videosClassName: videosClassName,
|
||||
_visible: visible,
|
||||
_verticalViewGrid,
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state)
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
|
||||
_videosClassName: videosClassName
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
FILMSTRIP_BREAKPOINT,
|
||||
FILMSTRIP_BREAKPOINT_OFFSET,
|
||||
TOOLBAR_HEIGHT,
|
||||
TOOLBAR_HEIGHT_MOBILE } from '../../constants';
|
||||
import { isFilmstripResizable, showGridInVerticalView } from '../../functions.web';
|
||||
|
||||
import Filmstrip from './Filmstrip';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
_currentLayout: string,
|
||||
|
||||
/**
|
||||
* The number of columns in tile view.
|
||||
*/
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* The width of the filmstrip.
|
||||
*/
|
||||
_filmstripWidth: number,
|
||||
|
||||
/**
|
||||
* The height of the filmstrip.
|
||||
*/
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip has scroll or not.
|
||||
*/
|
||||
_hasScroll: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the current layout is vertical filmstrip.
|
||||
*/
|
||||
_isVerticalFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
_remoteParticipants: Array<Object>,
|
||||
|
||||
/**
|
||||
* The length of the remote participants array.
|
||||
*/
|
||||
_remoteParticipantsLength: number,
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip should be user-resizable.
|
||||
*/
|
||||
_resizableFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
_rows: number,
|
||||
|
||||
/**
|
||||
* The height of the thumbnail.
|
||||
*/
|
||||
_thumbnailHeight: number,
|
||||
|
||||
/**
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
_thumbnailWidth: number,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should have a background color.
|
||||
*/
|
||||
_verticalViewBackground: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should be displayed as grid.
|
||||
*/
|
||||
_verticalViewGrid: boolean,
|
||||
|
||||
/**
|
||||
* Additional CSS class names to add to the container of all the thumbnails.
|
||||
*/
|
||||
_videosClassName: string,
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip videos should currently be displayed.
|
||||
*/
|
||||
_visible: boolean
|
||||
};
|
||||
|
||||
const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const {
|
||||
gridDimensions: dimensions = {},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
hasScroll: tileViewHasScroll,
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const _resizableFilmstrip = isFilmstripResizable(state);
|
||||
const _verticalViewGrid = showGridInVerticalView(state);
|
||||
let gridDimensions = dimensions;
|
||||
let _hasScroll = false;
|
||||
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const availableSpace = clientHeight - filmstripHeight;
|
||||
let filmstripPadding = 0;
|
||||
|
||||
if (availableSpace > 0) {
|
||||
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
|
||||
|
||||
if (paddingValue > 0) {
|
||||
filmstripPadding = paddingValue;
|
||||
}
|
||||
} else {
|
||||
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
|
||||
}
|
||||
|
||||
const collapseTileView = reduceHeight
|
||||
&& isMobileBrowser()
|
||||
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
|
||||
|
||||
const shouldReduceHeight = reduceHeight && (
|
||||
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
|
||||
|
||||
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
_hasScroll = Boolean(tileViewHasScroll);
|
||||
_thumbnailSize = tileViewThumbnailSize;
|
||||
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
|
||||
remoteFilmstripWidth = filmstripWidth;
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
const {
|
||||
remote,
|
||||
remoteVideosContainer,
|
||||
gridView,
|
||||
hasScroll
|
||||
} = state['features/filmstrip'].verticalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
|
||||
? TOOLBAR_HEIGHT : 0);
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
|
||||
if (_verticalViewGrid) {
|
||||
gridDimensions = gridView.gridDimensions;
|
||||
_thumbnailSize = gridView.thumbnailSize;
|
||||
_hasScroll = gridView.hasScroll;
|
||||
} else {
|
||||
_thumbnailSize = remote;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
_thumbnailSize = remote;
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height;
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_columns: gridDimensions.columns,
|
||||
_currentLayout,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_hasScroll,
|
||||
_remoteParticipantsLength: remoteParticipants.length,
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_resizableFilmstrip,
|
||||
_rows: gridDimensions.rows,
|
||||
_thumbnailWidth: _thumbnailSize?.width,
|
||||
_thumbnailHeight: _thumbnailSize?.height,
|
||||
_visible: visible,
|
||||
_verticalViewGrid,
|
||||
_verticalViewBackground: verticalFilmstripWidth.current + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(MainFilmstrip);
|
|
@ -0,0 +1,74 @@
|
|||
/* @flow */
|
||||
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IconPinParticipant } from '../../../base/icons';
|
||||
import { BaseIndicator } from '../../../base/react';
|
||||
import { getActiveParticipantsIds } from '../../functions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link PinnedIndicator}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The font-size for the icon.
|
||||
*/
|
||||
iconSize: number,
|
||||
|
||||
/**
|
||||
* The participant id who we want to render the raised hand indicator
|
||||
* for.
|
||||
*/
|
||||
participantId: string,
|
||||
|
||||
/**
|
||||
* From which side of the indicator the tooltip should appear from.
|
||||
*/
|
||||
tooltipPosition: string
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(() => {
|
||||
return {
|
||||
pinnedIndicator: {
|
||||
backgroundColor: 'rgba(0, 0, 0, .7)',
|
||||
padding: '2px',
|
||||
zIndex: 3,
|
||||
display: 'inline-block',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Thumbnail badge showing that the participant would like to speak.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const PinnedIndicator = ({
|
||||
iconSize,
|
||||
participantId,
|
||||
tooltipPosition
|
||||
}: Props) => {
|
||||
const isPinned = useSelector(getActiveParticipantsIds).find(id => id === participantId);
|
||||
const styles = useStyles();
|
||||
|
||||
if (!isPinned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { styles.pinnedIndicator }>
|
||||
<BaseIndicator
|
||||
icon = { IconPinParticipant }
|
||||
iconSize = { `${iconSize}px` }
|
||||
tooltipKey = 'pinnedParticipant'
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinnedIndicator;
|
|
@ -0,0 +1,161 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
TOOLBAR_HEIGHT_MOBILE
|
||||
} from '../../constants';
|
||||
import { getActiveParticipantsIds } from '../../functions';
|
||||
|
||||
import Filmstrip from './Filmstrip';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
_currentLayout: string,
|
||||
|
||||
/**
|
||||
* The number of columns in tile view.
|
||||
*/
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* The width of the filmstrip.
|
||||
*/
|
||||
_filmstripWidth: number,
|
||||
|
||||
/**
|
||||
* The height of the filmstrip.
|
||||
*/
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether or not the current layout is vertical filmstrip.
|
||||
*/
|
||||
_isVerticalFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
_remoteParticipants: Array<Object>,
|
||||
|
||||
/**
|
||||
* The length of the remote participants array.
|
||||
*/
|
||||
_remoteParticipantsLength: number,
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip should be user-resizable.
|
||||
*/
|
||||
_resizableFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
_rows: number,
|
||||
|
||||
/**
|
||||
* The height of the thumbnail.
|
||||
*/
|
||||
_thumbnailHeight: number,
|
||||
|
||||
/**
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
_thumbnailWidth: number,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should have a background color.
|
||||
*/
|
||||
_verticalViewBackground: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the vertical filmstrip should be displayed as grid.
|
||||
*/
|
||||
_verticalViewGrid: boolean,
|
||||
|
||||
/**
|
||||
* Additional CSS class names to add to the container of all the thumbnails.
|
||||
*/
|
||||
_videosClassName: string,
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip videos should currently be displayed.
|
||||
*/
|
||||
_visible: boolean
|
||||
};
|
||||
|
||||
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW && (
|
||||
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
|
||||
<Filmstrip
|
||||
{ ...props }
|
||||
_currentLayout = { LAYOUTS.TILE_VIEW }
|
||||
_stageFilmstrip = { true } />
|
||||
</span>
|
||||
);
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { visible } = state['features/filmstrip'];
|
||||
const activeParticipants = getActiveParticipantsIds(state);
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const {
|
||||
gridDimensions: dimensions = {},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
thumbnailSize
|
||||
} = state['features/filmstrip'].stageFilmstripDimensions;
|
||||
const gridDimensions = dimensions;
|
||||
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const availableSpace = clientHeight - filmstripHeight;
|
||||
let filmstripPadding = 0;
|
||||
|
||||
if (availableSpace > 0) {
|
||||
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
|
||||
|
||||
if (paddingValue > 0) {
|
||||
filmstripPadding = paddingValue;
|
||||
}
|
||||
} else {
|
||||
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
|
||||
}
|
||||
|
||||
const collapseTileView = reduceHeight
|
||||
&& isMobileBrowser()
|
||||
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
|
||||
|
||||
const remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
|
||||
|
||||
return {
|
||||
_columns: gridDimensions.columns,
|
||||
_currentLayout: getCurrentLayout(state),
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: filmstripWidth,
|
||||
_remoteParticipantsLength: activeParticipants.length,
|
||||
_remoteParticipants: activeParticipants,
|
||||
_resizableFilmstrip: false,
|
||||
_rows: gridDimensions.rows,
|
||||
_thumbnailWidth: thumbnailSize?.width,
|
||||
_thumbnailHeight: thumbnailSize?.height,
|
||||
_visible: visible,
|
||||
_verticalViewGrid: false,
|
||||
_verticalViewBackground: false
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(StageFilmstrip);
|
|
@ -7,6 +7,7 @@ import React, { Component } from 'react';
|
|||
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import {
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
getLocalAudioTrack,
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
getFakeScreenshareParticipantTrack,
|
||||
updateLastTrackVideoMediaEvent
|
||||
} from '../../../base/tracks';
|
||||
import { getVideoObjectPosition } from '../../../face-centering/functions';
|
||||
|
@ -28,6 +30,7 @@ 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 {
|
||||
DISPLAY_MODE_TO_CLASS_NAME,
|
||||
DISPLAY_VIDEO,
|
||||
|
@ -36,11 +39,14 @@ import {
|
|||
} from '../../constants';
|
||||
import {
|
||||
computeDisplayModeFromInput,
|
||||
getActiveParticipantsIds,
|
||||
getDisplayModeInput,
|
||||
isVideoPlayable,
|
||||
showGridInVerticalView
|
||||
} from '../../functions';
|
||||
import { isStageFilmstripEnabled } from '../../functions.web';
|
||||
|
||||
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
|
||||
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
|
||||
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
|
||||
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
|
||||
|
@ -108,16 +114,17 @@ export type Props = {|
|
|||
*/
|
||||
_height: number,
|
||||
|
||||
/**
|
||||
* Whether or not the participant is displayed on the stage filmstrip.
|
||||
* Used to hide the video from the vertical filmstrip.
|
||||
*/
|
||||
_isActiveParticipant: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the thumbnail should be hidden or not.
|
||||
*/
|
||||
_isHidden: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not there is a pinned participant.
|
||||
*/
|
||||
_isAnyParticipantPinned: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether audio only mode is enabled.
|
||||
*/
|
||||
|
@ -128,6 +135,12 @@ export type Props = {|
|
|||
*/
|
||||
_isCurrentlyOnLargeVideo: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is a fake screen share participant. This prop is behind the
|
||||
* sourceNameSignaling feature flag.
|
||||
*/
|
||||
_isFakeScreenShareParticipant: boolean,
|
||||
|
||||
/**
|
||||
* Whether we are currently running in a mobile browser.
|
||||
*/
|
||||
|
@ -173,6 +186,11 @@ export type Props = {|
|
|||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the stage filmstrip is disabled.
|
||||
*/
|
||||
_stageFilmstripDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The video object position for the participant.
|
||||
*/
|
||||
|
@ -208,6 +226,11 @@ export type Props = {|
|
|||
*/
|
||||
participantID: ?string,
|
||||
|
||||
/**
|
||||
* Whether the tile is displayed in the stage filmstrip or not.
|
||||
*/
|
||||
stageFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* Styles that will be set to the Thumbnail's main span element.
|
||||
*/
|
||||
|
@ -498,6 +521,13 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_hidePopover() {
|
||||
const { _currentLayout } = this.props;
|
||||
|
||||
if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
this.setState({
|
||||
isHovered: false
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
popoverVisible: false
|
||||
});
|
||||
|
@ -514,6 +544,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
_currentLayout,
|
||||
_disableTileEnlargement,
|
||||
_height,
|
||||
_isFakeScreenShareParticipant,
|
||||
_isHidden,
|
||||
_isScreenSharing,
|
||||
_participant,
|
||||
|
@ -551,7 +582,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
|| _disableTileEnlargement
|
||||
|| _isScreenSharing;
|
||||
|
||||
if (canPlayEventReceived || _participant.local) {
|
||||
if (canPlayEventReceived || _participant.local || _isFakeScreenShareParticipant) {
|
||||
videoStyles = {
|
||||
objectFit: doNotStretchVideo ? 'contain' : 'cover'
|
||||
};
|
||||
|
@ -596,10 +627,14 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _participant, dispatch } = this.props;
|
||||
const { _participant, dispatch, _stageFilmstripDisabled } = this.props;
|
||||
const { id, pinned } = _participant;
|
||||
|
||||
dispatch(pinParticipant(pinned ? null : id));
|
||||
if (_stageFilmstripDisabled) {
|
||||
dispatch(pinParticipant(pinned ? null : id));
|
||||
} else {
|
||||
dispatch(addStageParticipant(id, true));
|
||||
}
|
||||
}
|
||||
|
||||
_onMouseEnter: () => void;
|
||||
|
@ -747,7 +782,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
_isDominantSpeakerDisabled,
|
||||
_participant,
|
||||
_currentLayout,
|
||||
_isAnyParticipantPinned,
|
||||
_raisedHand,
|
||||
classes
|
||||
} = this.props;
|
||||
|
@ -758,17 +792,12 @@ class Thumbnail extends Component<Props, State> {
|
|||
className += ` ${classes.raisedHand}`;
|
||||
}
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
|
||||
className += ` ${classes.activeSpeaker} dominant-speaker`;
|
||||
}
|
||||
} else if (_isAnyParticipantPinned) {
|
||||
if (_participant?.pinned) {
|
||||
className += ` videoContainerFocused ${classes.activeSpeaker}`;
|
||||
}
|
||||
} else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
|
||||
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
|
||||
className += ` ${classes.activeSpeaker} dominant-speaker`;
|
||||
}
|
||||
if (_currentLayout !== LAYOUTS.TILE_VIEW && _participant?.pinned) {
|
||||
className += ' videoContainerFocused';
|
||||
}
|
||||
|
||||
return className;
|
||||
}
|
||||
|
@ -876,8 +905,9 @@ class Thumbnail extends Component<Props, State> {
|
|||
_localFlipX,
|
||||
_participant,
|
||||
_videoTrack,
|
||||
_gifSrc,
|
||||
classes,
|
||||
_gifSrc
|
||||
stageFilmstrip
|
||||
} = this.props;
|
||||
const { id } = _participant || {};
|
||||
const { isHovered, popoverVisible } = this.state;
|
||||
|
@ -914,7 +944,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
return (
|
||||
<span
|
||||
className = { containerClassName }
|
||||
id = { local ? 'localVideoContainer' : `participant_${id}` }
|
||||
id = { local
|
||||
? `localVideoContainer${stageFilmstrip ? '_stage' : ''}`
|
||||
: `participant_${id}${stageFilmstrip ? '_stage' : ''}`
|
||||
}
|
||||
{ ...(_isMobile
|
||||
? {
|
||||
onTouchEnd: this._onTouchEnd,
|
||||
|
@ -988,7 +1021,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _participant } = this.props;
|
||||
const { _participant, _isFakeScreenShareParticipant } = this.props;
|
||||
|
||||
if (!_participant) {
|
||||
return null;
|
||||
|
@ -1004,6 +1037,29 @@ class Thumbnail extends Component<Props, State> {
|
|||
return this._renderFakeParticipant();
|
||||
}
|
||||
|
||||
if (_isFakeScreenShareParticipant) {
|
||||
const { isHovered } = this.state;
|
||||
const { _videoTrack, _isMobile, classes } = this.props;
|
||||
|
||||
return (
|
||||
<FakeScreenShareParticipant
|
||||
classes = { classes }
|
||||
containerClassName = { this._getContainerClassName() }
|
||||
isHovered = { isHovered }
|
||||
isMobile = { _isMobile }
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
onMouseMove = { this._onMouseMove }
|
||||
onTouchEnd = { this._onTouchEnd }
|
||||
onTouchMove = { this._onTouchMove }
|
||||
onTouchStart = { this._onTouchStart }
|
||||
participantId = { _participant.id }
|
||||
styles = { this._getStyles() }
|
||||
videoTrack = { _videoTrack } />
|
||||
);
|
||||
}
|
||||
|
||||
return this._renderParticipant();
|
||||
}
|
||||
}
|
||||
|
@ -1017,17 +1073,25 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
const { participantID, stageFilmstrip } = ownProps;
|
||||
|
||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
||||
const id = participant?.id;
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const _videoTrack = isLocal
|
||||
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
|
||||
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
|
||||
|
||||
let _videoTrack;
|
||||
|
||||
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
|
||||
_videoTrack = getFakeScreenshareParticipantTrack(tracks, id);
|
||||
} else {
|
||||
_videoTrack = isLocal
|
||||
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
|
||||
}
|
||||
const _audioTrack = isLocal
|
||||
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state);
|
||||
let size = {};
|
||||
let _isMobilePortrait = false;
|
||||
const {
|
||||
|
@ -1039,6 +1103,7 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
} = state['features/base/config'];
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const _isMobile = isMobileBrowser();
|
||||
const activeParticipants = getActiveParticipantsIds(state);
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
|
@ -1079,12 +1144,26 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
break;
|
||||
}
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
||||
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
|
||||
const {
|
||||
stageFilmstripDimensions = {
|
||||
thumbnailSize: {}
|
||||
}
|
||||
} = state['features/filmstrip'];
|
||||
|
||||
size = {
|
||||
_width: width,
|
||||
_height: height
|
||||
_width: thumbnailSize?.width,
|
||||
_height: thumbnailSize?.height
|
||||
};
|
||||
|
||||
if (stageFilmstrip) {
|
||||
const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
|
||||
|
||||
size = {
|
||||
_width,
|
||||
_height
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1098,10 +1177,12 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
_defaultLocalDisplayName: defaultLocalDisplayName,
|
||||
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
|
||||
_disableTileEnlargement: Boolean(disableTileEnlargement),
|
||||
_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,
|
||||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
|
||||
_isMobile,
|
||||
_isMobilePortrait,
|
||||
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||
|
@ -1110,6 +1191,7 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
_localFlipX: Boolean(localFlipX),
|
||||
_participant: participant,
|
||||
_raisedHand: hasRaisedHand(participant),
|
||||
_stageFilmstripDisabled: !isStageFilmstripEnabled(state),
|
||||
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
|
||||
_videoTrack,
|
||||
...size,
|
||||
|
|
|
@ -32,7 +32,12 @@ type Props = {
|
|||
/**
|
||||
* Id of the participant for which the component is displayed.
|
||||
*/
|
||||
participantId: string
|
||||
participantId: string,
|
||||
|
||||
/**
|
||||
* Whether or not to show the status indicators.
|
||||
*/
|
||||
showStatusIndicators: string
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => {
|
||||
|
@ -58,7 +63,8 @@ const ThumbnailBottomIndicators = ({
|
|||
className,
|
||||
currentLayout,
|
||||
local,
|
||||
participantId
|
||||
participantId,
|
||||
showStatusIndicators = true
|
||||
}: Props) => {
|
||||
const styles = useStyles();
|
||||
const _allowEditing = !useSelector(isNameReadOnly);
|
||||
|
@ -66,11 +72,13 @@ const ThumbnailBottomIndicators = ({
|
|||
const _showDisplayName = useSelector(isDisplayNameVisible);
|
||||
|
||||
return (<div className = { className }>
|
||||
<StatusIndicators
|
||||
audio = { true }
|
||||
moderator = { true }
|
||||
participantID = { participantId }
|
||||
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
|
||||
{
|
||||
showStatusIndicators && <StatusIndicators
|
||||
audio = { true }
|
||||
moderator = { true }
|
||||
participantID = { participantId }
|
||||
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
|
||||
}
|
||||
{
|
||||
_showDisplayName && (
|
||||
<span className = { styles.nameContainer }>
|
||||
|
|
|
@ -5,12 +5,14 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
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 { getIndicatorsTooltipPosition } from '../../functions.web';
|
||||
|
||||
import PinnedIndicator from './PinnedIndicator';
|
||||
import RaisedHandIndicator from './RaisedHandIndicator';
|
||||
import StatusIndicators from './StatusIndicators';
|
||||
import VideoMenuTriggerButton from './VideoMenuTriggerButton';
|
||||
|
@ -39,6 +41,11 @@ type Props = {
|
|||
*/
|
||||
isHovered: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the thumbnail is a fake screen share participant.
|
||||
*/
|
||||
isFakeScreenShareParticipant: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the indicators are for the local participant.
|
||||
*/
|
||||
|
@ -76,6 +83,7 @@ const ThumbnailTopIndicators = ({
|
|||
currentLayout,
|
||||
hidePopover,
|
||||
indicatorsClassName,
|
||||
isFakeScreenShareParticipant,
|
||||
isHovered,
|
||||
local,
|
||||
participantId,
|
||||
|
@ -91,12 +99,31 @@ const ThumbnailTopIndicators = ({
|
|||
useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
|
||||
const _connectionIndicatorDisabled = _isMobile
|
||||
|| Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled));
|
||||
|
||||
const sourceNameSignalingEnabled = useSelector(getSourceNameSignalingFeatureFlag);
|
||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||
|
||||
if (sourceNameSignalingEnabled && isFakeScreenShareParticipant) {
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
{!_connectionIndicatorDisabled
|
||||
&& <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
enableStatsDisplay = { true }
|
||||
iconSize = { _indicatorIconSize }
|
||||
participantId = { participantId }
|
||||
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = { styles.container }>
|
||||
<PinnedIndicator
|
||||
iconSize = { _indicatorIconSize }
|
||||
participantId = { participantId }
|
||||
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
|
||||
{!_connectionIndicatorDisabled
|
||||
&& <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
|
@ -119,6 +146,7 @@ const ThumbnailTopIndicators = ({
|
|||
</div>
|
||||
<div className = { styles.container }>
|
||||
<VideoMenuTriggerButton
|
||||
currentLayout = { currentLayout }
|
||||
hidePopover = { hidePopover }
|
||||
local = { local }
|
||||
participantId = { participantId }
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
import React, { Component } from 'react';
|
||||
import { shouldComponentUpdate } from 'react-window';
|
||||
|
||||
import { getPinnedParticipant } from '../../../base/participants';
|
||||
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { shouldHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { showGridInVerticalView } from '../../functions';
|
||||
import { showGridInVerticalView, getActiveParticipantsIds } from '../../functions';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
|
||||
|
@ -26,14 +27,19 @@ type Props = {
|
|||
_horizontalOffset: number,
|
||||
|
||||
/**
|
||||
* Whether or not there is a pinned participant.
|
||||
* The ID of the participant associated with the Thumbnail.
|
||||
*/
|
||||
_isAnyParticipantPinned: boolean,
|
||||
_participantID: ?string,
|
||||
|
||||
/**
|
||||
* Whether or not the thumbnail is a local screen share.
|
||||
*/
|
||||
_isLocalScreenShare: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant associated with the Thumbnail.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
_stageFilmstrip: boolean,
|
||||
|
||||
/**
|
||||
* The index of the column in tile view.
|
||||
|
@ -82,7 +88,14 @@ class ThumbnailWrapper extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props;
|
||||
const {
|
||||
_disableSelfView,
|
||||
_horizontalOffset = 0,
|
||||
_isLocalScreenShare = false,
|
||||
_participantID,
|
||||
_stageFilmstrip,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
if (typeof _participantID !== 'string') {
|
||||
return null;
|
||||
|
@ -91,18 +104,28 @@ class ThumbnailWrapper extends Component<Props> {
|
|||
if (_participantID === 'local') {
|
||||
return _disableSelfView ? null : (
|
||||
<Thumbnail
|
||||
_isAnyParticipantPinned = { _isAnyParticipantPinned }
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = 'local'
|
||||
stageFilmstrip = { _stageFilmstrip }
|
||||
style = { style } />);
|
||||
}
|
||||
|
||||
if (_isLocalScreenShare) {
|
||||
return _disableSelfView ? null : (
|
||||
<Thumbnail
|
||||
_isAnyParticipantPinned = { _isAnyParticipantPinned }
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = 'localScreenShare'
|
||||
participantID = { _participantID }
|
||||
style = { style } />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Thumbnail
|
||||
_isAnyParticipantPinned = { _isAnyParticipantPinned }
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = { `remote_${_participantID}` }
|
||||
participantID = { _participantID }
|
||||
stageFilmstrip = { _stageFilmstrip }
|
||||
style = { style } />);
|
||||
}
|
||||
}
|
||||
|
@ -117,47 +140,103 @@ class ThumbnailWrapper extends Component<Props> {
|
|||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const remoteParticipantsLength = remoteParticipants.length;
|
||||
const { remoteParticipants: remote } = state['features/filmstrip'];
|
||||
const activeParticipants = getActiveParticipantsIds(state);
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
|
||||
const _verticalViewGrid = showGridInVerticalView(state);
|
||||
const _isAnyParticipantPinned = Boolean(getPinnedParticipant(state));
|
||||
const stageFilmstrip = ownProps.data?.stageFilmstrip;
|
||||
const remoteParticipants = stageFilmstrip ? activeParticipants : remote;
|
||||
const remoteParticipantsLength = remoteParticipants.length;
|
||||
const localId = getLocalParticipant(state).id;
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || stageFilmstrip) {
|
||||
const { columnIndex, rowIndex } = ownProps;
|
||||
const { gridDimensions: dimensions = {}, thumbnailSize: size } = state['features/filmstrip'].tileViewDimensions;
|
||||
const { gridView } = state['features/filmstrip'].verticalViewDimensions;
|
||||
const gridDimensions = _verticalViewGrid ? gridView.gridDimensions : dimensions;
|
||||
const thumbnailSize = _verticalViewGrid ? gridView.thumbnailSize : size;
|
||||
const { tileViewDimensions, stageFilmstripDimensions, verticalViewDimensions } = state['features/filmstrip'];
|
||||
const { gridView } = verticalViewDimensions;
|
||||
let gridDimensions = tileViewDimensions.gridDimensions,
|
||||
thumbnailSize = tileViewDimensions.thumbnailSize;
|
||||
|
||||
if (stageFilmstrip) {
|
||||
gridDimensions = stageFilmstripDimensions.gridDimensions;
|
||||
thumbnailSize = stageFilmstripDimensions.thumbnailSize;
|
||||
} else if (_verticalViewGrid) {
|
||||
gridDimensions = gridView.gridDimensions;
|
||||
thumbnailSize = gridView.thumbnailSize;
|
||||
}
|
||||
const { columns, rows } = gridDimensions;
|
||||
const index = (rowIndex * columns) + columnIndex;
|
||||
let horizontalOffset;
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const localParticipantsLength = localScreenShare ? 2 : 1;
|
||||
let participantsLength;
|
||||
|
||||
if (sourceNameSignalingEnabled) {
|
||||
participantsLength = remoteParticipantsLength
|
||||
|
||||
// Add local camera and screen share to total participant count when self view is not disabled.
|
||||
+ (disableSelfView ? 0 : localParticipantsLength)
|
||||
|
||||
// Removes iAmRecorder from the total participants count.
|
||||
- (iAmRecorder ? 1 : 0);
|
||||
} else {
|
||||
participantsLength = stageFilmstrip ? remoteParticipantsLength
|
||||
: remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
|
||||
}
|
||||
|
||||
if (rowIndex === rows - 1) { // center the last row
|
||||
const { width: thumbnailWidth } = thumbnailSize;
|
||||
const partialLastRowParticipantsNumber = participantsLenght % columns;
|
||||
const partialLastRowParticipantsNumber = participantsLength % columns;
|
||||
|
||||
if (partialLastRowParticipantsNumber > 0) {
|
||||
horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (index > participantsLenght - 1) {
|
||||
if (index > participantsLength - 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (stageFilmstrip) {
|
||||
return {
|
||||
_disableSelfView: disableSelfView,
|
||||
_participantID: remoteParticipants[index] === localId ? 'local' : remoteParticipants[index],
|
||||
_horizontalOffset: horizontalOffset,
|
||||
_stageFilmstrip: stageFilmstrip
|
||||
};
|
||||
}
|
||||
|
||||
// When the thumbnails are reordered, local participant is inserted at index 0.
|
||||
const localIndex = enableThumbnailReordering && !disableSelfView ? 0 : remoteParticipantsLength;
|
||||
const remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
|
||||
|
||||
// Local screen share is inserted at index 1 after the local camera.
|
||||
const localScreenShareIndex = enableThumbnailReordering && !disableSelfView ? 1 : remoteParticipantsLength;
|
||||
|
||||
let remoteIndex;
|
||||
|
||||
if (sourceNameSignalingEnabled) {
|
||||
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView
|
||||
? index - localParticipantsLength : index;
|
||||
} else {
|
||||
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
|
||||
}
|
||||
|
||||
if (!iAmRecorder && index === localIndex) {
|
||||
return {
|
||||
_disableSelfView: disableSelfView,
|
||||
_participantID: 'local',
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
if (sourceNameSignalingEnabled && !iAmRecorder && localScreenShare && index === localScreenShareIndex) {
|
||||
return {
|
||||
_disableSelfView: disableSelfView,
|
||||
_isLocalScreenShare: true,
|
||||
_participantID: localScreenShare?.id,
|
||||
_horizontalOffset: horizontalOffset,
|
||||
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
|
||||
};
|
||||
|
@ -165,8 +244,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
|
||||
return {
|
||||
_participantID: remoteParticipants[remoteIndex],
|
||||
_horizontalOffset: horizontalOffset,
|
||||
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -177,8 +255,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
}
|
||||
|
||||
return {
|
||||
_participantID: remoteParticipants[index],
|
||||
_isAnyParticipantPinned
|
||||
_participantID: remoteParticipants[index]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,11 @@ import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../..
|
|||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
currentLayout: string,
|
||||
|
||||
/**
|
||||
* Hide popover callback.
|
||||
*/
|
||||
|
@ -39,6 +44,7 @@ type Props = {
|
|||
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
const VideoMenuTriggerButton = ({
|
||||
currentLayout,
|
||||
hidePopover,
|
||||
local,
|
||||
participantId,
|
||||
|
@ -50,6 +56,7 @@ const VideoMenuTriggerButton = ({
|
|||
<span id = 'localvideomenu'>
|
||||
<LocalVideoMenuTriggerButton
|
||||
buttonVisible = { visible }
|
||||
currentLayout = { currentLayout }
|
||||
hidePopover = { hidePopover }
|
||||
popoverVisible = { popoverVisible }
|
||||
showPopover = { showPopover } />
|
||||
|
@ -59,6 +66,7 @@ const VideoMenuTriggerButton = ({
|
|||
<span id = 'remotevideomenu'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
buttonVisible = { visible }
|
||||
currentLayout = { currentLayout }
|
||||
hidePopover = { hidePopover }
|
||||
participantID = { participantId }
|
||||
popoverVisible = { popoverVisible }
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
export { default as AudioMutedIndicator } from './AudioMutedIndicator';
|
||||
export { default as Filmstrip } from './Filmstrip';
|
||||
export { default as MainFilmstrip } from './MainFilmstrip';
|
||||
export { default as ModeratorIndicator } from './ModeratorIndicator';
|
||||
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
|
||||
export { default as StageFilmstrip } from './StageFilmstrip';
|
||||
export { default as StatusIndicators } from './StatusIndicators';
|
||||
export { default as Thumbnail } from './Thumbnail';
|
||||
|
|
|
@ -284,3 +284,13 @@ export const MIN_STAGE_VIEW_WIDTH = 800;
|
|||
*/
|
||||
export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
|
||||
+ SCROLL_SIZE + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
|
||||
|
||||
/**
|
||||
* The time after which a participant should be removed from active participants.
|
||||
*/
|
||||
export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
|
||||
|
||||
/**
|
||||
* The max number of participants to be displayed on the stage filmstrip.
|
||||
*/
|
||||
export const MAX_ACTIVE_PARTICIPANTS = 4;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { getSourceNameSignalingFeatureFlag } from '../base/config';
|
||||
import { getFakeScreenShareParticipantOwnerId } from '../base/participants';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isReorderingEnabled } from './functions';
|
||||
|
||||
|
@ -15,7 +18,9 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
|
|||
const state = store.getState();
|
||||
let reorderedParticipants = [];
|
||||
|
||||
if (!isReorderingEnabled(state)) {
|
||||
const { sortedRemoteFakeScreenShareParticipants } = state['features/base/participants'];
|
||||
|
||||
if (!isReorderingEnabled(state) && !sortedRemoteFakeScreenShareParticipants.size) {
|
||||
if (participantId) {
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
|
@ -34,13 +39,28 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
|
|||
} = state['features/base/participants'];
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const screenShares = new Map(sortedRemoteScreenshares);
|
||||
const screenShareParticipants = sortedRemoteFakeScreenShareParticipants
|
||||
? [ ...sortedRemoteFakeScreenShareParticipants.keys() ] : [];
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
const speakers = new Map(speakersList);
|
||||
|
||||
for (const screenshare of screenShares.keys()) {
|
||||
remoteParticipants.delete(screenshare);
|
||||
speakers.delete(screenshare);
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
for (const screenshare of screenShareParticipants) {
|
||||
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
|
||||
|
||||
remoteParticipants.delete(ownerId);
|
||||
remoteParticipants.delete(screenshare);
|
||||
|
||||
speakers.delete(ownerId);
|
||||
speakers.delete(screenshare);
|
||||
}
|
||||
} else {
|
||||
for (const screenshare of screenShares.keys()) {
|
||||
remoteParticipants.delete(screenshare);
|
||||
speakers.delete(screenshare);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
speakers.delete(sharedVideo);
|
||||
|
@ -49,13 +69,32 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
|
|||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
|
||||
// Always update the order of the thumnails.
|
||||
reorderedParticipants = [
|
||||
...Array.from(screenShares.keys()),
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
if (getSourceNameSignalingFeatureFlag(state)) {
|
||||
// Always update the order of the thumnails.
|
||||
const participantsWithScreenShare = screenShareParticipants.reduce((acc, screenshare) => {
|
||||
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
|
||||
|
||||
acc.push(ownerId);
|
||||
acc.push(screenshare);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
reorderedParticipants = [
|
||||
...participantsWithScreenShare,
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
} else {
|
||||
// Always update the order of the thumnails.
|
||||
reorderedParticipants = [
|
||||
...Array.from(screenShares.keys()),
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
}
|
||||
|
||||
store.dispatch(setRemoteParticipants(reorderedParticipants));
|
||||
}
|
||||
|
|
|
@ -102,3 +102,12 @@ export function isReorderingEnabled(state) {
|
|||
return enableThumbnailReordering;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the stage filmstrip is disabled or not.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isStageFilmstripEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
isRemoteTrackMuted
|
||||
} from '../base/tracks/functions';
|
||||
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
|
||||
import { isSharingStatus } from '../shared-video/functions';
|
||||
import {
|
||||
getCurrentLayout,
|
||||
getNotResponsiveTileViewGridDimensions,
|
||||
|
@ -228,9 +229,11 @@ export function getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnl
|
|||
export function getNumberOfPartipantsForTileView(state) {
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const localParticipantsCount = getSourceNameSignalingFeatureFlag(state) && localScreenShare ? 2 : 1;
|
||||
const numberOfParticipants = getParticipantCountWithFake(state)
|
||||
- (iAmRecorder ? 1 : 0)
|
||||
- (disableSelfView ? 1 : 0);
|
||||
- (disableSelfView ? localParticipantsCount : 0);
|
||||
|
||||
return numberOfParticipants;
|
||||
}
|
||||
|
@ -240,16 +243,18 @@ 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 calculateNotResponsiveTileViewDimensions(state) {
|
||||
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { disableTileEnlargement } = state['features/base/config'];
|
||||
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
|
||||
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
|
||||
const filmstripWidth = getVerticalViewMaxWidth(state);
|
||||
const size = calculateThumbnailSizeForTileView({
|
||||
columns: c,
|
||||
minVisibleRows,
|
||||
clientWidth,
|
||||
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
|
||||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
disableResponsiveTiles: true
|
||||
|
@ -289,7 +294,7 @@ export function calculateResponsiveTileViewDimensions({
|
|||
clientWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement = false,
|
||||
isVerticalFilmstrip = false,
|
||||
noHorizontalContainerMargin = false,
|
||||
maxColumns,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
|
||||
|
@ -320,7 +325,7 @@ export function calculateResponsiveTileViewDimensions({
|
|||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
disableResponsiveTiles: false,
|
||||
isVerticalFilmstrip
|
||||
noHorizontalContainerMargin
|
||||
});
|
||||
|
||||
if (size) {
|
||||
|
@ -389,12 +394,12 @@ export function calculateThumbnailSizeForTileView({
|
|||
clientHeight,
|
||||
disableResponsiveTiles = false,
|
||||
disableTileEnlargement = false,
|
||||
isVerticalFilmstrip = false
|
||||
noHorizontalContainerMargin = false
|
||||
}: Object) {
|
||||
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
|
||||
const minHeight = getThumbnailMinHeight(clientWidth);
|
||||
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
|
||||
- (isVerticalFilmstrip ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
|
||||
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
|
||||
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
|
||||
const initialWidth = viewWidth / columns;
|
||||
let initialHeight = viewHeight / minVisibleRows;
|
||||
|
@ -486,8 +491,10 @@ export function getVerticalFilmstripVisibleAreaWidth() {
|
|||
*/
|
||||
export function computeDisplayModeFromInput(input: Object) {
|
||||
const {
|
||||
isActiveParticipant,
|
||||
isAudioOnly,
|
||||
isCurrentlyOnLargeVideo,
|
||||
isFakeScreenShareParticipant,
|
||||
isScreenSharing,
|
||||
canPlayEventReceived,
|
||||
isRemoteParticipant,
|
||||
|
@ -495,7 +502,11 @@ export function computeDisplayModeFromInput(input: Object) {
|
|||
} = input;
|
||||
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
|
||||
|
||||
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
|
||||
if (isFakeScreenShareParticipant) {
|
||||
return DISPLAY_VIDEO;
|
||||
}
|
||||
|
||||
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant) || isActiveParticipant)) {
|
||||
return DISPLAY_AVATAR;
|
||||
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
|
@ -519,8 +530,10 @@ export function computeDisplayModeFromInput(input: Object) {
|
|||
export function getDisplayModeInput(props: Object, state: Object) {
|
||||
const {
|
||||
_currentLayout,
|
||||
_isActiveParticipant,
|
||||
_isAudioOnly,
|
||||
_isCurrentlyOnLargeVideo,
|
||||
_isFakeScreenShareParticipant,
|
||||
_isScreenSharing,
|
||||
_isVideoPlayable,
|
||||
_participant,
|
||||
|
@ -530,6 +543,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
|
|||
const { canPlayEventReceived } = state;
|
||||
|
||||
return {
|
||||
isActiveParticipant: _isActiveParticipant,
|
||||
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
|
||||
isAudioOnly: _isAudioOnly,
|
||||
tileViewActive,
|
||||
|
@ -539,6 +553,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
|
|||
videoStream: Boolean(_videoTrack),
|
||||
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
|
||||
isScreenSharing: _isScreenSharing,
|
||||
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
|
||||
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
|
||||
};
|
||||
}
|
||||
|
@ -613,7 +628,7 @@ export function isReorderingEnabled(state) {
|
|||
const { testing = {} } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
|
||||
return enableThumbnailReordering && isFilmstripScollVisible(state);
|
||||
return enableThumbnailReordering && isFilmstripScrollVisible(state);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -622,7 +637,7 @@ export function isReorderingEnabled(state) {
|
|||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - True if the scroll is displayed and false otherwise.
|
||||
*/
|
||||
export function isFilmstripScollVisible(state) {
|
||||
export function isFilmstripScrollVisible(state) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
let hasScroll = false;
|
||||
|
||||
|
@ -642,3 +657,43 @@ export function isFilmstripScollVisible(state) {
|
|||
|
||||
return hasScroll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ids of the active participants.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveParticipantsIds(state) {
|
||||
const { activeParticipants } = state['features/filmstrip'];
|
||||
|
||||
return activeParticipants.map(p => p.participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not the stage filmstrip should be displayed.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldDisplayStageFilmstrip(state) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the stage filmstrip is disabled or not.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isStageFilmstripEnabled(state) {
|
||||
const { filmstrip } = state['features/base/config'];
|
||||
|
||||
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,52 @@
|
|||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
getDominantSpeakerParticipant,
|
||||
getLocalParticipant,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
||||
import { SETTINGS_UPDATED } from '../base/settings';
|
||||
import {
|
||||
getCurrentLayout,
|
||||
LAYOUTS
|
||||
LAYOUTS,
|
||||
setTileView
|
||||
} from '../video-layout';
|
||||
|
||||
import { SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
|
||||
import { ADD_STAGE_PARTICIPANT, REMOVE_STAGE_PARTICIPANT, SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
|
||||
import {
|
||||
addStageParticipant,
|
||||
removeStageParticipant,
|
||||
setFilmstripWidth,
|
||||
setHorizontalViewDimensions,
|
||||
setStageParticipants,
|
||||
setTileViewDimensions,
|
||||
setVerticalViewDimensions
|
||||
} from './actions';
|
||||
import { DEFAULT_FILMSTRIP_WIDTH, MIN_STAGE_VIEW_WIDTH } from './constants';
|
||||
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
|
||||
import { isFilmstripResizable } from './functions.web';
|
||||
import {
|
||||
ACTIVE_PARTICIPANT_TIMEOUT,
|
||||
DEFAULT_FILMSTRIP_WIDTH,
|
||||
MAX_ACTIVE_PARTICIPANTS,
|
||||
MIN_STAGE_VIEW_WIDTH
|
||||
} from './constants';
|
||||
import {
|
||||
isFilmstripResizable,
|
||||
updateRemoteParticipants,
|
||||
updateRemoteParticipantsOnLeave
|
||||
} from './functions';
|
||||
import './subscriber';
|
||||
import { getActiveParticipantsIds, isStageFilmstripEnabled } from './functions.web';
|
||||
|
||||
/**
|
||||
* Map of timers.
|
||||
*
|
||||
* @type {Map}
|
||||
*/
|
||||
const timers = new Map();
|
||||
|
||||
/**
|
||||
* The middleware of the feature Filmstrip.
|
||||
|
@ -35,7 +61,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
updateRemoteParticipantsOnLeave(store, action.participant?.id);
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
let result;
|
||||
|
||||
switch (action.type) {
|
||||
case CLIENT_RESIZED: {
|
||||
|
@ -74,6 +100,10 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
if (action.participant?.isLocalScreenShare) {
|
||||
break;
|
||||
}
|
||||
result = next(action);
|
||||
updateRemoteParticipants(store, action.participant?.id);
|
||||
break;
|
||||
}
|
||||
|
@ -82,12 +112,114 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
// TODO: This needs to be removed once the large video is Reactified.
|
||||
VideoLayout.onLocalFlipXChanged();
|
||||
}
|
||||
if (action.settings?.disableSelfView) {
|
||||
const state = store.getState();
|
||||
const local = getLocalParticipant(state);
|
||||
const activeParticipantsIds = getActiveParticipantsIds(state);
|
||||
|
||||
if (activeParticipantsIds.find(id => id === local.id)) {
|
||||
store.dispatch(removeStageParticipant(local.id));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_USER_FILMSTRIP_WIDTH: {
|
||||
VideoLayout.refreshLayout();
|
||||
break;
|
||||
}
|
||||
case ADD_STAGE_PARTICIPANT: {
|
||||
const { dispatch, getState } = store;
|
||||
const { participantId, pinned } = action;
|
||||
const state = getState();
|
||||
const { activeParticipants } = state['features/filmstrip'];
|
||||
let queue;
|
||||
|
||||
if (activeParticipants.find(p => p.participantId === participantId)) {
|
||||
queue = activeParticipants.filter(p => p.participantId !== participantId);
|
||||
queue.push({
|
||||
participantId,
|
||||
pinned
|
||||
});
|
||||
const tid = timers.get(participantId);
|
||||
|
||||
clearTimeout(tid);
|
||||
} else if (activeParticipants.length < MAX_ACTIVE_PARTICIPANTS) {
|
||||
queue = [ ...activeParticipants, {
|
||||
participantId,
|
||||
pinned
|
||||
} ];
|
||||
} else {
|
||||
const notPinnedIndex = activeParticipants.findIndex(p => !p.pinned);
|
||||
|
||||
if (notPinnedIndex === -1) {
|
||||
if (pinned) {
|
||||
queue = [ ...activeParticipants, {
|
||||
participantId,
|
||||
pinned
|
||||
} ];
|
||||
queue.shift();
|
||||
}
|
||||
} else {
|
||||
queue = [ ...activeParticipants, {
|
||||
participantId,
|
||||
pinned
|
||||
} ];
|
||||
queue.splice(notPinnedIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setStageParticipants(queue));
|
||||
if (!pinned) {
|
||||
const timeoutId = setTimeout(() => dispatch(removeStageParticipant(participantId)),
|
||||
ACTIVE_PARTICIPANT_TIMEOUT);
|
||||
|
||||
timers.set(participantId, timeoutId);
|
||||
}
|
||||
if (getCurrentLayout(state) === LAYOUTS.TILE_VIEW) {
|
||||
dispatch(setTileView(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case REMOVE_STAGE_PARTICIPANT: {
|
||||
const state = store.getState();
|
||||
const { participantId } = action;
|
||||
const tid = timers.get(participantId);
|
||||
|
||||
clearTimeout(tid);
|
||||
timers.delete(participantId);
|
||||
const dominant = getDominantSpeakerParticipant(state);
|
||||
|
||||
if (participantId === dominant?.id) {
|
||||
const timeoutId = setTimeout(() => store.dispatch(removeStageParticipant(participantId)),
|
||||
ACTIVE_PARTICIPANT_TIMEOUT);
|
||||
|
||||
timers.set(participantId, timeoutId);
|
||||
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
const { id } = action.participant;
|
||||
const state = store.getState();
|
||||
const stageFilmstrip = isStageFilmstripEnabled(state);
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
|
||||
if (stageFilmstrip && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
store.dispatch(addStageParticipant(id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { id } = action.participant;
|
||||
const activeParticipantsIds = getActiveParticipantsIds(store.getState());
|
||||
|
||||
if (activeParticipantsIds.find(pId => pId === id)) {
|
||||
store.dispatch(removeStageParticipant(id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result ?? next(action);
|
||||
});
|
||||
|
|
|
@ -4,11 +4,14 @@ import { PARTICIPANT_LEFT } from '../base/participants';
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
REMOVE_STAGE_PARTICIPANT,
|
||||
SET_STAGE_PARTICIPANTS,
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_FILMSTRIP_WIDTH,
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_REMOTE_PARTICIPANTS,
|
||||
SET_STAGE_FILMSTRIP_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_USER_FILMSTRIP_WIDTH,
|
||||
SET_USER_IS_RESIZING,
|
||||
|
@ -18,6 +21,12 @@ import {
|
|||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
|
||||
/**
|
||||
* The list of participants to be displayed on the stage filmstrip.
|
||||
*/
|
||||
activeParticipants: [],
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the {@link Filmstrip} is enabled.
|
||||
*
|
||||
|
@ -57,6 +66,14 @@ const DEFAULT_STATE = {
|
|||
*/
|
||||
remoteParticipants: [],
|
||||
|
||||
/**
|
||||
* The stage filmstrip view dimensions.
|
||||
*
|
||||
* @public
|
||||
* @type {Object}
|
||||
*/
|
||||
stageFilmstripDimensions: {},
|
||||
|
||||
/**
|
||||
* The tile view dimensions.
|
||||
*
|
||||
|
@ -223,6 +240,24 @@ ReducerRegistry.register(
|
|||
isResizing: action.resizing
|
||||
};
|
||||
}
|
||||
case SET_STAGE_FILMSTRIP_DIMENSIONS: {
|
||||
return {
|
||||
...state,
|
||||
stageFilmstripDimensions: action.dimensions
|
||||
};
|
||||
}
|
||||
case SET_STAGE_PARTICIPANTS: {
|
||||
return {
|
||||
...state,
|
||||
activeParticipants: action.queue
|
||||
};
|
||||
}
|
||||
case REMOVE_STAGE_PARTICIPANT: {
|
||||
return {
|
||||
...state,
|
||||
activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -6,12 +6,14 @@ import { StateListenerRegistry } from '../base/redux';
|
|||
import { clientResized } from '../base/responsive-ui';
|
||||
import { shouldHideSelfView } from '../base/settings';
|
||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
||||
import { selectParticipantInLargeVideo } from '../large-video/actions.any';
|
||||
import { getParticipantsPaneOpen } from '../participants-pane/functions';
|
||||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||
import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
|
||||
import {
|
||||
setHorizontalViewDimensions,
|
||||
setStageFilmstripViewDimensions,
|
||||
setTileViewDimensions,
|
||||
setVerticalViewDimensions
|
||||
} from './actions';
|
||||
|
@ -19,7 +21,13 @@ import {
|
|||
ASPECT_RATIO_BREAKPOINT,
|
||||
DISPLAY_DRAWER_THRESHOLD
|
||||
} from './constants';
|
||||
import { isFilmstripResizable, isFilmstripScollVisible, updateRemoteParticipants } from './functions';
|
||||
import {
|
||||
isFilmstripResizable,
|
||||
isFilmstripScrollVisible,
|
||||
shouldDisplayStageFilmstrip,
|
||||
updateRemoteParticipants
|
||||
} from './functions';
|
||||
|
||||
import './subscriber.any';
|
||||
|
||||
|
||||
|
@ -30,7 +38,8 @@ StateListenerRegistry.register(
|
|||
/* selector */ state => {
|
||||
return {
|
||||
numberOfParticipants: getParticipantCountWithFake(state),
|
||||
disableSelfView: shouldHideSelfView(state)
|
||||
disableSelfView: shouldHideSelfView(state),
|
||||
localScreenShare: state['features/base/participants'].localScreenShare
|
||||
};
|
||||
},
|
||||
/* listener */ (currentState, store) => {
|
||||
|
@ -145,5 +154,39 @@ StateListenerRegistry.register(
|
|||
* Listens for changes in the filmstrip scroll visibility.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => isFilmstripScollVisible(state),
|
||||
/* selector */ state => isFilmstripScrollVisible(state),
|
||||
/* listener */ (_, store) => updateRemoteParticipants(store));
|
||||
|
||||
/**
|
||||
* Listens for changes to determine the size of the stage filmstrip tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
return {
|
||||
remoteScreenShares: state['features/video-layout'].remoteScreenShares.length,
|
||||
length: state['features/filmstrip'].activeParticipants.length,
|
||||
width: state['features/filmstrip'].width?.current,
|
||||
visible: state['features/filmstrip'].visible,
|
||||
clientWidth: state['features/base/responsive-ui'].clientWidth,
|
||||
tileView: state['features/video-layout'].tileViewEnabled
|
||||
};
|
||||
},
|
||||
/* listener */(_, store) => {
|
||||
if (shouldDisplayStageFilmstrip(store.getState())) {
|
||||
store.dispatch(setStageFilmstripViewDimensions());
|
||||
}
|
||||
}, {
|
||||
deepEquals: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes in the active participants count determine the stage participant (when
|
||||
* there's just one).
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/filmstrip'].activeParticipants.length,
|
||||
/* listener */(length, store) => {
|
||||
if (length <= 1) {
|
||||
store.dispatch(selectParticipantInLargeVideo());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -102,7 +102,14 @@ function _electLastVisibleRemoteVideo(tracks) {
|
|||
* @returns {(string|undefined)}
|
||||
*/
|
||||
function _electParticipantInLargeVideo(state) {
|
||||
// 1. If a participant is pinned, they will be shown in the LargeVideo
|
||||
// 1. If there's a remote screenshare, pick the most recent one that was added to the conference.
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
|
||||
if (remoteScreenShares?.length) {
|
||||
return remoteScreenShares[remoteScreenShares.length - 1];
|
||||
}
|
||||
|
||||
// 2. Next, if a participant is pinned, they will be shown in the LargeVideo
|
||||
// (regardless of whether they are local or remote).
|
||||
let participant = getPinnedParticipant(state);
|
||||
|
||||
|
@ -110,13 +117,6 @@ function _electParticipantInLargeVideo(state) {
|
|||
return participant.id;
|
||||
}
|
||||
|
||||
// 2. Next, pick the most recent remote screenshare that was added to the conference.
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
|
||||
if (remoteScreenShares?.length) {
|
||||
return remoteScreenShares[remoteScreenShares.length - 1];
|
||||
}
|
||||
|
||||
// 3. Next, pick the dominant speaker (other than self).
|
||||
participant = getDominantSpeakerParticipant(state);
|
||||
if (participant && !participant.local) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
|||
import { Watermarks } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { setColorAlpha } from '../../base/util';
|
||||
import { DominantSpeakerName } from '../../display-name';
|
||||
import { StageParticipantNameLabel } from '../../display-name';
|
||||
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable } from '../../filmstrip';
|
||||
import { getVerticalViewMaxWidth } from '../../filmstrip/functions.web';
|
||||
import { SharedVideo } from '../../shared-video/components/web';
|
||||
|
@ -175,7 +175,7 @@ class LargeVideo extends Component<Props> {
|
|||
</div>
|
||||
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|
||||
|| <Captions /> }
|
||||
{_showDominantSpeakerBadge && <DominantSpeakerName />}
|
||||
{_showDominantSpeakerBadge && <StageParticipantNameLabel />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
|
||||
import { type Dispatch } from 'redux';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../app/actions';
|
||||
|
||||
import { hideLobbyScreen, setKnockingState } from './actions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
|
@ -12,8 +12,11 @@ export * from './actions.any';
|
|||
* @returns {Function}
|
||||
*/
|
||||
export function cancelKnocking() {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
dispatch(appNavigate(undefined));
|
||||
return dispatch => {
|
||||
batch(() => {
|
||||
dispatch(setKnockingState(false));
|
||||
dispatch(hideLobbyScreen());
|
||||
dispatch(appNavigate(undefined));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ export function showLobbyChatButton(
|
|||
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
const lobbyLocalId = conference.myLobbyUserId();
|
||||
const lobbyLocalId = conference?.myLobbyUserId();
|
||||
|
||||
if (!enableLobbyChat) {
|
||||
return false;
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* An registry that dispatches hardware back button events for subscribers with a custom logic.
|
||||
*/
|
||||
class BackButtonRegistry {
|
||||
_listeners: Array<Function>;
|
||||
|
||||
/**
|
||||
* Instantiates a new instance of the registry.
|
||||
*/
|
||||
constructor() {
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to the registry.
|
||||
*
|
||||
* NOTE: Due to the different order of component mounts, we allow a component to register
|
||||
* its listener to the top of the list, so then that will be invoked before other, 'non-top'
|
||||
* listeners. For example a 'non-top' listener can be the one that puts the app into PiP mode,
|
||||
* while a 'top' listener is the one that closes a modal in a conference.
|
||||
*
|
||||
* @param {Function} listener - The listener function.
|
||||
* @param {boolean?} top - If true, the listener will be put on the top (eg for modal-like components).
|
||||
* @returns {void}
|
||||
*/
|
||||
addListener(listener: Function, top: boolean = false) {
|
||||
if (top) {
|
||||
this._listeners.splice(0, 0, listener);
|
||||
} else {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the registry.
|
||||
*
|
||||
* @param {Function} listener - The listener to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeListener(listener: Function) {
|
||||
this._listeners = this._listeners.filter(f => f !== listener);
|
||||
}
|
||||
|
||||
onHardwareBackPress: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback for the back button press event.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onHardwareBackPress() {
|
||||
for (const listener of this._listeners) {
|
||||
const result = listener();
|
||||
|
||||
if (result === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default new BackButtonRegistry();
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
|
||||
export { default as BackButtonRegistry } from './BackButtonRegistry';
|
|
@ -1,36 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import { BackHandler } from 'react-native';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
import BackButtonRegistry from './BackButtonRegistry';
|
||||
|
||||
// Binding function to the proper instance, so then the event emitter won't replace the
|
||||
// underlying instance.
|
||||
BackButtonRegistry.onHardwareBackPress = BackButtonRegistry.onHardwareBackPress.bind(BackButtonRegistry);
|
||||
|
||||
/**
|
||||
* Middleware that captures App lifetime actions and subscribes to application
|
||||
* state changes. When the application state changes it will fire the action
|
||||
* required to mute or unmute the local video in case the application goes to
|
||||
* the background or comes back from it.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
MiddlewareRegistry.register(() => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
BackHandler.addEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
BackHandler.removeEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -3,6 +3,7 @@
|
|||
import { FlagGroupContext } from '@atlaskit/flag/flag-group';
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
||||
|
@ -67,10 +68,6 @@ const useStyles = theme => {
|
|||
maxWidth: 'calc(100% - 32px)'
|
||||
},
|
||||
|
||||
containerChatOpen: {
|
||||
left: '331px'
|
||||
},
|
||||
|
||||
transitionGroup: {
|
||||
'& > *': {
|
||||
marginBottom: '20px',
|
||||
|
@ -166,12 +163,9 @@ class NotificationsContainer extends Component<Props> {
|
|||
<AtlasKitThemeProvider mode = 'light'>
|
||||
<FlagGroupContext.Provider value = { this._api }>
|
||||
<div
|
||||
className = { `${this.props.classes.container} ${this.props.portal
|
||||
? this.props.classes.containerPortal
|
||||
: this.props._isChatOpen
|
||||
? this.props.classes.containerChatOpen
|
||||
: ''}`
|
||||
}
|
||||
className = { clsx(this.props.classes.container, {
|
||||
[this.props.classes.containerPortal]: this.props.portal
|
||||
}) }
|
||||
id = 'notifications-container'>
|
||||
<TransitionGroup className = { this.props.classes.transitionGroup }>
|
||||
{this._renderFlags()}
|
||||
|
|
|
@ -71,8 +71,8 @@ export default {
|
|||
},
|
||||
|
||||
transparentButton: {
|
||||
...baseButton,
|
||||
backgroundColor: 'transparent'
|
||||
backgroundColor: 'transparent',
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
leaveButtonLabel: {
|
||||
|
|
|
@ -207,7 +207,7 @@ class MeetingParticipantList extends PureComponent<Props> {
|
|||
// If there are only meeting participants available,
|
||||
// we take the full container height
|
||||
const onlyMeetingParticipants
|
||||
= breakoutRooms.length === 0 && lobbyParticipants.length === 0;
|
||||
= breakoutRooms?.length === 0 && lobbyParticipants.length === 0;
|
||||
const containerStyle
|
||||
= onlyMeetingParticipants
|
||||
? styles.meetingListFullContainer : styles.meetingListContainer;
|
||||
|
|
|
@ -12,7 +12,7 @@ import { connect } from '../../../base/redux';
|
|||
import { getBreakoutRoomsConfig } from '../../../breakout-rooms/functions';
|
||||
import { MuteEveryoneDialog } from '../../../video-menu/components/';
|
||||
import { close } from '../../actions';
|
||||
import { classList, findAncestorByClass, getParticipantsPaneOpen } from '../../functions';
|
||||
import { findAncestorByClass, getParticipantsPaneOpen } from '../../functions';
|
||||
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
|
||||
import { RoomList } from '../breakout-rooms/components/web/RoomList';
|
||||
|
||||
|
@ -214,7 +214,7 @@ class ParticipantsPane extends Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
|
||||
<div className = 'participants_pane'>
|
||||
<div className = 'participants_pane-content'>
|
||||
<div className = { classes.header }>
|
||||
<div
|
||||
|
|
|
@ -22,14 +22,6 @@ import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
|||
|
||||
import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants';
|
||||
|
||||
/**
|
||||
* Generates a class attribute value.
|
||||
*
|
||||
* @param {Iterable<string>} args - String iterable.
|
||||
* @returns {string} Class attribute value.
|
||||
*/
|
||||
export const classList = (...args: Array<string | boolean>) => args.filter(Boolean).join(' ');
|
||||
|
||||
/**
|
||||
* Find the first styled ancestor component of an element.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { getAvailableDevices } from '../../../base/devices';
|
||||
|
@ -39,6 +40,11 @@ declare var interfaceConfig: Object;
|
|||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: Object,
|
||||
|
||||
/**
|
||||
* Which settings tab should be initially displayed. If not defined then
|
||||
* the first tab will be displayed.
|
||||
|
@ -56,6 +62,116 @@ type Props = {
|
|||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = theme => {
|
||||
return {
|
||||
settingsDialog: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
|
||||
'&.profile-pane': {
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
'& .auth-name': {
|
||||
marginBottom: `${theme.spacing(1)}px`
|
||||
},
|
||||
|
||||
'& .calendar-tab, & .device-selection': {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .mock-atlaskit-label': {
|
||||
color: '#b8c7e0',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.33,
|
||||
padding: `20px 0px ${theme.spacing(1)}px 0px`
|
||||
},
|
||||
|
||||
'& input[type="checkbox"]:checked + svg': {
|
||||
'--checkbox-background-color': '#6492e7',
|
||||
'--checkbox-border-color': '#6492e7'
|
||||
},
|
||||
|
||||
'& input[type="checkbox"] + svg + span': {
|
||||
color: '#9FB0CC'
|
||||
},
|
||||
|
||||
[[ '& .calendar-tab',
|
||||
'& .more-tab',
|
||||
'& .box' ]]: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'& .profile-edit': {
|
||||
display: 'flex',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'& .profile-edit-field': {
|
||||
flex: 0.5,
|
||||
marginRight: '20px'
|
||||
},
|
||||
|
||||
'& .settings-sub-pane': {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
'& .settings-sub-pane .right': {
|
||||
flex: 1
|
||||
},
|
||||
'& .settings-sub-pane .left': {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
'& .settings-sub-pane-element': {
|
||||
textAlign: 'left',
|
||||
flex: 1
|
||||
},
|
||||
|
||||
'& .moderator-settings-wrapper': {
|
||||
paddingTop: '20px'
|
||||
},
|
||||
|
||||
'& .calendar-tab': {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
fontSize: '14px',
|
||||
minHeight: '100px',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
'& .calendar-tab-sign-in': {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
'& .sign-out-cta': {
|
||||
marginBottom: '20px'
|
||||
},
|
||||
|
||||
'@media only screen and (max-width: 700px)': {
|
||||
'& .device-selection': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
'& .more-tab': {
|
||||
flexDirection: 'column'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog to modify local settings
|
||||
* and conference-wide (moderator) settings. This version is connected to
|
||||
|
@ -130,12 +246,14 @@ class SettingsDialog extends Component<Props> {
|
|||
* {@code ConnectedSettingsDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* tabs: Array<Object>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { classes } = ownProps;
|
||||
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// The settings sections to display.
|
||||
|
@ -173,7 +291,7 @@ function _mapStateToProps(state) {
|
|||
selectedVideoInputId: tabState.selectedVideoInputId
|
||||
};
|
||||
},
|
||||
styles: 'settings-pane devices-pane',
|
||||
styles: `settings-pane ${classes.settingsDialog} devices-pane`,
|
||||
submit: submitDeviceSelectionTab
|
||||
});
|
||||
}
|
||||
|
@ -184,7 +302,7 @@ function _mapStateToProps(state) {
|
|||
component: ProfileTab,
|
||||
label: 'profile.title',
|
||||
props: getProfileTabProps(state),
|
||||
styles: 'settings-pane profile-pane',
|
||||
styles: `settings-pane ${classes.settingsDialog} profile-pane`,
|
||||
submit: submitProfileTab
|
||||
});
|
||||
}
|
||||
|
@ -206,7 +324,7 @@ function _mapStateToProps(state) {
|
|||
startReactionsMuted: tabState?.startReactionsMuted
|
||||
};
|
||||
},
|
||||
styles: 'settings-pane moderator-pane',
|
||||
styles: `settings-pane ${classes.settingsDialog} moderator-pane`,
|
||||
submit: submitModeratorTab
|
||||
});
|
||||
}
|
||||
|
@ -215,8 +333,8 @@ function _mapStateToProps(state) {
|
|||
tabs.push({
|
||||
name: SETTINGS_TABS.CALENDAR,
|
||||
component: CalendarTab,
|
||||
label: 'settings.calendar.title',
|
||||
styles: 'settings-pane calendar-pane'
|
||||
label: 'settings-pane settings.calendar.title',
|
||||
styles: `${classes.settingsDialog} calendar-pane`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -226,7 +344,7 @@ function _mapStateToProps(state) {
|
|||
component: SoundsTab,
|
||||
label: 'settings.sounds',
|
||||
props: getSoundsTabProps(state),
|
||||
styles: 'settings-pane profile-pane',
|
||||
styles: `settings-pane ${classes.settingsDialog} profile-pane`,
|
||||
submit: submitSoundsTab
|
||||
});
|
||||
}
|
||||
|
@ -249,7 +367,7 @@ function _mapStateToProps(state) {
|
|||
enabledNotifications: tabState?.enabledNotifications
|
||||
};
|
||||
},
|
||||
styles: 'settings-pane more-pane',
|
||||
styles: `settings-pane ${classes.settingsDialog} more-pane`,
|
||||
submit: submitMoreTab
|
||||
});
|
||||
}
|
||||
|
@ -257,4 +375,4 @@ function _mapStateToProps(state) {
|
|||
return { _tabs: tabs };
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(SettingsDialog);
|
||||
export default withStyles(styles)(connect(_mapStateToProps)(SettingsDialog));
|
||||
|
|
|
@ -10,6 +10,16 @@
|
|||
export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
|
||||
= 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of the action which sets the list of known remote fake screen share participant IDs.
|
||||
*
|
||||
* @returns {{
|
||||
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
* participantIds: Array<string>
|
||||
* }}
|
||||
*/
|
||||
export const FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED = 'FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of the action which enables or disables the feature for showing
|
||||
* video thumbnails in a two-axis tile view.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SET_TILE_VIEW
|
||||
} from './actionTypes';
|
||||
|
@ -26,6 +27,22 @@ export function setRemoteParticipantsWithScreenShare(participantIds: Array<strin
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a (redux) action which signals that the list of known remote fake screen share participant ids has changed.
|
||||
*
|
||||
* @param {string} participantIds - The remote fake screen share participants.
|
||||
* @returns {{
|
||||
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
* participantIds: Array<string>
|
||||
* }}
|
||||
*/
|
||||
export function fakeScreenshareParticipantsUpdated(participantIds: Array<string>) {
|
||||
return {
|
||||
type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
participantIds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a (redux) action which signals to set the UI layout to be tiled view
|
||||
* or not.
|
||||
|
|
|
@ -73,13 +73,14 @@ export function getMaxColumnCount() {
|
|||
* which rows will be added but no more columns.
|
||||
*
|
||||
* @param {Object} state - The redux store state.
|
||||
* @param {number} width - Custom width to use for calculation.
|
||||
* @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) {
|
||||
export function getNotResponsiveTileViewGridDimensions(state: Object, stageFilmstrip: boolean = false) {
|
||||
const maxColumns = getMaxColumnCount(state);
|
||||
const numberOfParticipants = getNumberOfPartipantsForTileView(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);
|
||||
|
@ -240,3 +241,15 @@ export function getVideoQualityForLargeVideo() {
|
|||
|
||||
return getVideoQualityForHeight(wrapper.clientHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the video quality level for the thumbnails in the stage filmstrip.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getVideoQualityForStageThumbnails(state) {
|
||||
const height = state['features/filmstrip'].stageFilmstripDimensions?.thumbnailSize?.height;
|
||||
|
||||
return getVideoQualityForHeight(height);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { TRACK_REMOVED } from '../base/tracks';
|
||||
import { SET_DOCUMENT_EDITING_STATUS } from '../etherpad';
|
||||
import { isStageFilmstripEnabled } from '../filmstrip/functions';
|
||||
import { isFollowMeActive } from '../follow-me';
|
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes';
|
||||
|
@ -68,11 +69,15 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
|
||||
// Things to update when tile view state changes
|
||||
case SET_TILE_VIEW:
|
||||
if (action.enabled && getPinnedParticipant(store)) {
|
||||
case SET_TILE_VIEW: {
|
||||
const state = store.getState();
|
||||
const stageFilmstrip = isStageFilmstripEnabled(state);
|
||||
|
||||
if (action.enabled && !stageFilmstrip && getPinnedParticipant(state)) {
|
||||
store.dispatch(pinParticipant(null));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the remoteScreenShares.
|
||||
// Because of the debounce in the subscriber which updates the remoteScreenShares we need to handle
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
|
||||
SET_TILE_VIEW
|
||||
} from './actionTypes';
|
||||
|
@ -27,6 +28,7 @@ const STORE_NAME = 'features/video-layout';
|
|||
|
||||
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
|
||||
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -2,12 +2,45 @@
|
|||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { getSourceNameSignalingFeatureFlag } from '../base/config';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { isFollowMeActive } from '../follow-me';
|
||||
|
||||
import { setRemoteParticipantsWithScreenShare } from './actions';
|
||||
import { setRemoteParticipantsWithScreenShare, fakeScreenshareParticipantsUpdated } from './actions';
|
||||
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].sortedRemoteFakeScreenShareParticipants,
|
||||
/* listener */ (sortedRemoteFakeScreenShareParticipants, store) => {
|
||||
if (!getAutoPinSetting() || isFollowMeActive(store) || !getSourceNameSignalingFeatureFlag(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldScreenSharesOrder = store.getState()['features/video-layout'].remoteScreenShares || [];
|
||||
const knownSharingParticipantIds = [ ...sortedRemoteFakeScreenShareParticipants.keys() ];
|
||||
|
||||
// Filter out any participants which are no longer screen sharing
|
||||
// by looping through the known sharing participants and removing any
|
||||
// participant IDs which are no longer sharing.
|
||||
const newScreenSharesOrder = oldScreenSharesOrder.filter(
|
||||
participantId => knownSharingParticipantIds.includes(participantId));
|
||||
|
||||
// Make sure all new sharing participant get added to the end of the
|
||||
// known screen shares.
|
||||
knownSharingParticipantIds.forEach(participantId => {
|
||||
if (!newScreenSharesOrder.includes(participantId)) {
|
||||
newScreenSharesOrder.push(participantId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) {
|
||||
store.dispatch(fakeScreenshareParticipantsUpdated(newScreenSharesOrder));
|
||||
|
||||
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* For auto-pin mode, listen for changes to the known media tracks and look
|
||||
* for updates to screen shares. The listener is debounced to avoid state
|
||||
|
@ -20,7 +53,7 @@ StateListenerRegistry.register(
|
|||
// possible to have screen sharing participant that has already left in the remoteScreenShares array.
|
||||
// This can lead to rendering a thumbnails for already left participants since the remoteScreenShares
|
||||
// array is used for building the ordered list of remote participants.
|
||||
if (!getAutoPinSetting() || isFollowMeActive(store)) {
|
||||
if (!getAutoPinSetting() || isFollowMeActive(store) || getSourceNameSignalingFeatureFlag(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actio
|
|||
import { getHideSelfView } from '../../../base/settings';
|
||||
import { getLocalVideoTrack } from '../../../base/tracks';
|
||||
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
|
||||
import { LAYOUTS } from '../../../video-layout';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
|
||||
import TogglePinToStageButton from './TogglePinToStageButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -93,6 +95,11 @@ type Props = {
|
|||
*/
|
||||
_showLocalVideoFlipButton: boolean,
|
||||
|
||||
/**
|
||||
* Whether to render the pin to stage button.
|
||||
*/
|
||||
_showPinToStage: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
@ -158,6 +165,7 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
|
|||
_showConnectionInfo,
|
||||
_showHideSelfViewButton,
|
||||
_showLocalVideoFlipButton,
|
||||
_showPinToStage,
|
||||
buttonVisible,
|
||||
classes,
|
||||
hidePopover,
|
||||
|
@ -183,8 +191,15 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
|
|||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
onClick = { hidePopover } />
|
||||
}
|
||||
{
|
||||
_showPinToStage && <TogglePinToStageButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
noIcon = { true }
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{ isMobileBrowser()
|
||||
&& <ConnectionStatusButton participantId = { _localParticipantId } />
|
||||
&& <ConnectionStatusButton participantId = { _localParticipantId } />
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenu>
|
||||
|
@ -254,11 +269,12 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
|
|||
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { currentLayout } = ownProps;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { disableLocalVideoFlip, disableSelfViewSettings } = state['features/base/config'];
|
||||
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
|
@ -288,7 +304,8 @@ function _mapStateToProps(state) {
|
|||
_showHideSelfViewButton: showHideSelfViewButton,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_localParticipantId: localParticipant.id,
|
||||
_showConnectionInfo: showConnectionInfo
|
||||
_showConnectionInfo: showConnectionInfo,
|
||||
_showPinToStage: isStageFilmstripEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participant
|
|||
import { isParticipantAudioMuted } from '../../../base/tracks';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { setVolume } from '../../../filmstrip/actions.web';
|
||||
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
|
||||
import { isForceMuted } from '../../../participants-pane/functions';
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control';
|
||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
||||
|
@ -35,6 +36,7 @@ import {
|
|||
KickButton,
|
||||
PrivateMessageMenuButton,
|
||||
RemoteControlButton,
|
||||
TogglePinToStageButton,
|
||||
VolumeSlider
|
||||
} from './';
|
||||
|
||||
|
@ -144,6 +146,7 @@ const ParticipantContextMenu = ({
|
|||
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
|
||||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||
const isModerationSupported = useSelector(isAvModerationSupported());
|
||||
const stageFilmstrip = useSelector(isStageFilmstripEnabled);
|
||||
|
||||
const _currentRoomId = useSelector(getCurrentRoomId);
|
||||
const _rooms = Object.values(useSelector(getBreakoutRooms));
|
||||
|
@ -231,6 +234,12 @@ const ParticipantContextMenu = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (stageFilmstrip) {
|
||||
buttons2.push(<TogglePinToStageButton
|
||||
key = 'pinToStage'
|
||||
participantID = { _getCurrentParticipantId() } />);
|
||||
}
|
||||
|
||||
if (!disablePrivateChat) {
|
||||
buttons2.push(<PrivateMessageMenuButton
|
||||
key = 'privateMessage'
|
||||
|
|
|
@ -14,7 +14,7 @@ import { getParticipantById } from '../../../base/participants';
|
|||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { LAYOUTS } from '../../../video-layout';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import ParticipantContextMenu from './ParticipantContextMenu';
|
||||
|
@ -265,7 +265,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participantID } = ownProps;
|
||||
const { participantID, currentLayout } = ownProps;
|
||||
let _remoteControlState = null;
|
||||
const participant = getParticipantById(state, participantID);
|
||||
const _participantDisplayName = participant?.name;
|
||||
|
@ -289,7 +289,6 @@ function _mapStateToProps(state, ownProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
let _menuPosition;
|
||||
|
||||
switch (currentLayout) {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
|
||||
import { IconPinParticipant, IconUnpin } from '../../../base/icons';
|
||||
import { addStageParticipant, removeStageParticipant } from '../../../filmstrip/actions.web';
|
||||
import { getActiveParticipantsIds } from '../../../filmstrip/functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* Whether the icon should be hidden or not.
|
||||
*/
|
||||
noIcon: boolean,
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function,
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the button will act.
|
||||
*/
|
||||
participantID: string
|
||||
}
|
||||
|
||||
const TogglePinToStageButton = ({ className, noIcon = false, onClick, participantID }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isActive = Boolean(useSelector(getActiveParticipantsIds).find(p => p === participantID));
|
||||
const _onClick = useCallback(() => {
|
||||
dispatch(isActive
|
||||
? removeStageParticipant(participantID)
|
||||
: addStageParticipant(participantID, true));
|
||||
onClick && onClick();
|
||||
}, [ participantID, isActive ]);
|
||||
|
||||
const text = isActive
|
||||
? t('videothumbnail.unpinFromStage')
|
||||
: t('videothumbnail.pinToStage');
|
||||
|
||||
const icon = isActive ? IconUnpin : IconPinParticipant;
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { text }
|
||||
icon = { noIcon ? null : icon }
|
||||
onClick = { _onClick }
|
||||
text = { text }
|
||||
textClassName = { className } />
|
||||
);
|
||||
};
|
||||
|
||||
export default TogglePinToStageButton;
|
|
@ -13,6 +13,7 @@ export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog'
|
|||
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
|
||||
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
|
||||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
||||
export { default as TogglePinToStageButton } from './TogglePinToStageButton';
|
||||
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
||||
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
|
||||
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
|
||||
|
|
|
@ -9,9 +9,11 @@ import { getLocalParticipant, getParticipantCount } from '../base/participants';
|
|||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks';
|
||||
import { reportError } from '../base/util';
|
||||
import { getActiveParticipantsIds } from '../filmstrip/functions.web';
|
||||
import {
|
||||
getVideoQualityForLargeVideo,
|
||||
getVideoQualityForResizableFilmstripThumbnails,
|
||||
getVideoQualityForStageThumbnails,
|
||||
shouldDisplayTileView
|
||||
} from '../video-layout';
|
||||
|
||||
|
@ -92,6 +94,17 @@ StateListenerRegistry.register(
|
|||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the receiver constraints when the stage participants change.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getActiveParticipantsIds(state).sort()
|
||||
.join(),
|
||||
(_, store) => {
|
||||
_updateReceiverVideoConstraints(store);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* StateListenerRegistry provides a reliable way of detecting changes to
|
||||
* maxReceiverVideoQuality and preferredVideoQuality state and dispatching additional actions.
|
||||
|
@ -219,6 +232,7 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
const tracks = state['features/base/tracks'];
|
||||
const sourceNameSignaling = getSourceNameSignalingFeatureFlag(state);
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
const activeParticipantsIds = getActiveParticipantsIds(state);
|
||||
|
||||
let receiverConstraints;
|
||||
|
||||
|
@ -232,22 +246,36 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
};
|
||||
const visibleRemoteTrackSourceNames = [];
|
||||
let largeVideoSourceName;
|
||||
const activeParticipantsSources = [];
|
||||
|
||||
if (visibleRemoteParticipants?.size) {
|
||||
visibleRemoteParticipants.forEach(participantId => {
|
||||
const sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
|
||||
let sourceName;
|
||||
|
||||
if (remoteScreenShares.includes(participantId)) {
|
||||
sourceName = participantId;
|
||||
} else {
|
||||
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
|
||||
}
|
||||
|
||||
if (sourceName) {
|
||||
visibleRemoteTrackSourceNames.push(sourceName);
|
||||
if (activeParticipantsIds.find(id => id === participantId)) {
|
||||
activeParticipantsSources.push(sourceName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (localParticipantId !== largeVideoParticipantId) {
|
||||
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
|
||||
tracks, MEDIA_TYPE.VIDEO,
|
||||
largeVideoParticipantId
|
||||
);
|
||||
if (remoteScreenShares.includes(largeVideoParticipantId)) {
|
||||
largeVideoSourceName = largeVideoParticipantId;
|
||||
} else {
|
||||
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
|
||||
tracks, MEDIA_TYPE.VIDEO,
|
||||
largeVideoParticipantId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Tile view.
|
||||
|
@ -262,11 +290,7 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
|
||||
// Prioritize screenshare in tile view.
|
||||
if (remoteScreenShares?.length) {
|
||||
const remoteScreenShareSourceNames = remoteScreenShares.map(remoteScreenShare =>
|
||||
getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, remoteScreenShare)
|
||||
);
|
||||
|
||||
receiverConstraints.selectedSources = remoteScreenShareSourceNames;
|
||||
receiverConstraints.selectedSources = remoteScreenShares;
|
||||
}
|
||||
|
||||
// Stage view.
|
||||
|
@ -277,10 +301,14 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
|
||||
if (visibleRemoteTrackSourceNames?.length) {
|
||||
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
|
||||
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
|
||||
|
||||
visibleRemoteTrackSourceNames.forEach(sourceName => {
|
||||
receiverConstraints.constraints[sourceName] = { 'maxHeight': Math.min(qualityLevel,
|
||||
maxFrameHeight) };
|
||||
const isStageParticipant = activeParticipantsSources.find(name => name === sourceName);
|
||||
const quality = Math.min(maxFrameHeight, isStageParticipant
|
||||
? stageParticipantsLevel : qualityLevel);
|
||||
|
||||
receiverConstraints.constraints[sourceName] = { 'maxHeight': quality };
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -326,10 +354,14 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
|
||||
if (visibleRemoteParticipants?.size > 0) {
|
||||
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
|
||||
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
|
||||
|
||||
visibleRemoteParticipants.forEach(participantId => {
|
||||
receiverConstraints.constraints[participantId] = { 'maxHeight': Math.min(qualityLevel,
|
||||
maxFrameHeight) };
|
||||
const isStageParticipant = activeParticipantsIds.find(id => id === participantId);
|
||||
const quality = Math.min(maxFrameHeight, isStageParticipant
|
||||
? stageParticipantsLevel : qualityLevel);
|
||||
|
||||
receiverConstraints.constraints[participantId] = { 'maxHeight': quality };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
@ -42,6 +43,28 @@ type Props = {
|
|||
t: Function
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
addBackground: {
|
||||
marginRight: `${theme.spacing(2)}px`
|
||||
},
|
||||
button: {
|
||||
display: 'none'
|
||||
},
|
||||
label: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
lineHeight: '20px',
|
||||
marginLeft: '-10px',
|
||||
marginTop: `${theme.spacing(3)}px`,
|
||||
marginBottom: `${theme.spacing(2)}px`,
|
||||
color: '#669aec',
|
||||
display: 'inline-flex',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to upload an image.
|
||||
*
|
||||
|
@ -56,6 +79,7 @@ function UploadImageButton({
|
|||
storedImages,
|
||||
t
|
||||
}: Props) {
|
||||
const classes = useStyles();
|
||||
const uploadImageButton: Object = useRef(null);
|
||||
const uploadImageKeyPress = useCallback(e => {
|
||||
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
|
||||
|
@ -100,12 +124,12 @@ function UploadImageButton({
|
|||
<>
|
||||
{showLabel && <label
|
||||
aria-label = { t('virtualBackground.uploadImage') }
|
||||
className = 'file-upload-label'
|
||||
className = { classes.label }
|
||||
htmlFor = 'file-upload'
|
||||
onKeyPress = { uploadImageKeyPress }
|
||||
tabIndex = { 0 } >
|
||||
<Icon
|
||||
className = { 'add-background' }
|
||||
className = { classes.addBackground }
|
||||
size = { 20 }
|
||||
src = { IconPlusCircle } />
|
||||
{t('virtualBackground.addBackground')}
|
||||
|
@ -113,7 +137,7 @@ function UploadImageButton({
|
|||
|
||||
<input
|
||||
accept = 'image/*'
|
||||
className = 'file-upload-btn'
|
||||
className = { classes.button }
|
||||
id = 'file-upload'
|
||||
onChange = { uploadImage }
|
||||
ref = { uploadImageButton }
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import Spinner from '@atlaskit/spinner';
|
||||
import Bourne from '@hapi/bourne';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
@ -106,6 +108,142 @@ function _mapStateToProps(state): Object {
|
|||
|
||||
const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
marginLeft: '-10px',
|
||||
position: 'relative',
|
||||
maxHeight: '300px',
|
||||
color: 'white',
|
||||
display: 'inline-grid',
|
||||
gridTemplateColumns: 'auto auto auto auto auto',
|
||||
columnGap: '9px',
|
||||
cursor: 'pointer',
|
||||
[[ '& .desktop-share:hover',
|
||||
'& .thumbnail:hover',
|
||||
'& .blur:hover',
|
||||
'& .slight-blur:hover',
|
||||
'& .virtual-background-none:hover' ]]: {
|
||||
opacity: 0.5,
|
||||
border: '2px solid #99bbf3'
|
||||
},
|
||||
'& .background-option': {
|
||||
marginTop: `${theme.spacing(2)}px`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
height: '60px',
|
||||
width: '107px',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& thumbnail-container': {
|
||||
position: 'relative',
|
||||
'&:focus-within .thumbnail ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
'& .thumbnail': {
|
||||
objectFit: 'cover'
|
||||
},
|
||||
'& .thumbnail:hover ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .thumbnail-selected': {
|
||||
objectFit: 'cover',
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .blur': {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#7e8287',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .blur-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .slight-blur': {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#a4a4a4',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .slight-blur-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .virtual-background-none': {
|
||||
background: '#525252',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .none-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .desktop-share': {
|
||||
background: '#525252'
|
||||
},
|
||||
'& .desktop-share-selected': {
|
||||
border: '2px solid #246fe5',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& delete-image-icon': {
|
||||
background: '#3d3d3d',
|
||||
position: 'absolute',
|
||||
display: 'none',
|
||||
left: '96px',
|
||||
bottom: '51px',
|
||||
'&:hover': {
|
||||
display: 'block'
|
||||
},
|
||||
'@media (max-width: 632px)': {
|
||||
left: '51px'
|
||||
}
|
||||
},
|
||||
'@media (max-width: 360px)': {
|
||||
gridTemplateColumns: 'auto auto auto'
|
||||
},
|
||||
'@media (max-width: 632px)': {
|
||||
fontSize: '1.5vw',
|
||||
|
||||
[[ '& .desktop-share:hover',
|
||||
'& .thumbnail:hover',
|
||||
'& .blur:hover',
|
||||
'& .slight-blur:hover',
|
||||
'& .virtual-background-none:hover' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
},
|
||||
|
||||
[[ '& .desktop-share',
|
||||
'& .virtual-background-none,',
|
||||
'& .thumbnail,',
|
||||
'& .blur,',
|
||||
'& .slight-blur' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
},
|
||||
[[ '& .desktop-share-selected',
|
||||
'& .thumbnail-selected',
|
||||
'& .none-selected',
|
||||
'& .blur-selected',
|
||||
'& .slight-blur-selected' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
}
|
||||
}
|
||||
},
|
||||
dialogMarginTop: {
|
||||
marginTop: '44px'
|
||||
},
|
||||
virtualBackgroundLoading: {
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
marginTop: '10px',
|
||||
transform: 'translateX(-50%)'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders virtual background dialog.
|
||||
*
|
||||
|
@ -122,6 +260,7 @@ function VirtualBackground({
|
|||
initialOptions,
|
||||
t
|
||||
}: Props) {
|
||||
const classes = useStyles();
|
||||
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
|
||||
const [ options, setOptions ] = useState({ ...initialOptions });
|
||||
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
|
||||
|
@ -382,7 +521,7 @@ function VirtualBackground({
|
|||
loadedPreview = { loadedPreviewState }
|
||||
options = { options } />
|
||||
{loading ? (
|
||||
<div className = 'virtual-background-loading'>
|
||||
<div className = { classes.virtualBackgroundLoading }>
|
||||
<Spinner
|
||||
isCompleting = { false }
|
||||
size = 'medium' />
|
||||
|
@ -397,7 +536,7 @@ function VirtualBackground({
|
|||
showLabel = { previewIsLoaded }
|
||||
storedImages = { storedImages } />}
|
||||
<div
|
||||
className = { `virtual-background-dialog${previewIsLoaded ? '' : ' dialog-margin-top'}` }
|
||||
className = { clsx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) }
|
||||
role = 'radiogroup'
|
||||
tabIndex = '-1'>
|
||||
<Tooltip
|
||||
|
@ -406,8 +545,9 @@ function VirtualBackground({
|
|||
<div
|
||||
aria-checked = { _selectedThumbnail === 'none' }
|
||||
aria-label = { t('virtualBackground.removeBackground') }
|
||||
className = { _selectedThumbnail === 'none' ? 'background-option none-selected'
|
||||
: 'background-option virtual-background-none' }
|
||||
className = { clsx('background-option', 'virtual-background-none', {
|
||||
'none-selected': _selectedThumbnail === 'none'
|
||||
}) }
|
||||
onClick = { removeBackground }
|
||||
onKeyPress = { removeBackgroundKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -421,8 +561,9 @@ function VirtualBackground({
|
|||
<div
|
||||
aria-checked = { _selectedThumbnail === 'slight-blur' }
|
||||
aria-label = { t('virtualBackground.slightBlur') }
|
||||
className = { _selectedThumbnail === 'slight-blur'
|
||||
? 'background-option slight-blur-selected' : 'background-option slight-blur' }
|
||||
className = { clsx('background-option', 'slight-blur', {
|
||||
'slight-blur-selected': _selectedThumbnail === 'slight-blur'
|
||||
}) }
|
||||
onClick = { enableSlideBlur }
|
||||
onKeyPress = { enableSlideBlurKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -436,8 +577,9 @@ function VirtualBackground({
|
|||
<div
|
||||
aria-checked = { _selectedThumbnail === 'blur' }
|
||||
aria-label = { t('virtualBackground.blur') }
|
||||
className = { _selectedThumbnail === 'blur' ? 'background-option blur-selected'
|
||||
: 'background-option blur' }
|
||||
className = { clsx('background-option', 'blur', {
|
||||
'blur-selected': _selectedThumbnail === 'blur'
|
||||
}) }
|
||||
onClick = { enableBlur }
|
||||
onKeyPress = { enableBlurKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -452,9 +594,9 @@ function VirtualBackground({
|
|||
<div
|
||||
aria-checked = { _selectedThumbnail === 'desktop-share' }
|
||||
aria-label = { t('virtualBackground.desktopShare') }
|
||||
className = { _selectedThumbnail === 'desktop-share'
|
||||
? 'background-option desktop-share-selected'
|
||||
: 'background-option desktop-share' }
|
||||
className = { clsx('background-option', 'desktop-share', {
|
||||
'desktop-share-selected': _selectedThumbnail === 'desktop-share'
|
||||
}) }
|
||||
onClick = { shareDesktop }
|
||||
onKeyPress = { shareDesktopKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -494,8 +636,10 @@ function VirtualBackground({
|
|||
<img
|
||||
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
|
||||
aria-checked = { _selectedThumbnail === image.id }
|
||||
className = { _selectedThumbnail === image.id
|
||||
? 'background-option thumbnail-selected' : 'background-option thumbnail' }
|
||||
className = { clsx('background-option', {
|
||||
'thumbnail-selected': _selectedThumbnail === image.id,
|
||||
'thumbnail': _selectedThumbnail !== image.id
|
||||
}) }
|
||||
data-imageid = { image.id }
|
||||
onClick = { setUploadedImageBackground }
|
||||
onError = { onError }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import Spinner from '@atlaskit/spinner';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { hideDialog } from '../../base/dialog';
|
||||
|
@ -29,6 +30,11 @@ export type Props = {
|
|||
*/
|
||||
_currentCameraDeviceId: string,
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: Object,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
|
@ -71,6 +77,55 @@ type State = {
|
|||
jitsiTrack: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = theme => {
|
||||
return {
|
||||
virtualBackgroundPreview: {
|
||||
'& .video-preview': {
|
||||
height: '250px'
|
||||
},
|
||||
|
||||
'& .video-background-preview-entry': {
|
||||
marginLeft: '-10px',
|
||||
height: '250px',
|
||||
width: '570px',
|
||||
marginBottom: `${theme.spacing(2)}px`,
|
||||
zIndex: 2,
|
||||
|
||||
'@media (max-width: 632px)': {
|
||||
maxWidth: '336px'
|
||||
}
|
||||
},
|
||||
|
||||
'& .video-preview-loader': {
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'transparent',
|
||||
height: '250px',
|
||||
marginBottom: `${theme.spacing(2)}px`,
|
||||
width: '572px',
|
||||
position: 'fixed',
|
||||
zIndex: 2,
|
||||
|
||||
'& svg': {
|
||||
position: 'absolute',
|
||||
top: '40%',
|
||||
left: '45%'
|
||||
},
|
||||
|
||||
'@media (min-width: 432px) and (max-width: 632px)': {
|
||||
width: '340px'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link PureComponent} which displays the virtual
|
||||
* background preview.
|
||||
|
@ -265,11 +320,13 @@ class VirtualBackgroundPreview extends PureComponent<Props, State> {
|
|||
*/
|
||||
render() {
|
||||
const { jitsiTrack } = this.state;
|
||||
const { classes } = this.props;
|
||||
|
||||
return jitsiTrack
|
||||
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
|
||||
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
|
||||
;
|
||||
return (<div className = { classes.virtualBackgroundPreview }>
|
||||
{jitsiTrack
|
||||
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
|
||||
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
|
||||
}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,4 +344,4 @@ function _mapStateToProps(state): Object {
|
|||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(VirtualBackgroundPreview));
|
||||
export default translate(connect(_mapStateToProps)(withStyles(styles)(VirtualBackgroundPreview)));
|
||||
|
|
Loading…
Reference in New Issue