mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-06 06:42:28 +00:00
Compare commits
54 Commits
5943
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbabdf3351 | ||
|
|
3bcde982ac | ||
|
|
beb301e691 | ||
|
|
d0836ff651 | ||
|
|
c028511aaf | ||
|
|
664552bc05 | ||
|
|
ac35eea08e | ||
|
|
172683d645 | ||
|
|
858e83b09e | ||
|
|
e2750ee58e | ||
|
|
fe132581d4 | ||
|
|
21083e6777 | ||
|
|
a3c60d5943 | ||
|
|
28556e030c | ||
|
|
e54685a566 | ||
|
|
9ffb3eddb0 | ||
|
|
d064f607ec | ||
|
|
be454137f9 | ||
|
|
530369613a | ||
|
|
6b0b575bd4 | ||
|
|
aa5dd92846 | ||
|
|
85c505a29d | ||
|
|
b8e12e581f | ||
|
|
8456a63a23 | ||
|
|
89394e69b6 | ||
|
|
3ae50c1701 | ||
|
|
5da40a5fb0 | ||
|
|
f04a01ee3a | ||
|
|
9ad57127d4 | ||
|
|
a131644dfb | ||
|
|
649c1b0669 | ||
|
|
7bcf7bb686 | ||
|
|
8e035b03b0 | ||
|
|
3b05fd70a3 | ||
|
|
64d32a5005 | ||
|
|
fbe15aaa47 | ||
|
|
16bcb1b217 | ||
|
|
d4c314deb3 | ||
|
|
e8e9e24f77 | ||
|
|
9f72c318d6 | ||
|
|
5f1a4f189c | ||
|
|
cb9511ef2c | ||
|
|
e7c4a55add | ||
|
|
3884862996 | ||
|
|
8f1c693c3e | ||
|
|
4d2000a7a4 | ||
|
|
f6a79860f2 | ||
|
|
4ed2d55cae | ||
|
|
2dad8163bb | ||
|
|
985b37195c | ||
|
|
052b588b33 | ||
|
|
320aeaa4df | ||
|
|
8c9c1a29be | ||
|
|
c25fb702c2 |
@@ -26,5 +26,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=22.1.0
|
||||
sdkVersion=5.0.0
|
||||
appVersion=22.1.1
|
||||
sdkVersion=5.0.2
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
sendLocalParticipant,
|
||||
nonParticipantMessageReceived
|
||||
} from './react/features/base/conference';
|
||||
import { getReplaceParticipant } from './react/features/base/config/functions';
|
||||
import { getReplaceParticipant, getMultipleVideoSupportFeatureFlag } from './react/features/base/config/functions';
|
||||
import {
|
||||
checkAndNotifyForNewDevice,
|
||||
getAvailableDevices,
|
||||
@@ -106,6 +106,7 @@ import {
|
||||
updateSettings
|
||||
} from './react/features/base/settings';
|
||||
import {
|
||||
addLocalTrack,
|
||||
createLocalPresenterTrack,
|
||||
createLocalTracksF,
|
||||
destroyLocalTracks,
|
||||
@@ -1444,11 +1445,13 @@ export default {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
useVideoStream(newTrack) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
logger.debug(`useVideoStream: ${newTrack}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
_replaceLocalVideoTrackQueue.enqueue(onFinish => {
|
||||
const oldTrack = getLocalJitsiVideoTrack(APP.store.getState());
|
||||
const oldTrack = getLocalJitsiVideoTrack(state);
|
||||
|
||||
logger.debug(`useVideoStream: Replacing ${oldTrack} with ${newTrack}`);
|
||||
|
||||
@@ -1459,6 +1462,26 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// In the multi-stream mode, add the track to the conference if there is no existing track, replace it
|
||||
// otherwise.
|
||||
if (getMultipleVideoSupportFeatureFlag(state)) {
|
||||
const trackAction = oldTrack
|
||||
? replaceLocalTrack(oldTrack, newTrack, room)
|
||||
: addLocalTrack(newTrack);
|
||||
|
||||
APP.store.dispatch(trackAction)
|
||||
.then(() => {
|
||||
this.setVideoMuteStatus();
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
logger.error(`useVideoStream failed: ${error}`);
|
||||
reject(error);
|
||||
})
|
||||
.then(onFinish);
|
||||
|
||||
return;
|
||||
}
|
||||
APP.store.dispatch(
|
||||
replaceLocalTrack(oldTrack, newTrack, room))
|
||||
.then(() => {
|
||||
|
||||
11
config.js
11
config.js
@@ -605,6 +605,7 @@ var config = {
|
||||
// 'help',
|
||||
// 'highlight',
|
||||
// 'invite',
|
||||
// 'linktosalesforce',
|
||||
// 'livestreaming',
|
||||
// 'microphone',
|
||||
// 'mute-everyone',
|
||||
@@ -1280,6 +1281,7 @@ var config = {
|
||||
// 'notify.raisedHand', // shown when a partcipant used raise hand,
|
||||
// 'notify.startSilentTitle', // shown when user joined with no audio
|
||||
// 'notify.unmute', // shown to moderator when user raises hand during AV moderation
|
||||
// 'notify.hostAskedUnmute', // shown to participant when host asks them to unmute
|
||||
// 'prejoin.errorDialOut',
|
||||
// 'prejoin.errorDialOutDisconnected',
|
||||
// 'prejoin.errorDialOutFailed',
|
||||
@@ -1302,7 +1304,14 @@ var config = {
|
||||
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
|
||||
// // (width, tiles aspect ratios) through the interfaceConfig options.
|
||||
// disableResizable: false,
|
||||
// }
|
||||
// },
|
||||
|
||||
// Tile view related config options.
|
||||
// tileView: {
|
||||
// // The optimal number of tiles that are going to be shown in tile view. Depending on the screen size it may
|
||||
// // not be possible to show the exact number of participants specified here.
|
||||
// numberOfVisibleTiles: 25
|
||||
// },
|
||||
|
||||
|
||||
// Specifies whether the chat emoticons are disabled or not
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
.avatar {
|
||||
background-color: #AAA;
|
||||
border-radius: 50%;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-weight: 100;
|
||||
object-fit: cover;
|
||||
|
||||
&.avatar-small {
|
||||
height: 28px !important;
|
||||
width: 28px !important;
|
||||
}
|
||||
|
||||
&.avatar-xsmall {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
position: relative;
|
||||
|
||||
&-available::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-available;
|
||||
}
|
||||
|
||||
&-away::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-away;
|
||||
}
|
||||
|
||||
&-busy::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-busy;
|
||||
}
|
||||
|
||||
&-idle::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-idle;
|
||||
}
|
||||
}
|
||||
@@ -164,16 +164,6 @@ form {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialogs fade
|
||||
*/
|
||||
.aui-blanket {
|
||||
background: #000;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
transition-delay: 0.1s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#inviteLinkRef {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
|
||||
@@ -193,16 +193,3 @@
|
||||
@mixin transparentBg($color, $alpha) {
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar status badge mixin
|
||||
*/
|
||||
@mixin avatarBadge {
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 35%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
margin: 8px 16px 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
.participants_pane {
|
||||
height: 100vh;
|
||||
|
||||
@@ -28,10 +28,6 @@ $defaultSemiDarkColor: #ACACAC;
|
||||
$defaultDarkColor: #2b3d5c;
|
||||
$defaultWarningColor: rgb(215, 121, 118);
|
||||
$participantsPaneBgColor: #141414;
|
||||
$presence-available: rgb(110, 176, 5);
|
||||
$presence-away: rgb(250, 201, 20);
|
||||
$presence-busy: rgb(233, 0, 27);
|
||||
$presence-idle: rgb(172, 172, 172);
|
||||
|
||||
/**
|
||||
* Toolbar
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
.select2-container.aui-select2-container {
|
||||
background-color: transparent !important;
|
||||
margin-top: 2px;
|
||||
|
||||
a.select2-choice {
|
||||
height: 28px !important;
|
||||
line-height: 18px !important;
|
||||
width: 100% !important;
|
||||
background-color: $selectBg !important;
|
||||
border-color: $selectBg !important;
|
||||
color: $selectFontColor !important;
|
||||
text-shadow: none !important;
|
||||
font-size: 12px !important;
|
||||
margin: 0 auto !important;
|
||||
|
||||
&:after {
|
||||
border-top-color: $selectFontColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.select2-dropdown-open{
|
||||
a.select2-choice {
|
||||
background-color: $selectActiveBg !important;
|
||||
border-color: $selectActiveBg !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select2-drop.aui-select2-drop.aui-style-default {
|
||||
z-index: $dropdownZ;
|
||||
background-color: $selectActiveBg;
|
||||
border-color: $selectActiveBg;
|
||||
|
||||
.select2-results{
|
||||
background-color: $selectActiveBg;
|
||||
border-color: $selectActiveBg;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $selectActiveItemBg;
|
||||
}
|
||||
|
||||
.select2-result{
|
||||
&.select2-highlighted{
|
||||
background-color: $selectActiveItemBg;
|
||||
}
|
||||
|
||||
.select2-result-label{
|
||||
font-size: 12px;
|
||||
color: $selectFontColor !important;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.select2-drop-mask {
|
||||
z-index: $dropdownMaskZ;
|
||||
}
|
||||
@@ -73,6 +73,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filmstrip__videos.has-scroll {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.remote-videos {
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -90,7 +94,6 @@
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
.videocontainer {
|
||||
border: 0;
|
||||
|
||||
@@ -117,6 +117,25 @@
|
||||
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
|
||||
}
|
||||
|
||||
.filmstrip__videos.vertical-view-grid#remoteVideos {
|
||||
align-items: 'center';
|
||||
border: 0px;
|
||||
padding-right: 7px;
|
||||
|
||||
&.has-scroll {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.remote-videos > div {
|
||||
left: 0px; // fixes an issue on FF - the div is aligned to the right by default for some reason
|
||||
}
|
||||
|
||||
.videocontainer {
|
||||
border: 0px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-videos {
|
||||
display: flex;
|
||||
overscroll-behavior: contain;
|
||||
@@ -125,10 +144,6 @@
|
||||
transition: height .3s ease-in;
|
||||
}
|
||||
|
||||
&.vertical-grid-margin > div {
|
||||
margin-right: $scrollHeight;
|
||||
}
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
transition: opacity 1s;
|
||||
|
||||
@@ -24,8 +24,7 @@ $flagsImagePath: "../images/";
|
||||
/* Flags END */
|
||||
|
||||
/* Modules BEGIN */
|
||||
|
||||
@import 'aui_reset';
|
||||
@import 'reset';
|
||||
@import 'atlaskit_overrides';
|
||||
@import 'base';
|
||||
@import 'utils';
|
||||
@@ -42,7 +41,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'modals/settings/settings';
|
||||
@import 'modals/screen-share/share-audio';
|
||||
@import 'modals/screen-share/share-screen-warning';
|
||||
@import 'modals/speaker_stats/speaker_stats';
|
||||
@import 'modals/virtual-background/virtual-background';
|
||||
@import 'modals/local-recording/local-recording';
|
||||
@import 'videolayout_default';
|
||||
@@ -64,7 +62,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'components/input-control';
|
||||
@import 'components/input-slider';
|
||||
@import "connection-info";
|
||||
@import 'aui-components/dropdown';
|
||||
@import '404';
|
||||
@import 'policy';
|
||||
@import 'popover';
|
||||
@@ -82,7 +79,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'navigate_section_list';
|
||||
@import 'third-party-branding/google';
|
||||
@import 'third-party-branding/microsoft';
|
||||
@import 'avatar';
|
||||
@import 'promotional-footer';
|
||||
@import 'chrome-extension-banner';
|
||||
@import 'settings-button';
|
||||
|
||||
@@ -1,114 +1,3 @@
|
||||
.dialog {
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
min-height: 131px;
|
||||
overflow: visible;
|
||||
visibility: visible;
|
||||
width: 400px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $auiDialogColor;
|
||||
}
|
||||
|
||||
.aui {
|
||||
|
||||
&-dialog2 {
|
||||
&-header, &-footer {
|
||||
background-color: $auiDialogBg;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&-header {
|
||||
border-bottom: 1px solid $auiBorderColor;
|
||||
border-radius: 5px 5px 0 0;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
display: table;
|
||||
font-weight: normal;
|
||||
height: em(58, 12);
|
||||
margin-top: -69px;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
|
||||
h2 {
|
||||
font-size: em(20, 12);
|
||||
font-weight: $dialogTitleFontWeight;
|
||||
color: $auiDialogColor;
|
||||
}
|
||||
|
||||
&-main {
|
||||
display: table-cell;
|
||||
padding-right: 0;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top: 1px solid $auiBorderColor;
|
||||
border-radius: 0 0 5px 5px;
|
||||
box-sizing: border-box;
|
||||
height: 51px;
|
||||
overflow: hidden;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
&:empty {
|
||||
height: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: $auiDialogBg;
|
||||
box-sizing: border-box;
|
||||
color: $auiDialogColor;
|
||||
font-size: em(14, 12);
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
p,span, h3 {
|
||||
font-weight: $labelFontWeight;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
background-color: $auiDialogContentBg;
|
||||
color: $auiDialogColor;
|
||||
}
|
||||
|
||||
.form-control:not(:last-child) {
|
||||
border-bottom: 1px solid $auiBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 420px) {
|
||||
.aui-dialog2-small .aui-dialog2-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
margin-top: 5px !important;
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
.speaker-stats {
|
||||
list-style: none;
|
||||
.row{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.avatar {
|
||||
width: 32px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.name-time {
|
||||
width: calc(100% - 48px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.name-time_expressions-on {
|
||||
width: calc(47% - 48px);
|
||||
}
|
||||
.expressions {
|
||||
width: calc(53% - 29px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.expression {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
.con-status {
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.16px;
|
||||
line-height: 16px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
&-circle {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&--good {
|
||||
background: #31B76A;
|
||||
}
|
||||
|
||||
&--poor {
|
||||
background: #E12D2D;
|
||||
}
|
||||
|
||||
&--non-optimal {
|
||||
background: #E39623;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
margin-left: auto;
|
||||
transition: background-color 0.16s ease-out;
|
||||
|
||||
&--up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&>svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(1,1,1, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-details {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-top: 1px solid #5E6D7A;
|
||||
padding: 16px;
|
||||
transition: opacity 0.16s ease-out;
|
||||
|
||||
&-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
.device {
|
||||
&-status {
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
|
||||
&-error {
|
||||
align-items: flex-start;
|
||||
background-color: #F8AE1A;
|
||||
border-radius: 6px;
|
||||
color: #040404;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
&--ok {
|
||||
svg path {
|
||||
fill: #189b55;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
@import 'connection-status';
|
||||
@import 'device-status';
|
||||
@import 'lobby';
|
||||
@import 'premeeting-screens';
|
||||
@import 'prejoin';
|
||||
|
||||
@@ -116,72 +116,3 @@
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-dialog-dialin {
|
||||
text-align: center;
|
||||
|
||||
&-header {
|
||||
align-items: center;
|
||||
margin: 16px 0 32px 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&-num {
|
||||
background: #3e474f;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin: 4px;
|
||||
padding: 8px;
|
||||
|
||||
&-container {
|
||||
min-height: 48px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-link {
|
||||
color: #6FB1EA;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&-spaced-label {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
&-btns {
|
||||
&> div {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-dialog-calling {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
||||
&-header {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 15px;
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
|
||||
&-number {
|
||||
font-size: 19px;
|
||||
line-height: 28px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,30 +23,6 @@
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&-dropdown-btn {
|
||||
align-items: center;
|
||||
color: #1C2025;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
padding: 0 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: #DAEBFA;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown-icon {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
|
||||
& > svg {
|
||||
fill: #1C2025;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@@ -161,13 +161,6 @@
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.con-status {
|
||||
margin: 24px auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: $prejoinDefaultContentWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// mobile phone landscape
|
||||
@@ -175,10 +168,6 @@
|
||||
div.content {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.con-status {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
@@ -198,11 +187,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.con-status {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.device-status-error {
|
||||
border-radius: 0;
|
||||
margin: 0 -16px;
|
||||
|
||||
@@ -46,11 +46,6 @@ $reloadProgressBarBg: #0074E0;
|
||||
/**
|
||||
* Dialog colors
|
||||
**/
|
||||
$auiDialogColor: #eceef1;
|
||||
$auiDialogBg: #253858;
|
||||
$auiDialogContentBg: #344563;
|
||||
$auiBorderColor: #253858;
|
||||
$dialogTitleFontWeight: 400;
|
||||
$dialogErrorText: #344563;
|
||||
|
||||
/**
|
||||
|
||||
@@ -330,7 +330,7 @@ PODS:
|
||||
- react-native-video/Video (= 5.2.0)
|
||||
- react-native-video/Video (5.2.0):
|
||||
- React-Core
|
||||
- react-native-webrtc (1.98.0):
|
||||
- react-native-webrtc (1.94.2):
|
||||
- React-Core
|
||||
- react-native-webview (11.15.1):
|
||||
- React-Core
|
||||
@@ -683,7 +683,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
|
||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||
react-native-video: a4c2635d0802f983594b7057e1bce8f442f0ad28
|
||||
react-native-webrtc: 83e3360d31a88809db308b20a4243244646d4250
|
||||
react-native-webrtc: 1856ac061df94b1bd6037f1f3b56d1b8bc2b50e7
|
||||
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
|
||||
React-perflogger: 93075d8931c32cd1fce8a98c15d2d5ccc4d891bd
|
||||
React-RCTActionSheet: 7d3041e6761b4f3044a37079ddcb156575fb6d89
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>22.1.0</string>
|
||||
<string>22.1.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -37,13 +37,8 @@
|
||||
[builder setFeatureFlag:@"welcomepage.enabled" withBoolean:YES];
|
||||
[builder setFeatureFlag:@"resolution" withValue:@(360)];
|
||||
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];
|
||||
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
|
||||
|
||||
// Apple rejected our app because they claim requiring a
|
||||
// Dropbox account for recording is not acceptable.
|
||||
#if DEBUG
|
||||
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
|
||||
#endif
|
||||
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
|
||||
}];
|
||||
|
||||
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>22.1.0</string>
|
||||
<string>22.1.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>22.1.0</string>
|
||||
<string>22.1.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>22.1.0</string>
|
||||
<string>22.1.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -71,7 +71,7 @@ platform :ios do
|
||||
|
||||
# Inrement the build number by 1
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
build_number: Time.now.to_i,
|
||||
xcodeproj: "app/app.xcodeproj"
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.0.0</string>
|
||||
<string>5.0.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"enter": "أدخل الغرفة",
|
||||
"error": "خطأ: لم تُرسَل رسالتك. السبب: {{error}}",
|
||||
"fieldPlaceHolder": "اكتب رسالتك هنا",
|
||||
"lobbyChatMessageTo": "رسالة دردشة لوبي إلى {{recipient}}",
|
||||
"message": "رسالة",
|
||||
"messageAccessibleTitle": "{{user}} مقولة:",
|
||||
"messageAccessibleTitleMe": "أنا أقول:",
|
||||
@@ -215,6 +216,8 @@
|
||||
"liveStreaming": "بث حي مباشر"
|
||||
},
|
||||
"add": "أضف",
|
||||
"addMeetingNote": "أضف ملاحظة حول هذا المُلتقى",
|
||||
"addOptionalNote": "أضف ملاحظة (اختياري)",
|
||||
"allow": "اسمح",
|
||||
"alreadySharedVideoMsg": "يشارك أحد الحضور الفيديو حاليًا، ولا يسمح هذا الإجتماع سوى بمشاركة فيديو واحد في آن واحد",
|
||||
"alreadySharedVideoTitle": "لا يُسمَح سوى بفيديو مشارك واحد على الأكثر في آن واحد.",
|
||||
@@ -266,6 +269,8 @@
|
||||
"kickParticipantDialog": "أمتأكد من طرد هذا المشارك؟",
|
||||
"kickParticipantTitle": "أتريد طرد هذا المشارك؟",
|
||||
"kickTitle": "عذرًا! تم طردك {{participantDisplayName}} من المُلتقى",
|
||||
"linkMeeting": "ربط المُلتقى",
|
||||
"linkMeetingTitle": "ربط المُلتقى بـ Salesforce",
|
||||
"liveStreaming": "البث المباشر الحي",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "غير ممكن أثناء التسجيل",
|
||||
"liveStreamingDisabledTooltip": "بدء بثٍ حيٍّ مُعطَّل",
|
||||
@@ -320,6 +325,7 @@
|
||||
"popupError": "يمنع متصفحك النوافذ المنبثقة من هذا الموقع. فعِّل رجاءً النوافذ المنبثقة في المتصفح من إعدادات الحماية وحاول مرة أخرى.",
|
||||
"popupErrorTitle": "النوافذ المنبثقة محجوبة.",
|
||||
"readMore": "أكثر",
|
||||
"recentlyUsedObjects": "الأشياء المستخدمة مؤخرًا",
|
||||
"recording": "قيد التسجيل",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "ليس بالإمكان ما دمت قيد البث المباشر",
|
||||
"recordingDisabledTooltip": "عملية التسجيل معطلة.",
|
||||
@@ -342,6 +348,12 @@
|
||||
"screenSharingFailed": "عجبًا! حصل خطأ ما، فلن نتمكن من مشاركة الشاشة للأسف!",
|
||||
"screenSharingFailedTitle": "فشلت عملية مشاركة الشاشة!",
|
||||
"screenSharingPermissionDeniedError": "عجبًا! حصل خطأ ما متعلق بأذونات إضافة مشاركة الشاشة. أعد التحميل وجرِّب مرة أخرى، رجاءً.",
|
||||
"searchInSalesforce": "ابحث في Salesforce",
|
||||
"searchResults": "نتائج البحث ({{count}})",
|
||||
"searchResultsDetailsError": "حدث خطأ ما أثناء استرداد بيانات المالك.",
|
||||
"searchResultsError": "حدث خطأ ما أثناء استرداد البيانات.",
|
||||
"searchResultsNotFound": "لم يتم العثور على نتائج عن البحث.",
|
||||
"searchResultsTryAgain": "حاول استخدام كلمات رئيسية بديلة.",
|
||||
"sendPrivateMessage": "وصلتك رسالة خاصة للتو، أتنوي الرد عليها ردًا خاصًا أم تريد إرسال رسالتك على المجموعة؟",
|
||||
"sendPrivateMessageCancel": "أرسل إلى المجموعة",
|
||||
"sendPrivateMessageOk": "أرسل ردًا خاصًا",
|
||||
@@ -409,6 +421,10 @@
|
||||
"veryBad": "سيئة للغاية",
|
||||
"veryGood": "ممتازة"
|
||||
},
|
||||
"giphy": {
|
||||
"noResults": "لم يتم العثور على نتائج :(",
|
||||
"search": "ابحث في GIPHY"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "مركز المساعدة"
|
||||
},
|
||||
@@ -475,6 +491,7 @@
|
||||
"focusLocal": "ركز على الفيديو الخاص بك",
|
||||
"focusRemote": "ركز على فيديو مشارك آخر",
|
||||
"fullScreen": "استعمل/اخرج من وضع الشاشة الكاملة",
|
||||
"giphyMenu": "تبديل قائمة GIPHY",
|
||||
"keyboardShortcuts": "اختصارات لوحة المفاتيح",
|
||||
"localRecording": "اظهِر أو اخفِ التحكم بالتسجيل المحلي",
|
||||
"mute": "اكتم أو ألغ كتم المجهار (المايكروفون) الخاص بك",
|
||||
@@ -529,6 +546,7 @@
|
||||
"admitAll": "سمح للجميع بالدخول",
|
||||
"allow": "اسمح",
|
||||
"backToKnockModeButton": "لا يوجد كلمة مرور، اطلب الإذن بالدخول بدلًا من ذلك.",
|
||||
"chat": "دردشة",
|
||||
"dialogTitle": "ونضع غرفة الانتظار",
|
||||
"disableDialogContent": "وضع غرفة الانتظار مفعَّل. تسمح هذه الميزة بعدم السماح للغرباء بالانضمام إلى المُلتقى، فهل تريد حقًا تعطيلها؟",
|
||||
"disableDialogSubmit": "عطِّل",
|
||||
@@ -549,6 +567,8 @@
|
||||
"knockButton": "اطلب إذن الدخول",
|
||||
"knockTitle": "يريد أحدٌ الدخول إلى المُلتقى",
|
||||
"knockingParticipantList": "تنبيه قائمة المشاركين",
|
||||
"lobbyChatStartedNotification": "بدأ {{moderator}} دردشة في الردهة مع {{attendee}}",
|
||||
"lobbyChatStartedTitle": "بدأ {{moderator}} محادثة في الردهة معك.",
|
||||
"nameField": "أدخل اسمك",
|
||||
"notificationLobbyAccessDenied": "رفض {{originParticipantName}} مشاركة {{targetParticipantName}} للمُلتقى",
|
||||
"notificationLobbyAccessGranted": "سمح {{originParticipantName}} بمشاركة {{targetParticipantName}} للمُلتقى",
|
||||
@@ -620,6 +640,12 @@
|
||||
"leftOneMember": "{{name}} غادر المُلتقى",
|
||||
"leftThreePlusMembers": "غادر {{name}} والعديد من الأشخاص الآخرين المُلتقى",
|
||||
"leftTwoMembers": "غادر {{first}} و {{second}} المُلتقى",
|
||||
"linkToSalesforce": "ارتباط إلى Salesforce",
|
||||
"linkToSalesforceDescription": "يمكنك ربط ملخص الاجتماع بكائن Salesforce.",
|
||||
"linkToSalesforceError": "فشل ربط المُلتقى بـ Salesforce",
|
||||
"linkToSalesforceKey": "ربط هذا المُلتقى",
|
||||
"linkToSalesforceProgress": "جارٍ ربط الاجتماع بـ Salesforce ...",
|
||||
"linkToSalesforceSuccess": "تم ربط الاجتماع بـ Salesforce",
|
||||
"me": "أنا",
|
||||
"moderationInEffectCSDescription": "يرجى رفع اليد إذا كنت تريد مشاركة شاشتك.",
|
||||
"moderationInEffectCSTitle": "تم حظر مشاركة الشاشة من قبل المشرف",
|
||||
@@ -813,6 +839,18 @@
|
||||
},
|
||||
"raisedHand": "يرد التحدث",
|
||||
"raisedHandsLabel": "عدد الأيدي المرفوعة",
|
||||
"record": {
|
||||
"already": {
|
||||
"linked": "السجل مرتبط بالفعل بهذه الجلسة."
|
||||
},
|
||||
"type": {
|
||||
"account": "الحساب",
|
||||
"contact": "جهات الاتصال",
|
||||
"lead": "البدء",
|
||||
"opportunity": "الفرصة",
|
||||
"owner": "المالك"
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"authDropboxText": "رفع إلى Dropbox",
|
||||
"availableSpace": "المساحة المتاحة: {{spaceLeft}} ميغابايت (ما يقارب {{duration}} دقيقة تسجيل).",
|
||||
@@ -827,6 +865,10 @@
|
||||
"expandedPending": "بدء التسجيل...",
|
||||
"failedToStart": "فشل بدء التسجيل",
|
||||
"fileSharingdescription": "شارك التسجيل مع المشاركين للمُلتقى",
|
||||
"highlightMoment": "لحظة تسليط الضوء",
|
||||
"highlightMomentDisabled": "يمكنك تمييز اللحظات التي يبدأ فيها التسجيل",
|
||||
"highlightMomentSuccess": "تم تمييز اللحظة",
|
||||
"highlightMomentSucessDescription": "ستتم إضافة اللحظة المميزة إلى ملخص المُلتقى.",
|
||||
"inProgress": "التسجيل أو البث المباشر قيد التقدم",
|
||||
"limitNotificationDescriptionNative": "نظرًا للضغط الكبير، سيقيَّد التسجيل إلى {{limit}} د، ولكن إن أردت التسجيل لمدة مفتوحة، جرِّب <3>{{app}}</3>.",
|
||||
"limitNotificationDescriptionWeb": "نظرًا للضغط الكبير، سيقيَّد التسجيل إلى {{limit}} د، ولكن إن أردت التسجيل لمدة مفتوحة، جرِّب <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
@@ -841,6 +883,7 @@
|
||||
"rec": "تسجيل",
|
||||
"serviceDescription": "ستحفظ خدمة التسجيل الفيديو المستجل",
|
||||
"serviceDescriptionCloud": "تسجيل سحابي",
|
||||
"serviceDescriptionCloudInfo": "يتم مسح المُلتقيات المسجلة تلقائيًا بعد 24 ساعة من وقت التسجيل.",
|
||||
"serviceName": "خدمة التسجيل",
|
||||
"sessionAlreadyActive": "هذه الجلسة قيد التسجيل أو البث المباشر.",
|
||||
"signIn": "دخول",
|
||||
@@ -973,6 +1016,7 @@
|
||||
"expand": "وسّع",
|
||||
"feedback": "أبدِ رأيك",
|
||||
"fullScreen": "استعمل/اخرج من وضع الشاشة الكاملة",
|
||||
"giphy": "تبديل قائمة GIPHY",
|
||||
"grantModerator": "امنح صلاحيات رئيس الجلسة",
|
||||
"hangup": "أغلق المكالمة",
|
||||
"help": "مساعدة",
|
||||
@@ -980,6 +1024,7 @@
|
||||
"kick": "اطرد مشاركًا",
|
||||
"laugh": "يضحك",
|
||||
"like": "رفع الإبهام متمنيا النجاح",
|
||||
"linkToSalesforce": "ارتباط إلى Salesforce",
|
||||
"lobbyButton": "فعِّل/عطِّل وضع غرفة الانتظار",
|
||||
"localRecording": "اظهِر/اخفِ التحكم بالتسجيل المحلي",
|
||||
"lockRoom": "اظهِر/اخفِ كلمة مرور المُلتقى",
|
||||
@@ -1041,6 +1086,7 @@
|
||||
"exitFullScreen": "أدخل عنوان العرض",
|
||||
"exitTileView": "اخرج من وضع الشاشة الكاملة",
|
||||
"feedback": "أبدِ رأيك",
|
||||
"giphy": "تبديل قائمة GIPHY",
|
||||
"hangup": "غادر",
|
||||
"help": "مساعدة",
|
||||
"invite": "ادعُ أحدًا",
|
||||
@@ -1048,6 +1094,7 @@
|
||||
"laugh": "يضحك",
|
||||
"leaveBreakoutRoom": "اترك إلى غرفة الجانبية",
|
||||
"like": "رفع الإبهام متمنيا النجاح",
|
||||
"linkToSalesforce": "ارتباط إلى Salesforce",
|
||||
"lobbyButtonDisable": "عطِّل وضع غرفة الانتظار",
|
||||
"lobbyButtonEnable": "فعِّل وضع غرفة الانتظار",
|
||||
"login": "ادخل",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"enter": "Entrez dans le salon",
|
||||
"error": "Erreur : votre message n'a pas été envoyé. Raison : {{error}}",
|
||||
"fieldPlaceHolder": "Tapez votre message ici",
|
||||
"lobbyChatMessageTo": "Message de salle d'attente à {{recipient}}",
|
||||
"message": "Message",
|
||||
"messageAccessibleTitle": "{{user}} dit: ",
|
||||
"messageAccessibleTitleMe": "Je dis: ",
|
||||
@@ -215,6 +216,8 @@
|
||||
"liveStreaming": "Diffusion en direct"
|
||||
},
|
||||
"add": "Ajouter",
|
||||
"addMeetingNote": "Ajouter une note à cette conférence",
|
||||
"addOptionalNote": "Ajouter une note (optionnel):",
|
||||
"allow": "Autoriser",
|
||||
"alreadySharedVideoMsg": "Un autre participant est en train de partager sa vidéo. Cette conférence ne permet de partager qu'une seule vidéo à la fois.",
|
||||
"alreadySharedVideoTitle": "Une seule vidéo partagée est autorisée à la fois",
|
||||
@@ -266,6 +269,8 @@
|
||||
"kickParticipantDialog": "Êtes-vous sûr(e) de vouloir expulser ce participant ?",
|
||||
"kickParticipantTitle": "Expulser ce participant ?",
|
||||
"kickTitle": "Oups ! vous avez été expulsé(e) par {{participantDisplayName}}",
|
||||
"linkMeeting": "Relier la conférence",
|
||||
"linkMeetingTitle": "Relier la conférence à Salesforce",
|
||||
"liveStreaming": "Direct",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible durant l'enregistrement",
|
||||
"liveStreamingDisabledTooltip": "La diffusion en direct est désactivée",
|
||||
@@ -320,6 +325,7 @@
|
||||
"popupError": "Votre navigateur bloque les fenêtres pop-up. Veuillez autoriser les fenêtres pop-up dans les paramètres de votre navigateur.",
|
||||
"popupErrorTitle": "Pop-up bloquée",
|
||||
"readMore": "plus",
|
||||
"recentlyUsedObjects": "Vos objets récemment utilisés",
|
||||
"recording": "Enregistrement",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible durant le direct",
|
||||
"recordingDisabledTooltip": "L'enregistrement est désactivé.",
|
||||
@@ -342,6 +348,12 @@
|
||||
"screenSharingFailed": "Houla ! Quelque chose s'est mal passé, nous n'avons pas pu démarrer le partage d'écran !",
|
||||
"screenSharingFailedTitle": "Echec du partage d'écran !",
|
||||
"screenSharingPermissionDeniedError": "Houla ! Un problème est survenu avec vos autorisations de partage d'écran. Veuillez réessayer.",
|
||||
"searchInSalesforce": "Rechercher dans Salesforce",
|
||||
"searchResults": "Résultats de recherche({{count}})",
|
||||
"searchResultsDetailsError": "Un problème est survenu en récupérant les données du propriétaire.",
|
||||
"searchResultsError": "Un problème est survenu en récupérant des données.",
|
||||
"searchResultsNotFound": "Aucun résultat trouvé.",
|
||||
"searchResultsTryAgain": "Essayer d'utiliser d'autres mots clé.",
|
||||
"sendPrivateMessage": "Vous avez récemment reçu un message privé. Aviez-vous l'intention d'y répondre en privé, ou vouliez-vous envoyer votre message au groupe ?",
|
||||
"sendPrivateMessageCancel": "Envoyer au groupe",
|
||||
"sendPrivateMessageOk": "Envoyer en privé",
|
||||
@@ -409,6 +421,10 @@
|
||||
"veryBad": "Très mauvais",
|
||||
"veryGood": "Très bon"
|
||||
},
|
||||
"giphy": {
|
||||
"noResults": "Aucun résultat de recherche :(",
|
||||
"search": "Rechercher dans GIPHY"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Centre d'aide"
|
||||
},
|
||||
@@ -475,6 +491,7 @@
|
||||
"focusLocal": "Épingler ma vidéo",
|
||||
"focusRemote": "Épingler la vidéo de quelqu'un d'autre",
|
||||
"fullScreen": "Activer / Désactiver le mode plein écran",
|
||||
"giphyMenu": "Activer/désactiver le menu GIPHY",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"localRecording": "Afficher / Masquer les commandes de l'enregistrement local",
|
||||
"mute": "Activer / Couper le microphone",
|
||||
@@ -529,6 +546,7 @@
|
||||
"admitAll": "Tout accepter",
|
||||
"allow": "Autoriser",
|
||||
"backToKnockModeButton": "Aucun mot de passe, demander à rejoindre plutôt",
|
||||
"chat": "Chat",
|
||||
"dialogTitle": "Mode salle d'attente",
|
||||
"disableDialogContent": "Le mode salle d'attente est actuellement activé. Cette fonctionnalité garantit que les participants indésirables ne peuvent pas rejoindre votre réunion. Souhaitez-vous la désactiver ?",
|
||||
"disableDialogSubmit": "Désactiver",
|
||||
@@ -549,6 +567,8 @@
|
||||
"knockButton": "Demander à rejoindre",
|
||||
"knockTitle": "Quelqu'un souhaite rejoindre la réunion",
|
||||
"knockingParticipantList": "Liste des participants en attente",
|
||||
"lobbyChatStartedNotification": "Un modérateur dialogue en salle d'attente avec {{attendee}}",
|
||||
"lobbyChatStartedTitle": "Un modérateur dialogue en salle d'attente avec vous.",
|
||||
"nameField": "Saisissez votre nom",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} a été refusé par {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} a été accepté par {{originParticipantName}}",
|
||||
@@ -620,6 +640,12 @@
|
||||
"leftOneMember": "{{name}} a quitté la réunion",
|
||||
"leftThreePlusMembers": "{{name}} et beaucoup d'autres ont quitté la réunion",
|
||||
"leftTwoMembers": "{{first}} et {{second}} ont quitté la réunion",
|
||||
"linkToSalesforce": "Lien à Salesforce",
|
||||
"linkToSalesforceDescription": "Vous pouvez lier le résumé de la conférence à un objet Salesforce.",
|
||||
"linkToSalesforceError": "Impossible de relier la conférence à Salesforce",
|
||||
"linkToSalesforceKey": "Relier cette conférence",
|
||||
"linkToSalesforceProgress": "Liaison de la conférence à Salesforce...",
|
||||
"linkToSalesforceSuccess": "La conférence a été reliée à Salesforce",
|
||||
"me": "Moi",
|
||||
"moderationInEffectCSDescription": "Merci de lever la main si vous voulez partager votre écran.",
|
||||
"moderationInEffectCSTitle": "Le partage d'écran est interdit par le modérateur",
|
||||
@@ -813,6 +839,18 @@
|
||||
},
|
||||
"raisedHand": "Aimerait prendre la parole",
|
||||
"raisedHandsLabel": "Nombre de mains levées",
|
||||
"record": {
|
||||
"already": {
|
||||
"linked": "L'enregistrement est déjà relié à cette session."
|
||||
},
|
||||
"type": {
|
||||
"account": "Compte",
|
||||
"contact": "Contact",
|
||||
"lead": "Piste",
|
||||
"opportunity": "Opportunité",
|
||||
"owner": "Propriétaire"
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"authDropboxText": "Téléchargement vers Dropbox",
|
||||
"availableSpace": "Espace disponible : {{spaceLeft}} Mo (approximativement {{duration}} minutes d'enregistrement)",
|
||||
@@ -841,6 +879,7 @@
|
||||
"rec": "REC",
|
||||
"serviceDescription": "Votre enregistrement sera enregistré par le service dédié",
|
||||
"serviceDescriptionCloud": "Enregistrement Cloud",
|
||||
"serviceDescriptionCloudInfo": "Les conférences enregistrées sont automatiquement supprimées 24h après leur heure d'enregistrement.",
|
||||
"serviceName": "Service d'enregistrement",
|
||||
"sessionAlreadyActive": "Cette session est déjà en cours d'enregistrement ou de diffusion.",
|
||||
"signIn": "Se connecter",
|
||||
@@ -973,6 +1012,7 @@
|
||||
"expand": "Développer",
|
||||
"feedback": "Laisser des commentaires",
|
||||
"fullScreen": "Activer / Désactiver le plein écran",
|
||||
"giphy": "Activer/désactiver le menu GIPHY",
|
||||
"grantModerator": "donner des droits de modérateur",
|
||||
"hangup": "Quitter la conversation",
|
||||
"help": "Aide",
|
||||
@@ -980,6 +1020,7 @@
|
||||
"kick": "Expulser le participant",
|
||||
"laugh": "Rire",
|
||||
"like": "Approuver",
|
||||
"linkToSalesforce": "Lien à Salesforce",
|
||||
"lobbyButton": "Activer / Désactiver le mode salle d'attente",
|
||||
"localRecording": "Activer / Désactiver les contrôles d'enregistrement local",
|
||||
"lockRoom": "Activer / Désactiver le mot de passe de la réunion",
|
||||
@@ -1041,6 +1082,7 @@
|
||||
"exitFullScreen": "Quitter le mode plein écran",
|
||||
"exitTileView": "Quitter le mode mosaïque",
|
||||
"feedback": "Laisser des commentaires",
|
||||
"giphy": "Activer/désactiver le menu GIPHY",
|
||||
"hangup": "Quitter",
|
||||
"help": "Aide",
|
||||
"invite": "Inviter des participants",
|
||||
@@ -1048,6 +1090,7 @@
|
||||
"laugh": "Rire",
|
||||
"leaveBreakoutRoom": "Quitter salle annexe",
|
||||
"like": "Approuver",
|
||||
"linkToSalesforce": "Lien à Salesforce",
|
||||
"lobbyButtonDisable": "Désactiver le mode salle d'attente / contrôle des participant(e)s",
|
||||
"lobbyButtonEnable": "Activer le mode salle d'attente / contrôle des participant(e)s",
|
||||
"login": "Connexion",
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "Low bandwidth"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "Meeting ended."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"actions": {
|
||||
"add": "Add breakout room",
|
||||
@@ -865,6 +862,7 @@
|
||||
"expandedPending": "Recording is being started...",
|
||||
"failedToStart": "Recording failed to start",
|
||||
"fileSharingdescription": "Share the recording link with the meeting participants",
|
||||
"highlight": "Highlight",
|
||||
"highlightMoment": "Highlight moment",
|
||||
"highlightMomentDisabled": "You can highlight moments when the recording starts",
|
||||
"highlightMomentSuccess": "Moment highlighted",
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -73,7 +73,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1389.0.0+313e0dd3/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
|
||||
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -113,7 +113,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/jitsi/react-native-video#4f6dad990d17ce42894df993780b5386a9c11b85",
|
||||
"react-native-watch-connectivity": "1.0.4",
|
||||
"react-native-webrtc": "1.98.0",
|
||||
"react-native-webrtc": "1.94.2",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -11789,8 +11789,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1389.0.0+313e0dd3/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-+SDQ2xqBg1eO0b6vQCm+Lqxff9M+/SLK5LGg05dFDaZ3ih94yZ9v2qcSfZxD1pu/QPok4ioV48n/FeRFNNRByQ==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-1xMOjZ2hAwNiDm3UDgfGNoT8RwhCQJBYH1VjGvwmbDJTH5PC9WX89uZpBKoyD012GK5MdJv3oLrEr3g9tndfWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
@@ -15696,9 +15696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "1.98.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.98.0.tgz",
|
||||
"integrity": "sha512-EAaTybkLg7WXU6m75vck+3FPZcjyzSTz8wjm0qjDXWJEVXFSe8cI5AAzqfMEzWOw49LQagy3RayGZejQozvkeg==",
|
||||
"version": "1.94.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.2.tgz",
|
||||
"integrity": "sha512-8wf7CDLYmrxxvta3XxZbmtNrxUblE70fDpoxjYKWfEa48Y4FnFw7MF61g8qYo+V0adoOUNc3j4fIgVnAMghJ9Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
@@ -28784,8 +28784,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1389.0.0+313e0dd3/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-+SDQ2xqBg1eO0b6vQCm+Lqxff9M+/SLK5LGg05dFDaZ3ih94yZ9v2qcSfZxD1pu/QPok4ioV48n/FeRFNNRByQ==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-1xMOjZ2hAwNiDm3UDgfGNoT8RwhCQJBYH1VjGvwmbDJTH5PC9WX89uZpBKoyD012GK5MdJv3oLrEr3g9tndfWw==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
@@ -31767,9 +31767,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "1.98.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.98.0.tgz",
|
||||
"integrity": "sha512-EAaTybkLg7WXU6m75vck+3FPZcjyzSTz8wjm0qjDXWJEVXFSe8cI5AAzqfMEzWOw49LQagy3RayGZejQozvkeg==",
|
||||
"version": "1.94.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.2.tgz",
|
||||
"integrity": "sha512-8wf7CDLYmrxxvta3XxZbmtNrxUblE70fDpoxjYKWfEa48Y4FnFw7MF61g8qYo+V0adoOUNc3j4fIgVnAMghJ9Q==",
|
||||
"requires": {
|
||||
"base64-js": "1.5.1",
|
||||
"event-target-shim": "6.0.2",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1389.0.0+313e0dd3/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
|
||||
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -118,7 +118,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/jitsi/react-native-video#4f6dad990d17ce42894df993780b5386a9c11b85",
|
||||
"react-native-watch-connectivity": "1.0.4",
|
||||
"react-native-webrtc": "1.98.0",
|
||||
"react-native-webrtc": "1.94.2",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
diff --git a/node_modules/react-native-dialog/lib/Button.js b/node_modules/react-native-dialog/lib/Button.js
|
||||
index 19eeb22..b8a66f4 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Button.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Button.js
|
||||
@@ -50,7 +50,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "link_text_dark" : "link_text_dark_light"}`),
|
||||
+ color: isDark ? '#BFC7C7C7' : '#BF727272',
|
||||
textAlign: "center",
|
||||
backgroundColor: "transparent",
|
||||
padding: 8,
|
||||
diff --git a/node_modules/react-native-dialog/lib/CodeInput.js b/node_modules/react-native-dialog/lib/CodeInput.js
|
||||
index eceae56..cc4339d 100644
|
||||
--- a/node_modules/react-native-dialog/lib/CodeInput.js
|
||||
+++ b/node_modules/react-native-dialog/lib/CodeInput.js
|
||||
@@ -97,7 +97,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
color: PlatformColor("label"),
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
fontSize: 20,
|
||||
},
|
||||
default: {},
|
||||
@@ -107,7 +107,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
color: PlatformColor("label"),
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
fontSize: 14,
|
||||
},
|
||||
default: {},
|
||||
diff --git a/node_modules/react-native-dialog/lib/Container.js b/node_modules/react-native-dialog/lib/Container.js
|
||||
index 69e3764..109126f 100644
|
||||
index 69e3764..d7569fb 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Container.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Container.js
|
||||
@@ -82,7 +82,7 @@ DialogContainer.propTypes = {
|
||||
@@ -16,7 +51,68 @@ index 69e3764..109126f 100644
|
||||
},
|
||||
android: {
|
||||
- backgroundColor: PlatformColor("?attr/colorBackgroundFloating"),
|
||||
+ backgroundColor: PlatformColor(`@android:color/${isDark ? "background_dark" : "background_light"}`),
|
||||
+ backgroundColor: isDark ? '#212121' : '#FFFFFF',
|
||||
flexDirection: "column",
|
||||
borderRadius: 3,
|
||||
padding: 16,
|
||||
diff --git a/node_modules/react-native-dialog/lib/Description.js b/node_modules/react-native-dialog/lib/Description.js
|
||||
index 2da9ed3..248ac2f 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Description.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Description.js
|
||||
@@ -28,7 +28,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
marginTop: 4,
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "secondary_text_dark" : "secondary_text_light"}`),
|
||||
+ color: isDark ? '#C7C7C7' : '#727272',
|
||||
fontSize: 16,
|
||||
marginTop: 10,
|
||||
},
|
||||
diff --git a/node_modules/react-native-dialog/lib/Input.js b/node_modules/react-native-dialog/lib/Input.js
|
||||
index b33a1a0..063d7f8 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Input.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Input.js
|
||||
@@ -48,7 +48,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
color: PlatformColor("label"),
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
fontSize: 14,
|
||||
},
|
||||
default: {},
|
||||
@@ -58,7 +58,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
color: PlatformColor("label"),
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
marginLeft: -4,
|
||||
paddingLeft: 4,
|
||||
},
|
||||
diff --git a/node_modules/react-native-dialog/lib/Switch.js b/node_modules/react-native-dialog/lib/Switch.js
|
||||
index 26a05ca..05114fa 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Switch.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Switch.js
|
||||
@@ -52,7 +52,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
flex: 1,
|
||||
paddingRight: 8,
|
||||
fontSize: 16,
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
},
|
||||
default: {},
|
||||
}),
|
||||
diff --git a/node_modules/react-native-dialog/lib/Title.js b/node_modules/react-native-dialog/lib/Title.js
|
||||
index 1c6fd87..b5511cc 100644
|
||||
--- a/node_modules/react-native-dialog/lib/Title.js
|
||||
+++ b/node_modules/react-native-dialog/lib/Title.js
|
||||
@@ -28,7 +28,7 @@ const buildStyles = (isDark) => StyleSheet.create({
|
||||
fontWeight: "600",
|
||||
},
|
||||
android: {
|
||||
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
|
||||
+ color: isDark ? '#FAFAFA' : '#212121',
|
||||
fontWeight: "500",
|
||||
fontSize: 18,
|
||||
},
|
||||
|
||||
88
react/features/app/actions.any.js
Normal file
88
react/features/app/actions.any.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getLocationContextRoot } from '../base/util';
|
||||
|
||||
import { addTrackStateToURL } from './functions.any';
|
||||
|
||||
/**
|
||||
* Redirects to another page generated by replacing the path in the original URL
|
||||
* with the given path.
|
||||
*
|
||||
* @param {(string)} pathname - The path to navigate to.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectWithStoredParams(pathname: string) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
const newLocationURL = new URL(locationURL.href);
|
||||
|
||||
newLocationURL.pathname = pathname;
|
||||
window.location.assign(newLocationURL.toString());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a specific pathname to window.location.pathname taking into account
|
||||
* the context root of the Web app.
|
||||
*
|
||||
* @param {string} pathname - The pathname to assign to
|
||||
* window.location.pathname. If the specified pathname is relative, the context
|
||||
* root of the Web app will be prepended to the specified pathname before
|
||||
* assigning it to window.location.pathname.
|
||||
* @param {string} hashParam - Optional hash param to assign to
|
||||
* window.location.hash.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToStaticPage(pathname: string, hashParam: ?string) {
|
||||
return () => {
|
||||
const windowLocation = window.location;
|
||||
let newPathname = pathname;
|
||||
|
||||
if (!newPathname.startsWith('/')) {
|
||||
// A pathname equal to ./ specifies the current directory. It will be
|
||||
// fine but pointless to include it because contextRoot is the current
|
||||
// directory.
|
||||
newPathname.startsWith('./')
|
||||
&& (newPathname = newPathname.substring(2));
|
||||
newPathname = getLocationContextRoot(windowLocation) + newPathname;
|
||||
}
|
||||
|
||||
if (hashParam) {
|
||||
windowLocation.hash = hashParam;
|
||||
}
|
||||
|
||||
windowLocation.pathname = newPathname;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page by restoring the original URL.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadWithStoredParams() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
// Preserve the local tracks muted states.
|
||||
const newURL = addTrackStateToURL(locationURL, state);
|
||||
const windowLocation = window.location;
|
||||
const oldSearchString = windowLocation.search;
|
||||
|
||||
windowLocation.replace(newURL.toString());
|
||||
|
||||
if (newURL.search === oldSearchString) {
|
||||
// NOTE: Assuming that only the hash or search part of the URL will
|
||||
// be changed!
|
||||
// location.replace will not trigger redirect/reload when
|
||||
// only the hash params are changed. That's why we need to call
|
||||
// reload in addition to replace.
|
||||
windowLocation.reload();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
154
react/features/app/actions.native.js
Normal file
154
react/features/app/actions.native.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { setRoom } from '../base/conference';
|
||||
import {
|
||||
configWillLoad,
|
||||
createFakeConfig,
|
||||
loadConfigError,
|
||||
restoreConfig,
|
||||
setConfig,
|
||||
storeConfig
|
||||
} from '../base/config';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { createDesiredLocalTracks } from '../base/tracks';
|
||||
import {
|
||||
getBackendSafeRoomName,
|
||||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import { getDefaultURL } from './functions';
|
||||
import { addTrackStateToURL } from './functions.native';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Triggers an in-app navigation to a specific route. Allows navigation to be
|
||||
* abstracted between the mobile/React Native and Web/React applications.
|
||||
*
|
||||
* @param {string|undefined} uri - The URI to which to navigate. It may be a
|
||||
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
|
||||
* scheme, or a mere room name.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function appNavigate(uri: ?string) {
|
||||
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||
let location = parseURIString(uri);
|
||||
|
||||
// If the specified location (URI) does not identify a host, use the app's
|
||||
// default.
|
||||
if (!location || !location.host) {
|
||||
const defaultLocation = parseURIString(getDefaultURL(getState));
|
||||
|
||||
if (location) {
|
||||
location.host = defaultLocation.host;
|
||||
|
||||
// FIXME Turn location's host, hostname, and port properties into
|
||||
// setters in order to reduce the risks of inconsistent state.
|
||||
location.hostname = defaultLocation.hostname;
|
||||
location.pathname
|
||||
= defaultLocation.pathname + location.pathname.substr(1);
|
||||
location.port = defaultLocation.port;
|
||||
location.protocol = defaultLocation.protocol;
|
||||
} else {
|
||||
location = defaultLocation;
|
||||
}
|
||||
}
|
||||
|
||||
location.protocol || (location.protocol = 'https:');
|
||||
const { contextRoot, host, room } = location;
|
||||
const locationURL = new URL(location.toString());
|
||||
|
||||
if (room) {
|
||||
navigateRoot(screen.connecting);
|
||||
}
|
||||
|
||||
dispatch(disconnect());
|
||||
|
||||
dispatch(configWillLoad(locationURL, room));
|
||||
|
||||
let protocol = location.protocol.toLowerCase();
|
||||
|
||||
// The React Native app supports an app-specific scheme which is sure to not
|
||||
// be supported by fetch.
|
||||
protocol !== 'http:' && protocol !== 'https:' && (protocol = 'https:');
|
||||
|
||||
const baseURL = `${protocol}//${host}${contextRoot || '/'}`;
|
||||
let url = `${baseURL}config.js`;
|
||||
|
||||
// XXX In order to support multiple shards, tell the room to the deployment.
|
||||
room && (url += `?room=${getBackendSafeRoomName(room)}`);
|
||||
|
||||
let config;
|
||||
|
||||
// Avoid (re)loading the config when there is no room.
|
||||
if (!room) {
|
||||
config = restoreConfig(baseURL);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
try {
|
||||
config = await loadConfig(url);
|
||||
dispatch(storeConfig(baseURL, config));
|
||||
} catch (error) {
|
||||
config = restoreConfig(baseURL);
|
||||
|
||||
if (!config) {
|
||||
if (room) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no room (we are on the welcome page), don't fail, just create a fake one.
|
||||
logger.warn('Failed to load config but there is no room, applying a fake one');
|
||||
config = createFakeConfig(baseURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getState()['features/base/config'].locationURL !== locationURL) {
|
||||
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
|
||||
if (room) {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: Dispatch<Function>, getState: Function) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
// Preserve the local tracks muted state after the reload.
|
||||
const newURL = addTrackStateToURL(locationURL, state);
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
dispatch(appNavigate(toURLString(newURL)));
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import { API_ID } from '../../../modules/API';
|
||||
import { setRoom } from '../base/conference';
|
||||
import {
|
||||
configWillLoad,
|
||||
@@ -12,30 +12,33 @@ import {
|
||||
setConfig,
|
||||
storeConfig
|
||||
} from '../base/config';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import { toState } from '../base/redux';
|
||||
import { createDesiredLocalTracks, isLocalCameraTrackMuted, isLocalTrackMuted } from '../base/tracks';
|
||||
import {
|
||||
addHashParamsToURL,
|
||||
getBackendSafeRoomName,
|
||||
getLocationContextRoot,
|
||||
parseURIString,
|
||||
toURLString
|
||||
parseURIString
|
||||
} from '../base/util';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, clearNotifications, showNotification } from '../notifications';
|
||||
import {
|
||||
clearNotifications,
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
showNotification
|
||||
} from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import {
|
||||
getDefaultURL,
|
||||
getName
|
||||
} from './functions';
|
||||
redirectToStaticPage,
|
||||
redirectWithStoredParams,
|
||||
reloadWithStoredParams
|
||||
} from './actions.any';
|
||||
import { getDefaultURL, getName } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
|
||||
/**
|
||||
* Triggers an in-app navigation to a specific route. Allows navigation to be
|
||||
@@ -74,12 +77,6 @@ export function appNavigate(uri: ?string) {
|
||||
const { contextRoot, host, room } = location;
|
||||
const locationURL = new URL(location.toString());
|
||||
|
||||
// Disconnect from any current conference.
|
||||
// FIXME: unify with web.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(disconnect());
|
||||
}
|
||||
|
||||
// There are notifications now that gets displayed after we technically left
|
||||
// the conference, but we're still on the conference screen.
|
||||
dispatch(clearNotifications());
|
||||
@@ -135,137 +132,6 @@ export function appNavigate(uri: ?string) {
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
|
||||
// FIXME: unify with web, currently the connection and track creation happens in conference.js.
|
||||
if (room && navigator.product === 'ReactNative') {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to another page generated by replacing the path in the original URL
|
||||
* with the given path.
|
||||
*
|
||||
* @param {(string)} pathname - The path to navigate to.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectWithStoredParams(pathname: string) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
const newLocationURL = new URL(locationURL.href);
|
||||
|
||||
newLocationURL.pathname = pathname;
|
||||
window.location.assign(newLocationURL.toString());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a specific pathname to window.location.pathname taking into account
|
||||
* the context root of the Web app.
|
||||
*
|
||||
* @param {string} pathname - The pathname to assign to
|
||||
* window.location.pathname. If the specified pathname is relative, the context
|
||||
* root of the Web app will be prepended to the specified pathname before
|
||||
* assigning it to window.location.pathname.
|
||||
* @param {string} hashParam - Optional hash param to assign to
|
||||
* window.location.hash.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToStaticPage(pathname: string, hashParam: ?string) {
|
||||
return () => {
|
||||
const windowLocation = window.location;
|
||||
let newPathname = pathname;
|
||||
|
||||
if (!newPathname.startsWith('/')) {
|
||||
// A pathname equal to ./ specifies the current directory. It will be
|
||||
// fine but pointless to include it because contextRoot is the current
|
||||
// directory.
|
||||
newPathname.startsWith('./')
|
||||
&& (newPathname = newPathname.substring(2));
|
||||
newPathname = getLocationContextRoot(windowLocation) + newPathname;
|
||||
}
|
||||
|
||||
if (hashParam) {
|
||||
windowLocation.hash = hashParam;
|
||||
}
|
||||
|
||||
windowLocation.pathname = newPathname;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: Dispatch<Function>, getState: Function) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
// Preserve the local tracks muted state after the reload.
|
||||
const newURL = addTrackStateToURL(locationURL, state);
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(appNavigate(toURLString(newURL)));
|
||||
} else {
|
||||
dispatch(reloadWithStoredParams());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current track state to the passed URL.
|
||||
*
|
||||
* @param {URL} url - The URL that will be modified.
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState} function.
|
||||
* @returns {URL} - Returns the modified URL.
|
||||
*/
|
||||
function addTrackStateToURL(url, stateful) {
|
||||
const state = toState(stateful);
|
||||
const tracks = state['features/base/tracks'];
|
||||
const isVideoMuted = isLocalCameraTrackMuted(tracks);
|
||||
const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
|
||||
return addHashParamsToURL(new URL(url), { // use new URL object in order to not pollute the passed parameter.
|
||||
'config.startWithAudioMuted': isAudioMuted,
|
||||
'config.startWithVideoMuted': isVideoMuted
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page by restoring the original URL.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadWithStoredParams() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
// Preserve the local tracks muted states.
|
||||
const newURL = addTrackStateToURL(locationURL, state);
|
||||
const windowLocation = window.location;
|
||||
const oldSearchString = windowLocation.search;
|
||||
|
||||
windowLocation.replace(newURL.toString());
|
||||
|
||||
if (newURL.search === oldSearchString) {
|
||||
// NOTE: Assuming that only the hash or search part of the URL will
|
||||
// be changed!
|
||||
// location.replace will not trigger redirect/reload when
|
||||
// only the hash params are changed. That's why we need to call
|
||||
// reload in addition to replace.
|
||||
windowLocation.reload();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -341,3 +207,22 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: Dispatch<Function>, getState: Function) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const state = getState();
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
dispatch(reloadWithStoredParams());
|
||||
};
|
||||
}
|
||||
24
react/features/app/functions.any.js
Normal file
24
react/features/app/functions.any.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import { toState } from '../base/redux';
|
||||
import { isLocalCameraTrackMuted, isLocalTrackMuted } from '../base/tracks';
|
||||
import { addHashParamsToURL } from '../base/util';
|
||||
|
||||
/**
|
||||
* Adds the current track state to the passed URL.
|
||||
*
|
||||
* @param {URL} url - The URL that will be modified.
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState} function.
|
||||
* @returns {URL} - Returns the modified URL.
|
||||
*/
|
||||
export function addTrackStateToURL(url, stateful) {
|
||||
const state = toState(stateful);
|
||||
const tracks = state['features/base/tracks'];
|
||||
const isVideoMuted = isLocalCameraTrackMuted(tracks);
|
||||
const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
|
||||
return addHashParamsToURL(new URL(url), { // use new URL object in order to not pollute the passed parameter.
|
||||
'config.startWithAudioMuted': isAudioMuted,
|
||||
'config.startWithVideoMuted': isVideoMuted
|
||||
});
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { NativeModules } from 'react-native';
|
||||
import { toState } from '../base/redux';
|
||||
import { getServerURL } from '../base/settings';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Retrieves the default URL for the app. This can either come from a prop to
|
||||
* the root App component or be configured in the settings.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { toState } from '../base/redux';
|
||||
import { getServerURL } from '../base/settings';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -26,6 +26,7 @@ export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
|
||||
|
||||
export const MODERATION_NOTIFICATIONS = {
|
||||
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.SCREENSHARE]: CS_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.PRESENTER]: CS_MODERATION_NOTIFICATION_ID
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorPalette } from '../../../styles';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
const DEFAULT_SIZE = 65;
|
||||
|
||||
@@ -34,16 +35,16 @@ export default {
|
||||
|
||||
switch (status) {
|
||||
case 'available':
|
||||
color = 'rgb(110, 176, 5)';
|
||||
color = PRESENCE_AVAILABLE_COLOR;
|
||||
break;
|
||||
case 'away':
|
||||
color = 'rgb(250, 201, 20)';
|
||||
color = PRESENCE_AWAY_COLOR;
|
||||
break;
|
||||
case 'busy':
|
||||
color = 'rgb(233, 0, 27)';
|
||||
color = PRESENCE_BUSY_COLOR;
|
||||
break;
|
||||
case 'idle':
|
||||
color = 'rgb(172, 172, 172)';
|
||||
color = PRESENCE_IDLE_COLOR;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
5
react/features/base/avatar/components/styles.js
Normal file
5
react/features/base/avatar/components/styles.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// Colors for avatar status badge
|
||||
export const PRESENCE_AVAILABLE_COLOR = 'rgb(110, 176, 5)';
|
||||
export const PRESENCE_AWAY_COLOR = 'rgb(250, 201, 20)';
|
||||
export const PRESENCE_BUSY_COLOR = 'rgb(233, 0, 27)';
|
||||
export const PRESENCE_IDLE_COLOR = 'rgb(172, 172, 172)';
|
||||
@@ -1,12 +1,20 @@
|
||||
// @flow
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '../../../icons';
|
||||
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: Object,
|
||||
|
||||
/**
|
||||
* External class name passed through props.
|
||||
*/
|
||||
@@ -38,11 +46,77 @@ type Props = AbstractProps & {
|
||||
useCORS?: ?boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = () => {
|
||||
return {
|
||||
avatar: {
|
||||
backgroundColor: '#AAA',
|
||||
borderRadius: '50%',
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
fontWeight: '100',
|
||||
objectFit: 'cover',
|
||||
|
||||
'&.avatar-small': {
|
||||
height: '28px !important',
|
||||
width: '28px !important'
|
||||
},
|
||||
|
||||
'&.avatar-xsmall': {
|
||||
height: '16px !important',
|
||||
width: '16px !important'
|
||||
},
|
||||
|
||||
'& .jitsi-icon': {
|
||||
transform: 'translateY(50%)'
|
||||
},
|
||||
|
||||
'& .avatar-svg': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}
|
||||
},
|
||||
|
||||
badge: {
|
||||
position: 'relative',
|
||||
|
||||
'&.avatar-badge:after': {
|
||||
borderRadius: '50%',
|
||||
content: '""',
|
||||
display: 'block',
|
||||
height: '35%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '35%'
|
||||
},
|
||||
|
||||
'&.avatar-badge-available:after': {
|
||||
backgroundColor: PRESENCE_AVAILABLE_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-away:after': {
|
||||
backgroundColor: PRESENCE_AWAY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-busy:after': {
|
||||
backgroundColor: PRESENCE_BUSY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-idle:after': {
|
||||
backgroundColor: PRESENCE_IDLE_COLOR
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
||||
* props.
|
||||
*/
|
||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
@@ -66,7 +140,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
if (this._isIcon(url)) {
|
||||
return (
|
||||
<div
|
||||
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
|
||||
className = { clsx(this._getAvatarClassName(), this._getBadgeClassName()) }
|
||||
data-testid = { this.props.testId }
|
||||
id = { this.props.id }
|
||||
style = { this._getAvatarStyle(this.props.color) }>
|
||||
@@ -96,7 +170,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
if (initials) {
|
||||
return (
|
||||
<div
|
||||
className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
|
||||
className = { clsx(this._getAvatarClassName(), this._getBadgeClassName()) }
|
||||
data-testid = { this.props.testId }
|
||||
id = { this.props.id }
|
||||
style = { this._getAvatarStyle(this.props.color) }>
|
||||
@@ -157,7 +231,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
* @returns {string}
|
||||
*/
|
||||
_getAvatarClassName(additional) {
|
||||
return `avatar ${additional || ''} ${this.props.className || ''}`;
|
||||
return clsx('avatar', additional, this.props.className, this.props.classes.avatar);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +243,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
const { status } = this.props;
|
||||
|
||||
if (status) {
|
||||
return `avatar-badge avatar-badge-${status}`;
|
||||
return clsx('avatar-badge', `avatar-badge-${status}`, this.props.classes.badge);
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -192,3 +266,5 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(StatelessAvatar);
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
// @flow
|
||||
|
||||
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
|
||||
import { showNotification, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications';
|
||||
import { setSkipPrejoinOnReload } from '../../prejoin';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share';
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { setAudioOnly } from '../audio-only';
|
||||
import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any';
|
||||
import { JitsiConferenceErrors, JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, setScreenshareMuted, VIDEO_TYPE } from '../media';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import {
|
||||
addLocalTrack,
|
||||
createLocalTracksF,
|
||||
getLocalDesktopTrack,
|
||||
getLocalJitsiAudioTrack,
|
||||
replaceLocalTrack,
|
||||
TOGGLE_SCREENSHARING
|
||||
} from '../tracks';
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from './actionTypes';
|
||||
import { getCurrentConference } from './functions';
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
||||
switch (action.type) {
|
||||
@@ -25,7 +42,153 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case TOGGLE_SCREENSHARING: {
|
||||
getMultipleVideoSupportFeatureFlag(getState()) && _toggleScreenSharing(action, store);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays a UI notification for screensharing failure based on the error passed.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} error - The error.
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleScreensharingError(error, { dispatch }) {
|
||||
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
|
||||
return;
|
||||
}
|
||||
let descriptionKey, titleKey;
|
||||
|
||||
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
|
||||
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
|
||||
descriptionKey = 'dialog.cameraConstraintFailedError';
|
||||
titleKey = 'deviceError.cameraError';
|
||||
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
|
||||
descriptionKey = 'dialog.screenSharingFailed';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
|
||||
descriptionKey = 'notify.screenShareNoAudio';
|
||||
titleKey = 'notify.screenShareNoAudioTitle';
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey,
|
||||
descriptionKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop
|
||||
* audio track is added to the conference.
|
||||
*
|
||||
* @private
|
||||
* @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference.
|
||||
* @param {*} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _maybeApplyAudioMixerEffect(desktopAudioTrack, state) {
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (localAudio) {
|
||||
// If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API.
|
||||
const mixerEffect = new AudioMixerEffect(desktopAudioTrack);
|
||||
|
||||
await localAudio.setEffect(mixerEffect);
|
||||
} else {
|
||||
// If no local stream is present ( i.e. no input audio devices) we use the screen share audio
|
||||
// stream as we would use a regular stream.
|
||||
await conference.replaceTrack(null, desktopAudioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles screen sharing.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _toggleScreenSharing({ enabled, audioOnly = false }, store) {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
|
||||
|
||||
if (enabled) {
|
||||
let tracks;
|
||||
|
||||
try {
|
||||
tracks = await createLocalTracksF({ devices: [ VIDEO_TYPE.DESKTOP ] });
|
||||
} catch (error) {
|
||||
_handleScreensharingError(error, store);
|
||||
|
||||
return;
|
||||
}
|
||||
const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO);
|
||||
const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO);
|
||||
|
||||
// Dispose the desktop track for audio-only screensharing.
|
||||
if (audioOnly) {
|
||||
desktopVideoTrack.dispose();
|
||||
|
||||
if (!desktopAudioTrack) {
|
||||
_handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (desktopVideoTrack) {
|
||||
if (localScreenshare) {
|
||||
await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
|
||||
} else {
|
||||
await dispatch(addLocalTrack(desktopVideoTrack));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
|
||||
// otherwise without unmuting the microphone.
|
||||
if (desktopAudioTrack) {
|
||||
_maybeApplyAudioMixerEffect(desktopAudioTrack, state);
|
||||
dispatch(setScreenshareAudioTrack(desktopAudioTrack));
|
||||
}
|
||||
|
||||
// Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to
|
||||
// audio-only screensharing.
|
||||
const { enabled: bestPerformanceMode } = state['features/base/audio-only'];
|
||||
|
||||
if (bestPerformanceMode && !audioOnly) {
|
||||
dispatch(setAudioOnly(false));
|
||||
}
|
||||
} else {
|
||||
const { desktopAudioTrack } = state['features/screen-share'];
|
||||
|
||||
// Mute the desktop track instead of removing it from the conference since we don't want the client to signal
|
||||
// a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
|
||||
// same sender will be re-used without the need for signaling a new ssrc through source-add.
|
||||
dispatch(setScreenshareMuted(true));
|
||||
if (desktopAudioTrack) {
|
||||
if (localAudio) {
|
||||
localAudio.setEffect(undefined);
|
||||
} else {
|
||||
await conference.replaceTrack(desktopAudioTrack, null);
|
||||
}
|
||||
desktopAudioTrack.dispose();
|
||||
dispatch(setScreenshareAudioTrack(null));
|
||||
}
|
||||
}
|
||||
|
||||
if (audioOnly) {
|
||||
dispatch(setScreenAudioShareState(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ function _authStatusChanged(state, { authEnabled, authLogin }) {
|
||||
*/
|
||||
function _conferenceFailed(state, { conference, error }) {
|
||||
// The current (similar to getCurrentConference in
|
||||
// base/conference/functions.js) conference which is joining or joined:
|
||||
// base/conference/functions.any.js) conference which is joining or joined:
|
||||
const conference_ = state.conference || state.joining;
|
||||
|
||||
if (conference_ && conference_ !== conference) {
|
||||
|
||||
@@ -216,6 +216,7 @@ export default [
|
||||
'testing',
|
||||
'toolbarButtons',
|
||||
'toolbarConfig',
|
||||
'tileView',
|
||||
'transcribingEnabled',
|
||||
'useHostPageLocalStorage',
|
||||
'useTurnUdp',
|
||||
|
||||
@@ -67,5 +67,6 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
|
||||
*/
|
||||
|
||||
export const FEATURE_FLAGS = {
|
||||
MULTIPLE_VIDEO_STREAMS_SUPPORT: 'sendMultipleVideoStreams',
|
||||
SOURCE_NAME_SIGNALING: 'sourceNameSignaling'
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import Bourne from '@hapi/bourne';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { browser } from '../lib-jitsi-meet';
|
||||
import { parseURLParams } from '../util';
|
||||
|
||||
import CONFIG_WHITELIST from './configWhitelist';
|
||||
@@ -12,7 +13,7 @@ import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
|
||||
import logger from './logger';
|
||||
|
||||
// XXX The function getRoomName is split out of
|
||||
// functions.js because it is bundled in both app.bundle and
|
||||
// functions.any.js because it is bundled in both app.bundle and
|
||||
// do_external_connect, webpack 1 does not support tree shaking, and we don't
|
||||
// want all functions to be bundled in do_external_connect.
|
||||
export { default as getRoomName } from './getRoomName';
|
||||
@@ -49,6 +50,18 @@ export function getMeetingRegion(state: Object) {
|
||||
return state['features/base/config']?.deploymentInfo?.region || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the sendMultipleVideoStreams feature flag.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getMultipleVideoSupportFeatureFlag(state: Object) {
|
||||
return getFeatureFlag(state, FEATURE_FLAGS.MULTIPLE_VIDEO_STREAMS_SUPPORT)
|
||||
&& getSourceNameSignalingFeatureFlag(state)
|
||||
&& isUnifiedPlanEnabled(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the sourceNameSignaling feature flag.
|
||||
*
|
||||
@@ -196,6 +209,19 @@ export function isDisplayNameVisible(state: Object): boolean {
|
||||
return !state['features/base/config'].hideDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if Unified plan support is enabled.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUnifiedPlanEnabled(state: Object): boolean {
|
||||
const { enableUnifiedOnChrome = true } = state['features/base/config'];
|
||||
|
||||
return browser.supportsUnifiedPlan()
|
||||
&& (!browser.isChromiumBased() || (browser.isChromiumBased() && enableUnifiedOnChrome));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a Jitsi Meet config.js from {@code localStorage} if it was
|
||||
* previously downloaded from a specific {@code baseURL} and stored with
|
||||
|
||||
@@ -156,7 +156,7 @@ function _connectionWillConnect(
|
||||
}
|
||||
|
||||
/**
|
||||
* The current (similar to getCurrentConference in base/conference/functions.js)
|
||||
* The current (similar to getCurrentConference in base/conference/functions.any.js)
|
||||
* connection which is {@code connection} or {@code connecting}.
|
||||
*
|
||||
* @param {Object} baseConnectionState - The current state of the
|
||||
|
||||
@@ -40,6 +40,16 @@ export const SET_AUDIO_UNMUTE_PERMISSIONS = 'SET_AUDIO_UNMUTE_PERMISSIONS';
|
||||
*/
|
||||
export const SET_CAMERA_FACING_MODE = 'SET_CAMERA_FACING_MODE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to set the muted state of the local screenshare.
|
||||
*
|
||||
* {
|
||||
* type: SET_SCREENSHARE_MUTED,
|
||||
* muted: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_SCREENSHARE_MUTED = 'SET_SCREENSHARE_MUTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to adjust the availability of the local video.
|
||||
*
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SET_AUDIO_AVAILABLE,
|
||||
SET_AUDIO_UNMUTE_PERMISSIONS,
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_SCREENSHARE_MUTED,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
SET_VIDEO_UNMUTE_PERMISSIONS,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
type MediaType,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
} from './constants';
|
||||
|
||||
@@ -92,6 +94,47 @@ export function setCameraFacingMode(cameraFacingMode: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to set the muted state of the local screenshare.
|
||||
*
|
||||
* @param {boolean} muted - True if the local screenshare is to be enabled or false otherwise.
|
||||
* @param {MEDIA_TYPE} mediaType - The type of media.
|
||||
* @param {number} authority - The {@link SCREENSHARE_MUTISM_AUTHORITY} which is muting/unmuting the local screenshare.
|
||||
* @param {boolean} ensureTrack - True if we want to ensure that a new track is created if missing.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setScreenshareMuted(
|
||||
muted: boolean,
|
||||
mediaType: MediaType = MEDIA_TYPE.SCREENSHARE,
|
||||
authority: number = SCREENSHARE_MUTISM_AUTHORITY.USER,
|
||||
ensureTrack: boolean = false) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
|
||||
// check for A/V Moderation when trying to unmute
|
||||
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const oldValue = state['features/base/media'].screenshare.muted;
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const newValue = muted ? oldValue | authority : oldValue & ~authority;
|
||||
|
||||
return dispatch({
|
||||
type: SET_SCREENSHARE_MUTED,
|
||||
authority,
|
||||
mediaType,
|
||||
ensureTrack,
|
||||
muted: newValue
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to adjust the availability of the local video.
|
||||
*
|
||||
|
||||
@@ -10,7 +10,7 @@ export const CAMERA_FACING_MODE = {
|
||||
USER: 'user'
|
||||
};
|
||||
|
||||
export type MediaType = 'audio' | 'video' | 'presenter';
|
||||
export type MediaType = 'audio' | 'video' | 'presenter' | 'screenshare';
|
||||
|
||||
/**
|
||||
* The set of media types.
|
||||
@@ -20,12 +20,23 @@ export type MediaType = 'audio' | 'video' | 'presenter';
|
||||
export const MEDIA_TYPE = {
|
||||
AUDIO: 'audio',
|
||||
PRESENTER: 'presenter',
|
||||
SCREENSHARE: 'screenshare',
|
||||
VIDEO: 'video'
|
||||
};
|
||||
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
/**
|
||||
* The types of authorities which may mute/unmute the local screenshare.
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
export const SCREENSHARE_MUTISM_AUTHORITY = {
|
||||
AUDIO_ONLY: 1 << 0,
|
||||
USER: 1 << 2
|
||||
};
|
||||
|
||||
/**
|
||||
* The types of authorities which may mute/unmute the local video.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ import { isForceMuted } from '../../participants-pane/functions';
|
||||
import { isScreenMediaShared } from '../../screen-share/functions';
|
||||
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
|
||||
import { isRoomValid, SET_ROOM } from '../conference';
|
||||
import { getMultipleVideoSupportFeatureFlag } from '../config';
|
||||
import { getLocalParticipant } from '../participants';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { getPropertyValue } from '../settings';
|
||||
@@ -30,13 +31,20 @@ import {
|
||||
import {
|
||||
SET_AUDIO_MUTED,
|
||||
SET_AUDIO_UNMUTE_PERMISSIONS,
|
||||
SET_SCREENSHARE_MUTED,
|
||||
SET_VIDEO_MUTED,
|
||||
SET_VIDEO_UNMUTE_PERMISSIONS
|
||||
} from './actionTypes';
|
||||
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
|
||||
import {
|
||||
setAudioMuted,
|
||||
setCameraFacingMode,
|
||||
setScreenshareMuted,
|
||||
setVideoMuted
|
||||
} from './actions';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
MEDIA_TYPE,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
} from './constants';
|
||||
import { getStartWithAudioMuted, getStartWithVideoMuted } from './functions';
|
||||
@@ -100,6 +108,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_SCREENSHARE_MUTED: {
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_VIDEO_MUTED: {
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
@@ -167,15 +184,17 @@ function _appStateChanged({ dispatch, getState }, next, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setAudioOnly({ dispatch }, next, action) {
|
||||
function _setAudioOnly({ dispatch, getState }, next, action) {
|
||||
const { audioOnly, ensureVideoTrack } = action;
|
||||
const state = getState();
|
||||
|
||||
sendAnalytics(createTrackMutedEvent('video', 'audio-only mode', audioOnly));
|
||||
|
||||
// Make sure we mute both the desktop and video tracks.
|
||||
dispatch(setVideoMuted(audioOnly, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
if (getMultipleVideoSupportFeatureFlag(state)) {
|
||||
dispatch(setScreenshareMuted(audioOnly, MEDIA_TYPE.SCREENSHARE, SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY));
|
||||
} else if (navigator.product !== 'ReactNative') {
|
||||
dispatch(setVideoMuted(audioOnly, MEDIA_TYPE.PRESENTER, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY, ensureVideoTrack));
|
||||
}
|
||||
|
||||
@@ -292,11 +311,9 @@ function _syncTrackMutedState({ getState }, track) {
|
||||
// fired before track gets to state.
|
||||
if (track.muted !== muted) {
|
||||
sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
|
||||
logger.log(
|
||||
`Sync ${track.mediaType} track muted state to ${
|
||||
muted ? 'muted' : 'unmuted'}`);
|
||||
logger.log(`Sync ${track.mediaType} track muted state to ${muted ? 'muted' : 'unmuted'}`);
|
||||
|
||||
track.muted = muted;
|
||||
setTrackMuted(track.jitsiTrack, muted);
|
||||
setTrackMuted(track.jitsiTrack, muted, state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
SET_AUDIO_MUTED,
|
||||
SET_AUDIO_UNMUTE_PERMISSIONS,
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_SCREENSHARE_MUTED,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
SET_VIDEO_UNMUTE_PERMISSIONS,
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE } from './constants';
|
||||
import { CAMERA_FACING_MODE, SCREENSHARE_MUTISM_AUTHORITY } from './constants';
|
||||
|
||||
/**
|
||||
* Media state object for local audio.
|
||||
@@ -73,6 +74,54 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Media state object for local screenshare.
|
||||
*
|
||||
* @typedef {Object} ScreenshareMediaState
|
||||
* @property {boolean} available=true - Screenshare available state.
|
||||
* @property {boolean} muted=true - Screenshare muted state.
|
||||
* @property {boolean} unmuteBlocked=false - Screenshare unmute blocked state.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initial state for video.
|
||||
*
|
||||
* @type {ScreenshareMediaState}
|
||||
*/
|
||||
export const _SCREENSHARE_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
muted: SCREENSHARE_MUTISM_AUTHORITY.USER,
|
||||
unmuteBlocked: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for screenshare media state.
|
||||
*
|
||||
* @param {VideoMediaState} state - Media state of local screenshare.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @private
|
||||
* @returns {ScreenshareMediaState}
|
||||
*/
|
||||
function _screenshare(state = _SCREENSHARE_INITIAL_MEDIA_STATE, action) {
|
||||
switch (action.type) {
|
||||
case SET_SCREENSHARE_MUTED:
|
||||
return {
|
||||
...state,
|
||||
muted: action.muted
|
||||
};
|
||||
|
||||
case SET_VIDEO_UNMUTE_PERMISSIONS:
|
||||
return {
|
||||
...state,
|
||||
unmuteBlocked: action.blocked
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Media state object for local video.
|
||||
*
|
||||
@@ -179,6 +228,7 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
*/
|
||||
ReducerRegistry.register('features/base/media', combineReducers({
|
||||
audio: _audio,
|
||||
screenshare: _screenshare,
|
||||
video: _video
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { translate } from '../../../i18n';
|
||||
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
|
||||
import { connect } from '../../../redux';
|
||||
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
|
||||
import { CONNECTION_TYPE } from '../../constants';
|
||||
import { getConnectionData } from '../../functions';
|
||||
|
||||
@@ -26,6 +28,99 @@ type Props = {
|
||||
t: Function
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
connectionStatus: {
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.16px',
|
||||
lineHeight: '16px',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
|
||||
[theme.breakpoints.down('400')]: {
|
||||
margin: 0,
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
margin: `${theme.spacing(4)} auto`,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
width: PREJOIN_DEFAULT_CONTENT_WIDTH
|
||||
},
|
||||
|
||||
// mobile phone landscape
|
||||
'@media (max-height: 420px)': {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
'& .con-status-header': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
padding: '14px 16px'
|
||||
},
|
||||
|
||||
'& .con-status-circle': {
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
padding: theme.spacing(1),
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
|
||||
'& .con-status--good': {
|
||||
background: '#31B76A'
|
||||
},
|
||||
|
||||
'& .con-status--poor': {
|
||||
background: '#E12D2D'
|
||||
},
|
||||
|
||||
'& .con-status--non-optimal': {
|
||||
background: '#E39623'
|
||||
},
|
||||
|
||||
'& .con-status-arrow': {
|
||||
marginLeft: 'auto',
|
||||
transition: 'background-color 0.16s ease-out'
|
||||
},
|
||||
|
||||
'& .con-status-arrow--up': {
|
||||
transform: 'rotate(180deg)'
|
||||
},
|
||||
|
||||
'& .con-status-arrow > svg': {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
'& .con-status-arrow:hover': {
|
||||
backgroundColor: 'rgba(1, 1, 1, 0.1)'
|
||||
},
|
||||
|
||||
'& .con-status-text': {
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
'& .con-status-details': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderTop: '1px solid #5E6D7A',
|
||||
padding: theme.spacing(3),
|
||||
transition: 'opacity 0.16s ease-out'
|
||||
},
|
||||
|
||||
'& .con-status-details-visible': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .con-status-details-hidden': {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const CONNECTION_TYPE_MAP = {
|
||||
[CONNECTION_TYPE.POOR]: {
|
||||
connectionClass: 'con-status--poor',
|
||||
@@ -51,11 +146,8 @@ const CONNECTION_TYPE_MAP = {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
|
||||
if (connectionType === CONNECTION_TYPE.NONE) {
|
||||
return null;
|
||||
}
|
||||
const classes = useStyles();
|
||||
|
||||
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType];
|
||||
const [ showDetails, toggleDetails ] = useState(false);
|
||||
const arrowClassName = showDetails
|
||||
? 'con-status-arrow con-status-arrow--up'
|
||||
@@ -77,8 +169,14 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
|
||||
}
|
||||
}, [ showDetails, toggleDetails ]);
|
||||
|
||||
if (connectionType === CONNECTION_TYPE.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType];
|
||||
|
||||
return (
|
||||
<div className = 'con-status'>
|
||||
<div className = { classes.connectionStatus }>
|
||||
<div
|
||||
aria-level = { 1 }
|
||||
className = 'con-status-header'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,6 +54,18 @@ export const TRACK_CREATE_CANCELED = 'TRACK_CREATE_CANCELED';
|
||||
*/
|
||||
export const TRACK_CREATE_ERROR = 'TRACK_CREATE_ERROR';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when the track mute/unmute operation fails at the conference level. This could
|
||||
* happen because of {@code getUserMedia} errors during unmute or replace track errors at the peerconnection level.
|
||||
*
|
||||
* {
|
||||
* type: TRACK_MUTE_UNMUTE_FAILED,
|
||||
* track: Track,
|
||||
* wasMuting: Boolean
|
||||
* }
|
||||
*/
|
||||
export const TRACK_MUTE_UNMUTE_FAILED = 'TRACK_MUTE_UNMUTE_FAILED';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when a track has triggered no data from source event.
|
||||
*
|
||||
|
||||
@@ -5,11 +5,14 @@ import {
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications';
|
||||
import { getCurrentConference } from '../conference';
|
||||
import { getMultipleVideoSupportFeatureFlag } from '../config';
|
||||
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
MEDIA_TYPE,
|
||||
setAudioMuted,
|
||||
setScreenshareMuted,
|
||||
setVideoMuted,
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
VIDEO_TYPE
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
TRACK_ADDED,
|
||||
TRACK_CREATE_CANCELED,
|
||||
TRACK_CREATE_ERROR,
|
||||
TRACK_MUTE_UNMUTE_FAILED,
|
||||
TRACK_NO_DATA_FROM_SOURCE,
|
||||
TRACK_REMOVED,
|
||||
TRACK_STOPPED,
|
||||
@@ -39,6 +43,35 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Add a given local track to the conference.
|
||||
*
|
||||
* @param {JitsiLocalTrack} newTrack - The local track to be added to the conference.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function addLocalTrack(newTrack) {
|
||||
return async (dispatch, getState) => {
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
if (conference) {
|
||||
await conference.addTrack(newTrack);
|
||||
}
|
||||
|
||||
const setMuted = newTrack.isVideoTrack()
|
||||
? getMultipleVideoSupportFeatureFlag(getState())
|
||||
&& newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
|
||||
? setScreenshareMuted
|
||||
: setVideoMuted
|
||||
: setAudioMuted;
|
||||
const isMuted = newTrack.isMuted();
|
||||
|
||||
logger.log(`Adding ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
|
||||
await dispatch(setMuted(isMuted));
|
||||
|
||||
return dispatch(_addTracks([ newTrack ]));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the creating of the desired media type tracks. Desire is expressed
|
||||
* by base/media unless the function caller specifies desired media types
|
||||
@@ -320,49 +353,34 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
function replaceStoredTracks(oldTrack, newTrack) {
|
||||
return dispatch => {
|
||||
return async (dispatch, getState) => {
|
||||
// We call dispose after doing the replace because dispose will
|
||||
// try and do a new o/a after the track removes itself. Doing it
|
||||
// after means the JitsiLocalTrack.conference is already
|
||||
// cleared, so it won't try and do the o/a.
|
||||
const disposePromise
|
||||
= oldTrack
|
||||
? dispatch(_disposeAndRemoveTracks([ oldTrack ]))
|
||||
: Promise.resolve();
|
||||
if (oldTrack) {
|
||||
await dispatch(_disposeAndRemoveTracks([ oldTrack ]));
|
||||
}
|
||||
|
||||
return disposePromise
|
||||
.then(() => {
|
||||
if (newTrack) {
|
||||
// The mute state of the new track should be
|
||||
// reflected in the app's mute state. For example,
|
||||
// if the app is currently muted and changing to a
|
||||
// new track that is not muted, the app's mute
|
||||
// state should be falsey. As such, emit a mute
|
||||
// event here to set up the app to reflect the
|
||||
// track's mute state. If this is not done, the
|
||||
// current mute state of the app will be reflected
|
||||
// on the track, not vice-versa.
|
||||
const setMuted
|
||||
= newTrack.isVideoTrack()
|
||||
? setVideoMuted
|
||||
: setAudioMuted;
|
||||
const isMuted = newTrack.isMuted();
|
||||
if (newTrack) {
|
||||
// The mute state of the new track should be reflected in the app's mute state. For example, if the
|
||||
// app is currently muted and changing to a new track that is not muted, the app's mute state
|
||||
// should be falsey. As such, emit a mute event here to set up the app to reflect the track's mute
|
||||
// state. If this is not done, the current mute state of the app will be reflected on the track,
|
||||
// not vice-versa.
|
||||
const setMuted = newTrack.isVideoTrack()
|
||||
? getMultipleVideoSupportFeatureFlag(getState()) && newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
|
||||
? setScreenshareMuted
|
||||
: setVideoMuted
|
||||
: setAudioMuted;
|
||||
const isMuted = newTrack.isMuted();
|
||||
|
||||
sendAnalytics(createTrackMutedEvent(
|
||||
newTrack.getType(),
|
||||
'track.replaced',
|
||||
isMuted));
|
||||
logger.log(`Replace ${newTrack.getType()} track - ${
|
||||
isMuted ? 'muted' : 'unmuted'}`);
|
||||
sendAnalytics(createTrackMutedEvent(newTrack.getType(), 'track.replaced', isMuted));
|
||||
logger.log(`Replace ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
|
||||
|
||||
return dispatch(setMuted(isMuted));
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (newTrack) {
|
||||
return dispatch(_addTracks([ newTrack ]));
|
||||
}
|
||||
});
|
||||
await dispatch(setMuted(isMuted));
|
||||
await dispatch(_addTracks([ newTrack ]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -384,7 +402,9 @@ export function trackAdded(track) {
|
||||
|
||||
// participantId
|
||||
const local = track.isLocal();
|
||||
const mediaType = track.getType();
|
||||
const mediaType = getMultipleVideoSupportFeatureFlag(getState()) && track.getVideoType() === VIDEO_TYPE.DESKTOP
|
||||
? MEDIA_TYPE.SCREENSHARE
|
||||
: track.getType();
|
||||
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
|
||||
|
||||
if (local) {
|
||||
@@ -471,6 +491,25 @@ export function trackMutedChanged(track) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
|
||||
* @param {boolean} wasMuting - If the operation that failed was a mute operation or an unmute operation.
|
||||
* @returns {{
|
||||
* type: TRACK_MUTE_UNMUTE_FAILED,
|
||||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackMuteUnmuteFailed(track, wasMuting) {
|
||||
return {
|
||||
type: TRACK_MUTE_UNMUTE_FAILED,
|
||||
track,
|
||||
wasMuting
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for when a track's no data from source notification information changes.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* global APP */
|
||||
|
||||
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';
|
||||
@@ -295,6 +296,33 @@ export function getLocalAudioTrack(tracks) {
|
||||
return getLocalTrack(tracks, MEDIA_TYPE.AUDIO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local desktop track.
|
||||
*
|
||||
* @param {Track[]} tracks - List of all tracks.
|
||||
* @param {boolean} [includePending] - Indicates whether a local track is to be returned if it is still pending.
|
||||
* A local track is pending if {@code getUserMedia} is still executing to create it and, consequently, its
|
||||
* {@code jitsiTrack} property is {@code undefined}. By default a pending local track is not returned.
|
||||
* @returns {(Track|undefined)}
|
||||
*/
|
||||
export function getLocalDesktopTrack(tracks, includePending = false) {
|
||||
return (
|
||||
getLocalTracks(tracks, includePending)
|
||||
.find(t => t.mediaType === MEDIA_TYPE.SCREENSHARE || t.videoType === VIDEO_TYPE.DESKTOP));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored local desktop jitsiLocalTrack.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {JitsiLocalTrack|undefined}
|
||||
*/
|
||||
export function getLocalJitsiDesktopTrack(state) {
|
||||
const track = getLocalDesktopTrack(getTrackState(state));
|
||||
|
||||
return track?.jitsiTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local track by media type.
|
||||
*
|
||||
@@ -524,20 +552,22 @@ export function isUserInteractionRequiredForUnmute(state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of
|
||||
* the specified {@code track} is already in accord with the specified
|
||||
* {@code muted} value, then does nothing.
|
||||
* Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of the specified {@code track} is already in
|
||||
* accord with the specified {@code muted} value, then does nothing.
|
||||
*
|
||||
* @param {JitsiLocalTrack} track - The {@code JitsiLocalTrack} to mute or
|
||||
* unmute.
|
||||
* @param {boolean} muted - If the specified {@code track} is to be muted, then
|
||||
* {@code true}; otherwise, {@code false}.
|
||||
* @param {JitsiLocalTrack} track - The {@code JitsiLocalTrack} to mute or unmute.
|
||||
* @param {boolean} muted - If the specified {@code track} is to be muted, then {@code true}; otherwise, {@code false}.
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function setTrackMuted(track, muted) {
|
||||
export function setTrackMuted(track, muted, state) {
|
||||
muted = Boolean(muted); // eslint-disable-line no-param-reassign
|
||||
|
||||
if (track.isMuted() === muted) {
|
||||
// Ignore the check for desktop track muted operation. When the screenshare is terminated by clicking on the
|
||||
// browser's 'Stop sharing' button, the local stream is stopped before the inactive stream handler is fired.
|
||||
// We still need to proceed here and remove the track from the peerconnection.
|
||||
if (track.isMuted() === muted
|
||||
&& !(track.getVideoType() === VIDEO_TYPE.DESKTOP && getMultipleVideoSupportFeatureFlag(state))) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -546,8 +576,9 @@ export function setTrackMuted(track, muted) {
|
||||
return track[f]().catch(error => {
|
||||
// Track might be already disposed so ignore such an error.
|
||||
if (error.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
|
||||
// FIXME Emit mute failed, so that the app can show error dialog.
|
||||
logger.error(`set track ${f} failed`, error);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +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 { getAvailableDevices } from '../devices/actions';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
@@ -18,15 +19,20 @@ import {
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
TOGGLE_CAMERA_FACING_MODE,
|
||||
toggleCameraFacingMode,
|
||||
VIDEO_TYPE
|
||||
SET_SCREENSHARE_MUTED,
|
||||
VIDEO_TYPE,
|
||||
setScreenshareMuted,
|
||||
SCREENSHARE_MUTISM_AUTHORITY
|
||||
} from '../media';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_MUTE_UNMUTE_FAILED,
|
||||
TRACK_NO_DATA_FROM_SOURCE,
|
||||
TRACK_REMOVED,
|
||||
TRACK_STOPPED,
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import {
|
||||
@@ -34,6 +40,7 @@ import {
|
||||
destroyLocalTracks,
|
||||
showNoDataFromSourceVideoError,
|
||||
toggleScreensharing,
|
||||
trackMuteUnmuteFailed,
|
||||
trackRemoved,
|
||||
trackNoDataFromSourceNotificationInfoChanged
|
||||
} from './actions';
|
||||
@@ -107,6 +114,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_SCREENSHARE_MUTED:
|
||||
_setMuted(store, action, action.mediaType);
|
||||
break;
|
||||
|
||||
case SET_VIDEO_MUTED:
|
||||
if (!action.muted
|
||||
&& isUserInteractionRequiredForUnmute(store.getState())) {
|
||||
@@ -156,19 +167,54 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
const { enabled, audioOnly, ignoreDidHaveVideo } = action;
|
||||
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, { enabled,
|
||||
audioOnly,
|
||||
ignoreDidHaveVideo });
|
||||
if (!getMultipleVideoSupportFeatureFlag(store.getState())) {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING,
|
||||
{
|
||||
enabled,
|
||||
audioOnly,
|
||||
ignoreDidHaveVideo
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TRACK_MUTE_UNMUTE_FAILED: {
|
||||
const { jitsiTrack } = action.track;
|
||||
const muted = action.wasMuted;
|
||||
const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP
|
||||
&& getMultipleVideoSupportFeatureFlag(store.getState())) {
|
||||
store.dispatch(setScreenshareMuted(!muted));
|
||||
} else if (isVideoTrack) {
|
||||
APP.conference.setVideoMuteStatus();
|
||||
} else {
|
||||
APP.conference.setAudioMuteStatus(!muted);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TRACK_STOPPED: {
|
||||
const { jitsiTrack } = action.track;
|
||||
|
||||
if (typeof APP !== 'undefined'
|
||||
&& getMultipleVideoSupportFeatureFlag(store.getState())
|
||||
&& jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
store.dispatch(toggleScreensharing(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TRACK_UPDATED: {
|
||||
// TODO Remove the following calls to APP.UI once components interested
|
||||
// in track mute changes are moved into React and/or redux.
|
||||
if (typeof APP !== 'undefined') {
|
||||
const result = next(action);
|
||||
const state = store.getState();
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -181,10 +227,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
// Do not change the video mute state for local presenter tracks.
|
||||
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
|
||||
APP.conference.mutePresenter(muted);
|
||||
} else if (jitsiTrack.isLocal() && !(jitsiTrack.videoType === VIDEO_TYPE.DESKTOP)) {
|
||||
} else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
|
||||
APP.conference.setVideoMuteStatus();
|
||||
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
|
||||
store.dispatch(toggleScreensharing(false, false, true));
|
||||
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
!getMultipleVideoSupportFeatureFlag(state)
|
||||
&& store.dispatch(toggleScreensharing(false, false, true));
|
||||
} else {
|
||||
APP.UI.setVideoMuted(participantID);
|
||||
}
|
||||
@@ -335,25 +382,34 @@ function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
|
||||
const localTrack
|
||||
= _getLocalTrack(store, mediaType, /* includePending */ true);
|
||||
async function _setMuted(store, { ensureTrack, authority, muted }, mediaType: MEDIA_TYPE) {
|
||||
const { dispatch, getState } = store;
|
||||
const localTrack = _getLocalTrack(store, mediaType, /* includePending */ true);
|
||||
const state = getState();
|
||||
|
||||
if (mediaType === MEDIA_TYPE.SCREENSHARE
|
||||
&& getMultipleVideoSupportFeatureFlag(state)
|
||||
&& !muted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localTrack) {
|
||||
// The `jitsiTrack` property will have a value only for a localTrack for
|
||||
// which `getUserMedia` has already completed. If there's no
|
||||
// `jitsiTrack`, then the `muted` state will be applied once the
|
||||
// `jitsiTrack` is created.
|
||||
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
|
||||
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
|
||||
// created.
|
||||
const { jitsiTrack } = localTrack;
|
||||
const isAudioOnly = authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY;
|
||||
const isAudioOnly = (mediaType === MEDIA_TYPE.VIDEO && authority === VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY)
|
||||
|| (mediaType === MEDIA_TYPE.SCREENSHARE && authority === SCREENSHARE_MUTISM_AUTHORITY.AUDIO_ONLY);
|
||||
|
||||
// screenshare cannot be muted or unmuted using the video mute button
|
||||
// anymore, unless it is muted by audioOnly.
|
||||
jitsiTrack && (jitsiTrack.videoType !== 'desktop' || isAudioOnly)
|
||||
&& setTrackMuted(jitsiTrack, muted);
|
||||
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(store.getState()))) {
|
||||
// Screenshare cannot be unmuted using the video mute button unless it is muted by audioOnly in the legacy
|
||||
// screensharing mode.
|
||||
if (jitsiTrack
|
||||
&& (jitsiTrack.videoType !== 'desktop' || isAudioOnly || getMultipleVideoSupportFeatureFlag(state))) {
|
||||
setTrackMuted(jitsiTrack, muted, state).catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
|
||||
}
|
||||
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
|
||||
// FIXME: This only runs on mobile now because web has its own way of
|
||||
// creating local tracks. Adjust the check once they are unified.
|
||||
store.dispatch(createLocalTracksA({ devices: [ mediaType ] }));
|
||||
dispatch(createLocalTracksA({ devices: [ mediaType ] }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const colorMap = {
|
||||
uiBackground: 'surface01',
|
||||
|
||||
// Container background
|
||||
ui00: 'surface00',
|
||||
ui01: 'surface02',
|
||||
ui02: 'surface03',
|
||||
ui03: 'surface04',
|
||||
@@ -376,6 +377,13 @@ export const typography = {
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongRegularLarge: {
|
||||
fontSize: 16,
|
||||
lineHeight: 26,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongBold: {
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
|
||||
6
react/features/base/ui/components/variables.js
Normal file
6
react/features/base/ui/components/variables.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Prejoin / premeeting screen.
|
||||
*/
|
||||
|
||||
// Maps SCSS variable $prejoinDefaultContentWidth
|
||||
export const PREJOIN_DEFAULT_CONTENT_WIDTH = '336px';
|
||||
@@ -9,6 +9,7 @@ export const commonClassName = {
|
||||
emptyList: 'empty-list',
|
||||
overflowMenuItem: 'overflow-menu-item',
|
||||
overflowMenuItemIcon: 'overflow-menu-item-icon',
|
||||
participantAvatar: 'participant-avatar',
|
||||
toolboxIcon: 'toolbox-icon',
|
||||
toolboxButton: 'toolbox-button',
|
||||
toolboxContentItems: 'toolbox-content-items'
|
||||
@@ -96,6 +97,9 @@ export const commonStyles = (theme: Object) => {
|
||||
width: 20
|
||||
}
|
||||
},
|
||||
[commonClassName.participantAvatar]: {
|
||||
margin: `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(2)} 0`
|
||||
},
|
||||
[commonClassName.toolboxIcon]: {
|
||||
display: 'flex',
|
||||
borderRadius: 3,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { RecordingLabel } from '../../../recording';
|
||||
import { openHighlightDialog } from '../../../recording/actions.native';
|
||||
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
|
||||
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
import {
|
||||
@@ -23,22 +26,32 @@ type Props = {
|
||||
createOnPress: Function
|
||||
}
|
||||
|
||||
const AlwaysOnLabels = ({ createOnPress }: Props) => (<>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RECORDING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_STREAMING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RAISED_HANDS_COUNT) } >
|
||||
<RaisedHandsCountLabel />
|
||||
</TouchableOpacity>
|
||||
</>);
|
||||
const AlwaysOnLabels = ({ createOnPress }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const openHighlightDialogCallback = useCallback(() => dispatch(openHighlightDialog()), [ dispatch ]);
|
||||
|
||||
return (<>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RECORDING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_STREAMING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { openHighlightDialogCallback }>
|
||||
<HighlightButton />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RAISED_HANDS_COUNT) } >
|
||||
<RaisedHandsCountLabel />
|
||||
</TouchableOpacity>
|
||||
</>);
|
||||
};
|
||||
|
||||
export default AlwaysOnLabels;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const useStyles = makeStyles(theme => {
|
||||
justifyContent: 'center',
|
||||
marginBottom: theme.spacing(7),
|
||||
transition: 'margin-bottom 0.3s',
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
||||
@@ -6,11 +6,21 @@ import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
* Extracts the fqn part from a path, where fqn represents
|
||||
* tenant/roomName.
|
||||
*
|
||||
* @param {string} path - The URL path.
|
||||
* @param {Object} state - A redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function extractFqnFromPath() {
|
||||
const parts = window.location.pathname.split('/');
|
||||
export function extractFqnFromPath(state?: Object) {
|
||||
let pathname;
|
||||
|
||||
if (window.location.pathname) {
|
||||
pathname = window.location.pathname;
|
||||
} else if (state && state['features/base/connection']) {
|
||||
pathname = state['features/base/connection'].locationURL.pathname;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts = pathname.split('/');
|
||||
const len = parts.length;
|
||||
|
||||
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : parts[1];
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// @flow
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getLocalParticipant, getParticipantById, pinParticipant } from '../base/participants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getRemoteParticipantCount,
|
||||
pinParticipant
|
||||
} from '../base/participants';
|
||||
import { shouldHideSelfView } from '../base/settings/functions.any';
|
||||
import { getTileViewGridDimensions } from '../video-layout';
|
||||
import { getMaxColumnCount } from '../video-layout';
|
||||
|
||||
import {
|
||||
SET_FILMSTRIP_WIDTH,
|
||||
@@ -21,12 +26,17 @@ import {
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
} from './constants';
|
||||
import {
|
||||
calculateNotResponsiveTileViewDimensions,
|
||||
calculateResponsiveTileViewDimensions,
|
||||
calculateThumbnailSizeForHorizontalView,
|
||||
calculateThumbnailSizeForTileView,
|
||||
calculateThumbnailSizeForVerticalView,
|
||||
getNumberOfPartipantsForTileView,
|
||||
isFilmstripResizable,
|
||||
showGridInVerticalView
|
||||
} from './functions';
|
||||
@@ -41,37 +51,55 @@ export * from './actions.any';
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setTileViewDimensions(dimensions: Object) {
|
||||
export function setTileViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { disableResponsiveTiles, disableTileEnlargement } = state['features/base/config'];
|
||||
const {
|
||||
disableResponsiveTiles,
|
||||
disableTileEnlargement,
|
||||
tileView = {}
|
||||
} = state['features/base/config'];
|
||||
const { numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES } = tileView;
|
||||
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
|
||||
const maxColumns = getMaxColumnCount(state);
|
||||
|
||||
const {
|
||||
height,
|
||||
width
|
||||
} = calculateThumbnailSizeForTileView({
|
||||
...dimensions,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles,
|
||||
disableTileEnlargement
|
||||
});
|
||||
const { columns, rows } = dimensions;
|
||||
width,
|
||||
columns,
|
||||
rows
|
||||
} = disableResponsiveTiles
|
||||
? calculateNotResponsiveTileViewDimensions(state)
|
||||
: calculateResponsiveTileViewDimensions({
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
maxColumns,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles
|
||||
});
|
||||
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
|
||||
const hasScroll = clientHeight < thumbnailsTotalHeight;
|
||||
const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
|
||||
const filmstripHeight = Math.min(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_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
gridDimensions: dimensions,
|
||||
gridDimensions: {
|
||||
columns,
|
||||
rows
|
||||
},
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
},
|
||||
filmstripHeight,
|
||||
filmstripWidth
|
||||
filmstripWidth,
|
||||
hasScroll
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -90,37 +118,51 @@ export function setVerticalViewDimensions() {
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const resizableFilmstrip = isFilmstripResizable(state);
|
||||
const _verticalViewGrid = showGridInVerticalView(state);
|
||||
const numberOfRemoteParticipants = getRemoteParticipantCount(state);
|
||||
|
||||
let gridView = {};
|
||||
let thumbnails = {};
|
||||
let filmstripDimensions = {};
|
||||
let hasScroll = false;
|
||||
let remoteVideosContainerWidth;
|
||||
let remoteVideosContainerHeight;
|
||||
|
||||
// grid view in the vertical filmstrip
|
||||
if (_verticalViewGrid) {
|
||||
const dimensions = getTileViewGridDimensions(state, filmstripWidth.current);
|
||||
const { tileView = {} } = state['features/base/config'];
|
||||
const { numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES } = tileView;
|
||||
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
|
||||
const maxColumns = getMaxColumnCount(state);
|
||||
const {
|
||||
height,
|
||||
width
|
||||
} = calculateThumbnailSizeForTileView({
|
||||
...dimensions,
|
||||
width,
|
||||
columns,
|
||||
rows
|
||||
} = calculateResponsiveTileViewDimensions({
|
||||
clientWidth: filmstripWidth.current,
|
||||
clientHeight,
|
||||
disableResponsiveTiles: false,
|
||||
disableTileEnlargement: false,
|
||||
isVerticalFilmstrip: true
|
||||
isVerticalFilmstrip: true,
|
||||
maxColumns,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles
|
||||
});
|
||||
const { columns, rows } = dimensions;
|
||||
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
|
||||
const hasScroll = clientHeight < thumbnailsTotalHeight;
|
||||
|
||||
hasScroll = clientHeight < thumbnailsTotalHeight;
|
||||
const widthOfFilmstrip = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
|
||||
const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
|
||||
const filmstripHeight = Math.min(clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN, thumbnailsTotalHeight);
|
||||
|
||||
gridView = {
|
||||
gridDimensions: dimensions,
|
||||
gridDimensions: {
|
||||
columns,
|
||||
rows
|
||||
},
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
}
|
||||
},
|
||||
hasScroll
|
||||
};
|
||||
|
||||
filmstripDimensions = {
|
||||
@@ -129,6 +171,14 @@ export function setVerticalViewDimensions() {
|
||||
};
|
||||
} else {
|
||||
thumbnails = calculateThumbnailSizeForVerticalView(clientWidth, filmstripWidth.current, resizableFilmstrip);
|
||||
|
||||
remoteVideosContainerWidth
|
||||
= thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE;
|
||||
remoteVideosContainerHeight
|
||||
= clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height) - VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
|
||||
hasScroll
|
||||
= remoteVideosContainerHeight
|
||||
< (thumbnails?.remote.height + TILE_VERTICAL_MARGIN) * numberOfRemoteParticipants;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
@@ -136,12 +186,11 @@ export function setVerticalViewDimensions() {
|
||||
dimensions: {
|
||||
...thumbnails,
|
||||
remoteVideosContainer: _verticalViewGrid ? filmstripDimensions : {
|
||||
width: thumbnails?.local?.width
|
||||
+ TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE,
|
||||
height: clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height)
|
||||
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
width: remoteVideosContainerWidth,
|
||||
height: remoteVideosContainerHeight
|
||||
},
|
||||
gridView
|
||||
gridView,
|
||||
hasScroll
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -158,16 +207,24 @@ export function setHorizontalViewDimensions() {
|
||||
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
|
||||
const remoteVideosContainerWidth
|
||||
= clientWidth - (disableSelfView ? 0 : thumbnails?.local?.width) - HORIZONTAL_FILMSTRIP_MARGIN;
|
||||
const remoteVideosContainerHeight
|
||||
= thumbnails?.local?.height + TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE;
|
||||
const numberOfRemoteParticipants = getRemoteParticipantCount(state);
|
||||
const hasScroll
|
||||
= remoteVideosContainerHeight
|
||||
< (thumbnails?.remote.width + TILE_HORIZONTAL_MARGIN) * numberOfRemoteParticipants;
|
||||
|
||||
dispatch({
|
||||
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
...thumbnails,
|
||||
remoteVideosContainer: {
|
||||
width: clientWidth - (disableSelfView ? 0 : thumbnails?.local?.width) - HORIZONTAL_FILMSTRIP_MARGIN,
|
||||
height: thumbnails?.local?.height
|
||||
+ TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
|
||||
}
|
||||
width: remoteVideosContainerWidth,
|
||||
height: remoteVideosContainerHeight
|
||||
},
|
||||
hasScroll
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -88,6 +88,11 @@ type Props = {
|
||||
*/
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether or not we have scroll on the filmstrip.
|
||||
*/
|
||||
_hasScroll: boolean,
|
||||
|
||||
/**
|
||||
* Whether this is a recorder or not.
|
||||
*/
|
||||
@@ -320,7 +325,8 @@ class Filmstrip extends PureComponent <Props, State> {
|
||||
const filmstrip = (<>
|
||||
<div
|
||||
className = { clsx(this.props._videosClassName,
|
||||
!tileViewActive && !_resizableFilmstrip && 'filmstrip-hover') }
|
||||
!tileViewActive && !_resizableFilmstrip && 'filmstrip-hover',
|
||||
_verticalViewGrid && 'vertical-view-grid') }
|
||||
id = 'remoteVideos'>
|
||||
{!_disableSelfView && !_verticalViewGrid && (
|
||||
<div
|
||||
@@ -566,6 +572,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
||||
_currentLayout,
|
||||
_filmstripHeight,
|
||||
_filmstripWidth,
|
||||
_hasScroll,
|
||||
_remoteParticipantsLength,
|
||||
_resizableFilmstrip,
|
||||
_rows,
|
||||
@@ -619,7 +626,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
||||
|
||||
if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
||||
const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
|
||||
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
|
||||
const isNotOverflowing = !_hasScroll;
|
||||
|
||||
props.itemSize = itemSize;
|
||||
|
||||
@@ -631,7 +638,7 @@ class Filmstrip extends PureComponent <Props, State> {
|
||||
|
||||
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
|
||||
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
|
||||
const isNotOverflowing = !_hasScroll;
|
||||
|
||||
if (isNotOverflowing) {
|
||||
props.className += ' is-not-overflowing';
|
||||
@@ -767,6 +774,7 @@ function _mapStateToProps(state) {
|
||||
gridDimensions: dimensions = {},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
hasScroll: tileViewHasScroll,
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
@@ -774,6 +782,7 @@ function _mapStateToProps(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;
|
||||
@@ -796,7 +805,7 @@ function _mapStateToProps(state) {
|
||||
const shouldReduceHeight = reduceHeight && (
|
||||
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
|
||||
|
||||
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
let videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
const className = `${remoteVideosVisible || _verticalViewGrid ? '' : 'hide-videos'} ${
|
||||
shouldReduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
|
||||
@@ -804,13 +813,23 @@ function _mapStateToProps(state) {
|
||||
|
||||
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 } = state['features/filmstrip'].verticalViewDimensions;
|
||||
const {
|
||||
remote,
|
||||
remoteVideosContainer,
|
||||
gridView,
|
||||
hasScroll
|
||||
} = state['features/filmstrip'].verticalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
|
||||
? TOOLBAR_HEIGHT : 0);
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
@@ -818,14 +837,19 @@ function _mapStateToProps(state) {
|
||||
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 } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
|
||||
_hasScroll = Boolean(hasScroll);
|
||||
_thumbnailSize = remote;
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height;
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
@@ -840,6 +864,7 @@ function _mapStateToProps(state) {
|
||||
_disableSelfView: disableSelfView,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_hasScroll,
|
||||
_iAmRecorder: Boolean(iAmRecorder),
|
||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_isToolboxVisible: isToolboxVisible(state),
|
||||
|
||||
@@ -28,16 +28,6 @@ export const SQUARE_TILE_ASPECT_RATIO = 1;
|
||||
*/
|
||||
export const DISPLAY_DRAWER_THRESHOLD = 512;
|
||||
|
||||
/**
|
||||
* Breakpoint past which a single column view is enforced in tile view.
|
||||
*/
|
||||
export const SINGLE_COLUMN_BREAKPOINT = 300;
|
||||
|
||||
/**
|
||||
* Breakpoint past which a two column view is enforced in tile view.
|
||||
*/
|
||||
export const TWO_COLUMN_BREAKPOINT = 1000;
|
||||
|
||||
/**
|
||||
* Breakpoint past which the aspect ratio is switched in tile view.
|
||||
* Also, past this breakpoint, if there are two participants in the conference, we enforce
|
||||
@@ -57,9 +47,14 @@ export const TILE_MIN_HEIGHT_SMALL = 150;
|
||||
export const TILE_MIN_HEIGHT_LARGE = 200;
|
||||
|
||||
/**
|
||||
* Aspect ratio for portrait tiles. (height / width).
|
||||
* Aspect ratio for portrait tiles.
|
||||
*/
|
||||
export const TILE_PORTRAIT_ASPECT_RATIO = 1.3;
|
||||
export const TILE_PORTRAIT_ASPECT_RATIO = 1 / 1.3;
|
||||
|
||||
/**
|
||||
* The default number of visible tiles for tile view.
|
||||
*/
|
||||
export const TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES = 25;
|
||||
|
||||
/**
|
||||
* The default number of columns for tile view.
|
||||
@@ -150,14 +145,14 @@ export const TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN = 2;
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_VIEW_GRID_VERTICAL_MARGIN = 12;
|
||||
export const TILE_VIEW_GRID_VERTICAL_MARGIN = 14;
|
||||
|
||||
/**
|
||||
* The horizontal margin of the tile grid container.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_VIEW_GRID_HORIZONTAL_MARGIN = 12;
|
||||
export const TILE_VIEW_GRID_HORIZONTAL_MARGIN = 14;
|
||||
|
||||
/**
|
||||
* The height of the whole toolbar.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isReorderingEnabled } from './functions';
|
||||
|
||||
/**
|
||||
* Computes the reorderd list of the remote participants.
|
||||
@@ -12,11 +13,9 @@ import { setRemoteParticipants } from './actions';
|
||||
*/
|
||||
export function updateRemoteParticipants(store: Object, participantId: ?number) {
|
||||
const state = store.getState();
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
let reorderedParticipants = [];
|
||||
|
||||
if (!enableThumbnailReordering) {
|
||||
if (!isReorderingEnabled(state)) {
|
||||
if (participantId) {
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
|
||||
@@ -88,3 +88,17 @@ export function getColumnCount(stateful: Object | Function) {
|
||||
|
||||
return Math.min(3, participantCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if thumbnail reordering is enabled and false otherwise.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - True if thumbnail reordering is enabled and false otherwise.
|
||||
*/
|
||||
export function isReorderingEnabled(state) {
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
|
||||
return enableThumbnailReordering;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getPinnedParticipant
|
||||
} from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { shouldHideSelfView } from '../base/settings/functions.any';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
@@ -17,7 +18,11 @@ import {
|
||||
isRemoteTrackMuted
|
||||
} from '../base/tracks/functions';
|
||||
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
|
||||
import { getCurrentLayout, LAYOUTS } from '../video-layout';
|
||||
import {
|
||||
getCurrentLayout,
|
||||
getNotResponsiveTileViewGridDimensions,
|
||||
LAYOUTS
|
||||
} from '../video-layout';
|
||||
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
@@ -35,6 +40,7 @@ import {
|
||||
TILE_MIN_HEIGHT_SMALL,
|
||||
TILE_PORTRAIT_ASPECT_RATIO,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
VERTICAL_VIEW_HORIZONTAL_MARGIN
|
||||
@@ -187,6 +193,189 @@ export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum height of a thumbnail.
|
||||
*
|
||||
* @param {number} clientWidth - The width of the window.
|
||||
* @returns {number} The minimum height of a thumbnail.
|
||||
*/
|
||||
export function getThumbnailMinHeight(clientWidth) {
|
||||
return clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default aspect ratio for a tile.
|
||||
*
|
||||
* @param {boolean} disableResponsiveTiles - Indicates whether the responsive tiles functionality is disabled.
|
||||
* @param {boolean} disableTileEnlargement - Indicates whether the tiles enlargement functionality is disabled.
|
||||
* @param {number} clientWidth - The width of the window.
|
||||
* @returns {number} The default aspect ratio for a tile.
|
||||
*/
|
||||
export function getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth) {
|
||||
if (!disableResponsiveTiles && disableTileEnlargement && clientWidth < ASPECT_RATIO_BREAKPOINT) {
|
||||
return SQUARE_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
return TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of participants that will be displayed in tile view.
|
||||
*
|
||||
* @param {Object} state - The redux store state.
|
||||
* @returns {number} The number of participants that will be displayed in tile view.
|
||||
*/
|
||||
export function getNumberOfPartipantsForTileView(state) {
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
const numberOfParticipants = getParticipantCountWithFake(state)
|
||||
- (iAmRecorder ? 1 : 0)
|
||||
- (disableSelfView ? 1 : 0);
|
||||
|
||||
return numberOfParticipants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimensions (thumbnail width/height and columns/row) for tile view when the responsive tiles are
|
||||
* disabled.
|
||||
*
|
||||
* @param {Object} state - The redux store state.
|
||||
* @returns {Object} - The dimensions.
|
||||
*/
|
||||
export function calculateNotResponsiveTileViewDimensions(state) {
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { disableTileEnlargement } = state['features/base/config'];
|
||||
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
|
||||
const size = calculateThumbnailSizeForTileView({
|
||||
columns: c,
|
||||
minVisibleRows,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
disableResponsiveTiles: true
|
||||
});
|
||||
|
||||
if (typeof size === 'undefined') { // The columns don't fit into the screen. We will have horizontal scroll.
|
||||
const aspectRatio = disableTileEnlargement
|
||||
? getTileDefaultAspectRatio(true, disableTileEnlargement, clientWidth)
|
||||
: TILE_PORTRAIT_ASPECT_RATIO;
|
||||
|
||||
const height = getThumbnailMinHeight(clientWidth);
|
||||
|
||||
return {
|
||||
height,
|
||||
width: aspectRatio * height,
|
||||
columns: c,
|
||||
rows: r
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
columns: c,
|
||||
rows: r
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimensions (thumbnail width/height and columns/row) for tile view when the responsive tiles are
|
||||
* enabled.
|
||||
*
|
||||
* @param {Object} state - The redux store state.
|
||||
* @returns {Object} - The dimensions.
|
||||
*/
|
||||
export function calculateResponsiveTileViewDimensions({
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement = false,
|
||||
isVerticalFilmstrip = false,
|
||||
maxColumns,
|
||||
numberOfParticipants,
|
||||
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
|
||||
}) {
|
||||
let height, width;
|
||||
let columns, rows;
|
||||
let dimensions = {
|
||||
maxArea: 0
|
||||
};
|
||||
let minHeightEnforcedDimensions = {
|
||||
maxArea: 0
|
||||
};
|
||||
let zeroVisibleRowsDimensions = {
|
||||
maxArea: 0
|
||||
};
|
||||
|
||||
for (let c = 1; c <= Math.min(maxColumns, numberOfParticipants); c++) {
|
||||
const r = Math.ceil(numberOfParticipants / c);
|
||||
|
||||
// we want to display as much as possible tumbnails up to numberOfVisibleTiles
|
||||
const visibleRows
|
||||
= numberOfParticipants <= numberOfVisibleTiles ? r : Math.floor(numberOfVisibleTiles / c);
|
||||
|
||||
const size = calculateThumbnailSizeForTileView({
|
||||
columns: c,
|
||||
minVisibleRows: visibleRows,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableTileEnlargement,
|
||||
disableResponsiveTiles: false,
|
||||
isVerticalFilmstrip
|
||||
});
|
||||
|
||||
if (size) {
|
||||
const { height: currentHeight, width: currentWidth, minHeightEnforced, maxVisibleRows } = size;
|
||||
let area = currentHeight * currentWidth * Math.min(c * maxVisibleRows, numberOfParticipants);
|
||||
const currentDimensions = {
|
||||
maxArea: area,
|
||||
height: currentHeight,
|
||||
width: currentWidth,
|
||||
columns: c,
|
||||
rows: r
|
||||
};
|
||||
|
||||
if (!minHeightEnforced && area > dimensions.maxArea) {
|
||||
dimensions = currentDimensions;
|
||||
} else if (minHeightEnforced && area > minHeightEnforcedDimensions.maxArea) {
|
||||
minHeightEnforcedDimensions = currentDimensions;
|
||||
} else if (minHeightEnforced && maxVisibleRows === 0) {
|
||||
area = currentHeight * currentWidth * Math.min(c, numberOfParticipants);
|
||||
|
||||
if (area > zeroVisibleRowsDimensions.maxArea) {
|
||||
zeroVisibleRowsDimensions = {
|
||||
...currentDimensions,
|
||||
maxArea: area
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dimensions.maxArea > 0) {
|
||||
({ height, width, columns, rows } = dimensions);
|
||||
} else if (minHeightEnforcedDimensions.maxArea > 0) {
|
||||
({ height, width, columns, rows } = minHeightEnforcedDimensions);
|
||||
} else if (zeroVisibleRowsDimensions.maxArea > 0) {
|
||||
({ height, width, columns, rows } = zeroVisibleRowsDimensions);
|
||||
} else { // This would mean that we can't fit even one thumbnail with minimal size.
|
||||
const aspectRatio = disableTileEnlargement
|
||||
? getTileDefaultAspectRatio(false, disableTileEnlargement, clientWidth)
|
||||
: TILE_PORTRAIT_ASPECT_RATIO;
|
||||
|
||||
height = getThumbnailMinHeight(clientWidth);
|
||||
width = aspectRatio * height;
|
||||
columns = 1;
|
||||
rows = numberOfParticipants;
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
width,
|
||||
columns,
|
||||
rows
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size for thumbnails when in tile view layout.
|
||||
*
|
||||
@@ -196,90 +385,79 @@ export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0,
|
||||
export function calculateThumbnailSizeForTileView({
|
||||
columns,
|
||||
minVisibleRows,
|
||||
rows,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles,
|
||||
disableTileEnlargement,
|
||||
disableResponsiveTiles = false,
|
||||
disableTileEnlargement = false,
|
||||
isVerticalFilmstrip = false
|
||||
}: Object) {
|
||||
let aspectRatio = TILE_ASPECT_RATIO;
|
||||
|
||||
if (!disableResponsiveTiles && clientWidth < ASPECT_RATIO_BREAKPOINT) {
|
||||
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
const minHeight = clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
|
||||
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
|
||||
const minHeight = getThumbnailMinHeight(clientWidth);
|
||||
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
|
||||
- (isVerticalFilmstrip ? 0 : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
|
||||
- (isVerticalFilmstrip ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
|
||||
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
|
||||
const initialWidth = viewWidth / columns;
|
||||
const initialHeight = viewHeight / minVisibleRows;
|
||||
const aspectRatioHeight = initialWidth / aspectRatio;
|
||||
const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
|
||||
const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
|
||||
let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
|
||||
let width = Math.floor(aspectRatio * height);
|
||||
let initialHeight = viewHeight / minVisibleRows;
|
||||
let minHeightEnforced = false;
|
||||
|
||||
if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
|
||||
const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
|
||||
|
||||
// Recalculating width/height to fit the available space when a scroll is displayed.
|
||||
// NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
|
||||
// height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
|
||||
// to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
|
||||
// bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
|
||||
// window.
|
||||
height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
|
||||
width = Math.floor(aspectRatio * height);
|
||||
|
||||
return {
|
||||
height,
|
||||
width
|
||||
};
|
||||
if (initialHeight < minHeight) {
|
||||
minHeightEnforced = true;
|
||||
initialHeight = minHeight;
|
||||
}
|
||||
|
||||
if (disableTileEnlargement) {
|
||||
return {
|
||||
height,
|
||||
width
|
||||
};
|
||||
}
|
||||
const aspectRatioHeight = initialWidth / aspectRatio;
|
||||
|
||||
if (initialHeight > noScrollHeight) {
|
||||
height = Math.max(height, viewHeight / rows, minHeight);
|
||||
width = Math.max(width, initialWidth);
|
||||
} else {
|
||||
height = Math.max(initialHeight, minHeight);
|
||||
width = initialWidth;
|
||||
}
|
||||
|
||||
if (height > width) {
|
||||
const heightFromWidth = TILE_PORTRAIT_ASPECT_RATIO * width;
|
||||
|
||||
if (height > heightFromWidth && heightFromWidth < minHeight) {
|
||||
return {
|
||||
height,
|
||||
width: height / TILE_PORTRAIT_ASPECT_RATIO
|
||||
};
|
||||
if (aspectRatioHeight < minHeight) { // we can't fit the required number of columns.
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
height: Math.min(height, heightFromWidth),
|
||||
width
|
||||
};
|
||||
} else if (height < width) {
|
||||
const height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
|
||||
|
||||
return {
|
||||
height,
|
||||
width: Math.min(width, aspectRatio * height)
|
||||
width: Math.floor(aspectRatio * height),
|
||||
minHeightEnforced,
|
||||
maxVisibleRows: Math.floor(viewHeight / height)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
width
|
||||
};
|
||||
const initialRatio = initialWidth / initialHeight;
|
||||
let height = Math.floor(initialHeight);
|
||||
|
||||
// The biggest area of the grid will be when the grid's height is equal to clientHeight or when the grid's width is
|
||||
// equal to clientWidth.
|
||||
|
||||
if (initialRatio > aspectRatio) {
|
||||
return {
|
||||
height,
|
||||
width: Math.floor(initialHeight * aspectRatio),
|
||||
minHeightEnforced,
|
||||
maxVisibleRows: Math.floor(viewHeight / height)
|
||||
};
|
||||
} else if (initialRatio >= TILE_PORTRAIT_ASPECT_RATIO) {
|
||||
return {
|
||||
height,
|
||||
width: Math.floor(initialWidth),
|
||||
minHeightEnforced,
|
||||
maxVisibleRows: Math.floor(viewHeight / height)
|
||||
};
|
||||
} else if (!minHeightEnforced) {
|
||||
height = Math.floor(initialWidth / TILE_PORTRAIT_ASPECT_RATIO);
|
||||
|
||||
if (height >= minHeight) {
|
||||
return {
|
||||
height,
|
||||
width: Math.floor(initialWidth),
|
||||
minHeightEnforced,
|
||||
maxVisibleRows: Math.floor(viewHeight / height)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// else
|
||||
// We can't fit that number of columns with the desired min height and aspect ratio.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,3 +601,44 @@ export function getVerticalViewMaxWidth(state) {
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if thumbnail reordering is enabled and false otherwise.
|
||||
* Note: The function will return false if all participants are displayed on the screen.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - True if thumbnail reordering is enabled and false otherwise.
|
||||
*/
|
||||
export function isReorderingEnabled(state) {
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
|
||||
return enableThumbnailReordering && isFilmstripScollVisible(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the scroll is displayed and false otherwise.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - True if the scroll is displayed and false otherwise.
|
||||
*/
|
||||
export function isFilmstripScollVisible(state) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
let hasScroll = false;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
({ hasScroll = false } = state['features/filmstrip'].tileViewDimensions);
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
({ hasScroll = false } = state['features/filmstrip'].verticalViewDimensions);
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
({ hasScroll = false } = state['features/filmstrip'].horizontalViewDimensions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasScroll;
|
||||
}
|
||||
|
||||
@@ -44,9 +44,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (layout) {
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
|
||||
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
store.dispatch(setTileViewDimensions());
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
|
||||
import { shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import { setTileViewDimensions } from './actions';
|
||||
import './subscriber.any';
|
||||
@@ -34,9 +34,7 @@ StateListenerRegistry.register(
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldDisplayTileView(state),
|
||||
/* listener */ (isTileView, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (isTileView) {
|
||||
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
|
||||
store.dispatch(setTileViewDimensions());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { clientResized } from '../base/responsive-ui';
|
||||
import { shouldHideSelfView } from '../base/settings';
|
||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
||||
import { getParticipantsPaneOpen } from '../participants-pane/functions';
|
||||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
|
||||
import {
|
||||
setHorizontalViewDimensions,
|
||||
@@ -17,11 +17,9 @@ import {
|
||||
} from './actions';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DISPLAY_DRAWER_THRESHOLD,
|
||||
SINGLE_COLUMN_BREAKPOINT,
|
||||
TWO_COLUMN_BREAKPOINT
|
||||
DISPLAY_DRAWER_THRESHOLD
|
||||
} from './constants';
|
||||
import { isFilmstripResizable } from './functions.web';
|
||||
import { isFilmstripResizable, isFilmstripScollVisible, updateRemoteParticipants } from './functions';
|
||||
import './subscriber.any';
|
||||
|
||||
|
||||
@@ -40,12 +38,7 @@ StateListenerRegistry.register(
|
||||
const resizableFilmstrip = isFilmstripResizable(state);
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const gridDimensions = getTileViewGridDimensions(state);
|
||||
const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
|
||||
|
||||
if (!equals(gridDimensions, oldGridDimensions)) {
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
store.dispatch(setTileViewDimensions());
|
||||
}
|
||||
if (resizableFilmstrip) {
|
||||
store.dispatch(setVerticalViewDimensions());
|
||||
@@ -60,11 +53,9 @@ StateListenerRegistry.register(
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => getCurrentLayout(state),
|
||||
/* listener */ (layout, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
switch (layout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
|
||||
store.dispatch(setTileViewDimensions());
|
||||
break;
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setHorizontalViewDimensions());
|
||||
@@ -132,50 +123,6 @@ StateListenerRegistry.register(
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Symbol mapping used for the tile view responsiveness computation.
|
||||
*/
|
||||
const responsiveColumnMapping = {
|
||||
multipleColumns: Symbol('multipleColumns'),
|
||||
singleColumn: Symbol('singleColumn'),
|
||||
twoColumns: Symbol('twoColumns'),
|
||||
twoParticipantsSingleColumn: Symbol('twoParticipantsSingleColumn')
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for changes in the screen size to recompute
|
||||
* the dimensions of the tile view grid and the tiles for responsiveness.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
if (clientWidth < TWO_COLUMN_BREAKPOINT && clientWidth >= ASPECT_RATIO_BREAKPOINT) {
|
||||
// Forcing the recomputation of tiles when screen switches in or out of
|
||||
// the (TWO_COLUMN_BREAKPOINT, ASPECT_RATIO_BREAKPOINT] interval.
|
||||
return responsiveColumnMapping.twoColumns;
|
||||
} else if (clientWidth < ASPECT_RATIO_BREAKPOINT && clientWidth >= SINGLE_COLUMN_BREAKPOINT) {
|
||||
// Forcing the recomputation of tiles when screen switches in or out of
|
||||
// the (ASPECT_RATIO_BREAKPOINT, SINGLE_COLUMN_BREAKPOINT] interval.
|
||||
return responsiveColumnMapping.twoParticipantsSingleColumn;
|
||||
} else if (clientWidth < SINGLE_COLUMN_BREAKPOINT) {
|
||||
// Forcing the recomputation of tiles when screen switches below SINGLE_COLUMN_BREAKPOINT.
|
||||
return responsiveColumnMapping.singleColumn;
|
||||
}
|
||||
|
||||
// Forcing the recomputation of tiles when screen switches above TWO_COLUMN_BREAKPOINT.
|
||||
return responsiveColumnMapping.multipleColumns;
|
||||
},
|
||||
/* listener */ (_, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const gridDimensions = getTileViewGridDimensions(state);
|
||||
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes in the filmstrip width to determine the size of the tiles.
|
||||
*/
|
||||
@@ -193,3 +140,10 @@ StateListenerRegistry.register(
|
||||
/* listener */(_, store) => {
|
||||
store.dispatch(setVerticalViewDimensions());
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes in the filmstrip scroll visibility.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => isFilmstripScollVisible(state),
|
||||
/* listener */ (_, store) => updateRemoteParticipants(store));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SafeAreaView, Text, View } from 'react-native';
|
||||
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { LoadingIndicator } from '../../../base/react';
|
||||
|
||||
import { navigationStyles, TEXT_COLOR } from './styles';
|
||||
|
||||
const ConnectingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<JitsiScreen style = { navigationStyles.connectingScreenContainer }>
|
||||
<View style = { navigationStyles.connectingScreenContent }>
|
||||
<SafeAreaView>
|
||||
<LoadingIndicator
|
||||
color = { TEXT_COLOR }
|
||||
size = 'large'
|
||||
style = { navigationStyles.connectingScreenIndicator } />
|
||||
<Text style = { navigationStyles.connectingScreenText }>
|
||||
{ t('connectingOverlay.joiningRoom') }
|
||||
</Text>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectingPage;
|
||||
@@ -7,7 +7,6 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { DialInSummary } from '../../../invite';
|
||||
import BlankPage from '../../../welcome/components/BlankPage';
|
||||
import { rootNavigationRef } from '../rootNavigationContainerRef';
|
||||
import { screen } from '../routes';
|
||||
import {
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
navigationContainerTheme
|
||||
} from '../screenOptions';
|
||||
|
||||
import ConnectingPage from './ConnectingPage';
|
||||
import ConferenceNavigationContainer
|
||||
from './conference/components/ConferenceNavigationContainer';
|
||||
import WelcomePageNavigationContainer from './welcome/components/WelcomePageNavigationContainer';
|
||||
@@ -33,39 +33,48 @@ type Props = {
|
||||
}
|
||||
|
||||
|
||||
const RootNavigationContainer = ({ isWelcomePageAvailable }: Props) => (
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer
|
||||
independent = { true }
|
||||
ref = { rootNavigationRef }
|
||||
theme = { navigationContainerTheme }>
|
||||
<RootStack.Navigator
|
||||
initialRouteName = { screen.root }>
|
||||
{
|
||||
isWelcomePageAvailable
|
||||
? <RootStack.Screen
|
||||
component = { WelcomePageNavigationContainer }
|
||||
name = { screen.root }
|
||||
options = { drawerNavigatorScreenOptions } />
|
||||
: <RootStack.Screen
|
||||
component = { BlankPage }
|
||||
name = { screen.root } />
|
||||
}
|
||||
<RootStack.Screen
|
||||
component = { DialInSummary }
|
||||
name = { screen.dialInSummary }
|
||||
options = { dialInSummaryScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
const RootNavigationContainer = ({ isWelcomePageAvailable }: Props) => {
|
||||
const initialRouteName = isWelcomePageAvailable
|
||||
? screen.root : screen.connecting;
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer
|
||||
independent = { true }
|
||||
ref = { rootNavigationRef }
|
||||
theme = { navigationContainerTheme }>
|
||||
<RootStack.Navigator
|
||||
initialRouteName = { initialRouteName }>
|
||||
{
|
||||
isWelcomePageAvailable
|
||||
&& <RootStack.Screen
|
||||
component = { WelcomePageNavigationContainer }
|
||||
name = { screen.root }
|
||||
options = { drawerNavigatorScreenOptions } />
|
||||
}
|
||||
<RootStack.Screen
|
||||
component = { ConnectingPage }
|
||||
name = { screen.connecting }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
<RootStack.Screen
|
||||
component = { DialInSummary }
|
||||
name = { screen.dialInSummary }
|
||||
options = { dialInSummaryScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
|
||||
@@ -70,7 +70,6 @@ const ConferenceNavigationContainer = () => {
|
||||
ref = { conferenceNavigationRef }
|
||||
theme = { navigationContainerTheme }>
|
||||
<ConferenceStack.Navigator
|
||||
initialRouteName = { screen.conference.main }
|
||||
screenOptions = {{
|
||||
presentation: 'modal'
|
||||
}}>
|
||||
|
||||
30
react/features/mobile/navigation/components/styles.js
Normal file
30
react/features/mobile/navigation/components/styles.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BoxModel } from '../../../base/styles';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
|
||||
|
||||
export const TEXT_COLOR = BaseTheme.palette.text01;
|
||||
|
||||
/**
|
||||
* Styles of the navigation feature.
|
||||
*/
|
||||
export const navigationStyles = {
|
||||
connectingScreenContainer: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
connectingScreenContent: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
connectingScreenIndicator: {
|
||||
margin: BoxModel.margin
|
||||
},
|
||||
|
||||
connectingScreenText: {
|
||||
color: TEXT_COLOR
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { SET_ROOM } from '../../base/conference/actionTypes';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import { readyToClose } from '../external-api/actions';
|
||||
|
||||
|
||||
import { isWelcomePageAppEnabled } from './components/welcome/functions';
|
||||
import { navigateRoot } from './rootNavigationContainerRef';
|
||||
import { screen } from './routes';
|
||||
|
||||
@@ -16,6 +21,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Debounced sending of `readyToClose`.
|
||||
*/
|
||||
const _sendReadyToClose = debounce(dispatch => {
|
||||
dispatch(readyToClose());
|
||||
}, 2500, { leading: true });
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
@@ -31,15 +42,21 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setRoom(store, next, action) {
|
||||
const { room: oldRoom } = store.getState()['features/base/conference'];
|
||||
function _setRoom({ dispatch, getState }, next, action) {
|
||||
const { room: oldRoom } = getState()['features/base/conference'];
|
||||
const result = next(action);
|
||||
const { room: newRoom } = store.getState()['features/base/conference'];
|
||||
const { room: newRoom } = getState()['features/base/conference'];
|
||||
const isWelcomePageEnabled = isWelcomePageAppEnabled(getState());
|
||||
|
||||
if (!oldRoom && newRoom) {
|
||||
navigateRoot(screen.conference.root);
|
||||
} else if (!newRoom) {
|
||||
navigateRoot(screen.root);
|
||||
if (isWelcomePageEnabled) {
|
||||
navigateRoot(screen.root);
|
||||
} else {
|
||||
// For JitsiSDK, WelcomePage is not available
|
||||
_sendReadyToClose(dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -12,6 +12,7 @@ export const screen = {
|
||||
help: 'Help'
|
||||
},
|
||||
dialInSummary: 'Dial-In Info',
|
||||
connecting: 'Connecting',
|
||||
conference: {
|
||||
root: 'Conference root',
|
||||
main: 'Conference',
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { SafeAreaView, Text, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { LoadingIndicator } from '../../../base/react';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
import styles, { TEXT_COLOR } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The color schemed style of the component.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* The Function to be invoked to translate i18n keys.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an overlay to tell the user that there is an operation in progress in the background during connect
|
||||
* so then the app doesn't seem hung.
|
||||
*/
|
||||
class LoadConfigOverlay extends PureComponent<Props> {
|
||||
/**
|
||||
* Determines whether this overlay needs to be rendered (according to a
|
||||
* specific redux state). Called by {@link OverlayContainer}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
|
||||
* {@code false}, otherwise.
|
||||
*/
|
||||
static needsRender(state: Object) {
|
||||
return Boolean(state['features/overlay'].loadConfigOverlayVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<OverlayFrame>
|
||||
<View style = { styles.loadingOverlayWrapper }>
|
||||
<SafeAreaView>
|
||||
<LoadingIndicator
|
||||
color = { TEXT_COLOR }
|
||||
size = 'large'
|
||||
style = { styles.connectIndicator } />
|
||||
<Text style = { styles.loadingOverlayText }>
|
||||
{ this.props.t('connectingOverlay.joiningRoom') }
|
||||
</Text>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverlayFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default translate(LoadConfigOverlay);
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export { default as LoadConfigOverlay } from './LoadConfigOverlay';
|
||||
export { default as OverlayFrame } from './OverlayFrame';
|
||||
export { default as PageReloadOverlay } from './PageReloadOverlay';
|
||||
|
||||
@@ -2,39 +2,20 @@
|
||||
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { BoxModel, ColorPalette } from '../../../base/styles';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const TEXT_COLOR = BaseTheme.palette.text01;
|
||||
|
||||
/**
|
||||
* The React {@code Component} styles of the overlay feature.
|
||||
*/
|
||||
export default {
|
||||
connectIndicator: {
|
||||
margin: BoxModel.margin
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for a backdrop overlay covering the screen the the overlay is
|
||||
* rendered.
|
||||
*/
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: ColorPalette.black
|
||||
},
|
||||
|
||||
loadingOverlayText: {
|
||||
color: TEXT_COLOR
|
||||
},
|
||||
|
||||
loadingOverlayWrapper: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
backgroundColor: BaseTheme.palette.ui00
|
||||
},
|
||||
|
||||
safeContainer: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
LoadConfigOverlay,
|
||||
PageReloadOverlay
|
||||
} from './components/native';
|
||||
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
*/
|
||||
export function getOverlays(): Array<React$Element<*>> {
|
||||
return [
|
||||
LoadConfigOverlay,
|
||||
PageReloadOverlay
|
||||
];
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ export default {
|
||||
},
|
||||
|
||||
transparentButton: {
|
||||
...baseButton,
|
||||
backgroundColor: 'transparent'
|
||||
backgroundColor: 'transparent',
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
leaveButtonLabel: {
|
||||
|
||||
@@ -31,7 +31,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const CollapsibleList = ({ children, containerStyle, onLongPress, title }: Props) => {
|
||||
const [ collapsed, setCollapsed ] = useState(true);
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
const _toggleCollapsed = useCallback(() => {
|
||||
setCollapsed(!collapsed);
|
||||
}, [ collapsed ]);
|
||||
|
||||
@@ -50,11 +50,21 @@ type Props = {
|
||||
*/
|
||||
_sortedRemoteParticipants: Map<string, string>,
|
||||
|
||||
/**
|
||||
* List of breakout rooms that were created.
|
||||
*/
|
||||
breakoutRooms: Array,
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* List of participants waiting in lobby.
|
||||
*/
|
||||
lobbyParticipants: Array,
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
@@ -180,6 +190,8 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||
_participantsCount,
|
||||
_showInviteButton,
|
||||
_sortedRemoteParticipants,
|
||||
breakoutRooms,
|
||||
lobbyParticipants,
|
||||
t
|
||||
} = this.props;
|
||||
const title = _currentRoom?.name
|
||||
@@ -192,12 +204,19 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||
// Regarding the fact that we have 3 sections, we apply
|
||||
// a certain height percentage for every section in order for all to fit
|
||||
// inside the participants pane container
|
||||
// If there are only meeting participants available,
|
||||
// we take the full container height
|
||||
const onlyMeetingParticipants
|
||||
= breakoutRooms?.length === 0 && lobbyParticipants.length === 0;
|
||||
const containerStyle
|
||||
= _participantsCount > 3 && styles.meetingListContainer;
|
||||
= onlyMeetingParticipants
|
||||
? styles.meetingListFullContainer : styles.meetingListContainer;
|
||||
const finalContainerStyle
|
||||
= _participantsCount > 3 && containerStyle;
|
||||
|
||||
return (
|
||||
<CollapsibleList
|
||||
containerStyle = { containerStyle }
|
||||
containerStyle = { finalContainerStyle }
|
||||
title = { title } >
|
||||
{
|
||||
_showInviteButton
|
||||
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
getCurrentRoomId,
|
||||
isInBreakoutRoom
|
||||
} from '../../../breakout-rooms/functions';
|
||||
import {
|
||||
getKnockingParticipants,
|
||||
getLobbyEnabled
|
||||
} from '../../../lobby/functions';
|
||||
import MuteEveryoneDialog
|
||||
from '../../../video-menu/components/native/MuteEveryoneDialog';
|
||||
import {
|
||||
@@ -63,11 +67,15 @@ const ParticipantsPane = () => {
|
||||
&& participantsCount > 2 && rooms.length > 1;
|
||||
const addBreakoutRoom
|
||||
= _isBreakoutRoomsSupported && !hideAddRoomButton && isLocalModerator;
|
||||
const lobbyEnabled = useSelector(getLobbyEnabled);
|
||||
const lobbyParticipants = useSelector(getKnockingParticipants);
|
||||
|
||||
return (
|
||||
<JitsiScreen style = { styles.participantsPaneContainer }>
|
||||
<LobbyParticipantList />
|
||||
<MeetingParticipantList
|
||||
breakoutRooms = { _isBreakoutRoomsSupported && rooms }
|
||||
lobbyParticipants = { lobbyEnabled && lobbyParticipants }
|
||||
searchString = { searchString }
|
||||
setSearchString = { setSearchString } />
|
||||
{
|
||||
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
},
|
||||
|
||||
lobbyListContent: {
|
||||
height: '20%'
|
||||
height: '16%'
|
||||
},
|
||||
|
||||
lobbyListDescription: {
|
||||
@@ -217,7 +217,11 @@ export default {
|
||||
},
|
||||
|
||||
meetingListContainer: {
|
||||
height: '60%'
|
||||
height: '56%'
|
||||
},
|
||||
|
||||
meetingListFullContainer: {
|
||||
height: '82%'
|
||||
},
|
||||
|
||||
meetingListDescription: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user