Compare commits

...

54 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
dbabdf3351 chore(rn,versions) bump app versions 2022-03-29 15:25:27 +02:00
Saúl Ibarra Corretgé
3bcde982ac chore(rn,versions) bump SDK versions 2022-03-29 15:24:08 +02:00
Saúl Ibarra Corretgé
beb301e691 fix(android,back-button) rework back button handling on Android
We used to have a registry which registered a single handlerwith RN.
THis was registered really early in the app.

When React Navigation was introduced we ddidn't realize it interacts
with the back button. In a stack nagigator it will navigate to the
previous screen. This meant our back button handling was broken.

This commit removes our previous registry and uses the RN back button
handler directly in the 2 components that use it: the conference and
bottom sheets.

Since these handlers are registered after navigation, our handlers are
going to run first so we cna implement the behavior we need, namely to
dismiss an open botom sheet or set the conference in PiP mode.
2022-03-29 15:21:40 +02:00
Calin Chitu
d0836ff651 feat(participants/native) - fix joining breakout room 2022-03-29 15:19:45 +02:00
Saúl Ibarra Corretgé
c028511aaf fix(rn,lobby) fix lobby not showing up on subsequent tries
We need to make sure to hide it explicitly so the Redux state is in sync
with reality.
2022-03-29 15:19:44 +02:00
Saúl Ibarra Corretgé
664552bc05 chore(rn,versions) bump SDK versions 2022-03-28 11:22:36 +02:00
Saúl Ibarra Corretgé
ac35eea08e feat(ios) enable Dropbox recording 2022-03-25 17:24:06 +01:00
Saúl Ibarra Corretgé
172683d645 fix(rn,recording) fix recording dialog state not updating 2022-03-25 17:23:30 +01:00
Calinteodor
858e83b09e fix(rn,recording) fix start button not being enabled 2022-03-25 11:53:30 +02:00
Calin Chitu
e2750ee58e feat(participants/native) - updated container styles 2022-03-25 11:51:01 +02:00
Calin Chitu
fe132581d4 fix(mobile/navigation) - fixed bottom color glitch 2022-03-23 19:09:52 +02:00
Saúl Ibarra Corretgé
21083e6777 fix(mobile,navigation) remove end meeting page
It will just flash for a split second and all it does is dispatch
readyToClose, so do it directly instead.
2022-03-22 12:10:38 +01:00
Calin Chitu
a3c60d5943 fix(mobile/navigation/sdk) - simplified check for sdk 2022-03-22 12:10:38 +01:00
Robert Pintilii
28556e030c fix(settings-dialog) Fix crash (#11191)
Fixes crash when the participant becomes moderator while the dialog is open
2022-03-22 13:10:08 +02:00
Shahab
e54685a566 refactor(premeeting): remove redundant styles from prejoin.scss (#11151)
"prejoin-preview-dropdown-btn" and "prejoin-preview-dropdown-icon" styles are already declared using jss in DropdownButton component.
2022-03-22 09:29:31 +02:00
Hristo Terezov
9ffb3eddb0 fix(shared-video): Can't click controls issue
The Dominant speaker name badge was overlaping the shared video
controls  (audio level, play/pause, etc).
2022-03-21 16:00:24 -05:00
Jaya Allamsetty
d064f607ec chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1399.0.0+1a98d919...v1401.0.0+584a8680
2022-03-21 14:57:59 -04:00
Aaron van Meerten
be454137f9 task: mod_muc_password_whitelist prosody module (#11184) 2022-03-21 11:38:21 -05:00
zygisS22
530369613a fix: removed platformColor from styles 2022-03-21 16:03:59 +02:00
Avram Tudor
6b0b575bd4 fix(highlight) fix notifications not disappearing (#11183) 2022-03-21 15:14:26 +02:00
Calin Chitu
aa5dd92846 patch(react-native-dialog)- replaced PlatformColor with hardcoded colors on Android 2022-03-18 16:14:26 +01:00
Gabriel Borlea
85c505a29d add(highlights): mobile flow (#11168)
* add(highlight): mobile initial flow

* fix(hightlight): get meeting fqn on mobile

* fix(dynamic-branding): extract fqn on mobile

* fix(highlights): remove local fqn extraction and grounp dispatches in batch

* fix(dynamic-branding): check if state is defined in extract fqn
2022-03-18 16:16:56 +02:00
Avram Tudor
b8e12e581f fix(recording) fix incorrect condition for recording notification message (#11167) 2022-03-18 13:21:10 +02:00
Avram Tudor
8456a63a23 fix(highlight) display option to start recording (#11146)
Fix incorrect handling of error case when highlighting moments
Allow users to start recording when trying to highlight while recording not started
2022-03-18 10:19:20 +02:00
Mihaela Dumitru
89394e69b6 fix(config): add missing toolbar button config (#11165) 2022-03-17 18:58:25 +02:00
Saúl Ibarra Corretgé
3ae50c1701 fix(css) remove no longer used AUI classes 2022-03-17 17:28:36 +01:00
Calinteodor
5da40a5fb0 fix(mobile/navigations) added LoadConfigOverlay to RootNavigator (#11067)
Converted LoadConfigOverlay to a JitsiScreen component that right now is part of navigation as ConnectingPage, plus I
separated appNative and other actions into web and native.
2022-03-17 16:13:58 +02:00
Saúl Ibarra Corretgé
f04a01ee3a fix(ios,build) use epoch seconds for build number
Avoids conflicts when a build is rejected by Apple since Fastlane cannot
detect it.
2022-03-17 14:03:24 +01:00
Christoph Settgast
9ad57127d4 feat: Use same recommendedBrowsers page for IE and browsers marked in interface_config.js (#10923)
Reason:
- This is easier overridable than then embedded react page
- Consistenty between IE and other unsupported browsers
2022-03-17 14:56:43 +02:00
Robert Pintilii
a131644dfb fix(video-constraints) Fix calculations (#11161)
Only calculate for Large Video on the web (otherwise native breaks)
Take preferred max into calculations
2022-03-17 14:18:49 +02:00
Mihaela Dumitru
649c1b0669 fix(salesforce): send link notes and default to empty string (#11160) 2022-03-17 12:58:12 +02:00
Robert Pintilii
7bcf7bb686 fix(resziable-filmstrip) Update video constraints on filmstrip resize (#11150)
Update video quality of large video as well
2022-03-16 16:57:30 +02:00
Shahab
8e035b03b0 fix(premeeting): fix undefined breakpoint in media query (#11148) 2022-03-16 15:16:01 +02:00
Saúl Ibarra Corretgé
3b05fd70a3 Revert "deps(rn) react-native-webrtc@1.98.0"
This reverts commit 4fa377c12a.
2022-03-16 12:05:06 +01:00
Shahab
64d32a5005 fix(premeeting): call hooks before any conditional block in ConnectionStatus (#11136) 2022-03-16 10:59:24 +02:00
Aaron van Meerten
fbe15aaa47 feature: patch for muc_owner_allow_kick in prosody 0.12 (#11142) 2022-03-15 21:15:14 -05:00
Hristo Terezov
16bcb1b217 feat(filmstrip): Don't reorder in small meetings. 2022-03-15 14:56:46 -05:00
Jaya Allamsetty
d4c314deb3 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1398.0.0+aed1fa44...v1399.0.0+1a98d919
2022-03-15 15:27:25 -04:00
JohannesPertl
e8e9e24f77 fix(config) add missing notify.hostAskedUnmute 2022-03-15 19:16:56 +01:00
Jaya Allamsetty
9f72c318d6 feat(multi-stream-support) Add screenshare as a second video track to the call.
* feat(multi-stream-support) Add screenshare as a second video track to the call.
This feature is behind a sendMultipleVideoStreams config.js flag. sourceNameSignaling flag also needs to enabled. Sending multiple tracks is currently supported only on endpoints running in unified plan mode. However, clients with source-name signaling enabled and running in plan-b can still receive multiple streams .

* squash: check if there is an existing track before adding camera/desktop

* squash: enable multi-stream only on unified plan endpoints.
2022-03-15 13:24:49 -04:00
Jaya Allamsetty
5f1a4f189c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1389.0.0+313e0dd3...v1398.0.0+aed1fa44
2022-03-15 11:18:22 -04:00
Hristo Terezov
cb9511ef2c fix(resizable-filmstrip): grid view paddings. 2022-03-15 07:48:51 -05:00
Hristo Terezov
e7c4a55add feat(tile-view): Optimize grid dimnsions.
Now the algorithm that calculates the
rows/columns/thumbnail-width/thumbnail-height configuration will
go trough all possible configurations and will choose the one
that covers with thumbnails the biggest area of the window.
2022-03-15 07:48:51 -05:00
Gabriel Borlea
3884862996 add(screenshot-capture): local participants id to participants array in metadata (#11134) 2022-03-15 14:42:26 +02:00
gpatel-fr
8f1c693c3e fix(lang) update french translation 2022-03-15 13:35:45 +01:00
Gabriel Borlea
4d2000a7a4 fix(screenshot-capture): send remote participant id instead of jid (#11132)
* fix(screenshot-capture): send remote participant id instead of jid

* fix: lint errors
2022-03-15 14:05:01 +02:00
Shahab
f6a79860f2 refactor(prejoin) use jss instead of sass in DialInDialog (#11122) 2022-03-15 13:30:38 +02:00
Shahab
4ed2d55cae refactor(participnats-pane) move participant-avatar to commmonStyles (#11120) 2022-03-15 13:29:42 +02:00
Shahab
2dad8163bb refactor(premeeting): use jss instead of sass in ConnectionStatus (#11115) 2022-03-15 13:28:16 +02:00
Shahab
985b37195c refactor(prejoin) use jss instead of sass in CallingDialog (#11117) 2022-03-15 13:16:01 +02:00
Ali Alhaidary
052b588b33 fix(lang) update Arabic translation 2022-03-15 12:13:48 +01:00
Shahab
320aeaa4df refactor(speaker-stats) use jss instead of sass in SpeakerStats (#11121) 2022-03-15 13:02:50 +02:00
Shahab
8c9c1a29be refactor(prejoin) use jss instead of sass in DeviceStatus (#11116) 2022-03-15 12:48:05 +02:00
Shahab
c25fb702c2 refactor(avatar) use jss instead of scss (#11037) 2022-03-15 12:27:40 +02:00
133 changed files with 2768 additions and 1539 deletions

View File

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

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,10 +25,6 @@
}
}
.participant-avatar {
margin: 8px 16px 8px 0;
}
@media (max-width: 580px) {
.participants_pane {
height: 100vh;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
@import 'connection-status';
@import 'device-status';
@import 'lobby';
@import 'premeeting-screens';
@import 'prejoin';

View File

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

View File

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

View File

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

View File

@@ -46,11 +46,6 @@ $reloadProgressBarBg: #0074E0;
/**
* Dialog colors
**/
$auiDialogColor: #eceef1;
$auiDialogBg: #253858;
$auiDialogContentBg: #344563;
$auiBorderColor: #253858;
$dialogTitleFontWeight: 400;
$dialogErrorText: #344563;
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
)

View File

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

View File

@@ -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": "ادخل",

View File

@@ -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",

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
},

View 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();
}
};
}

View 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)));
};
}

View File

@@ -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());
};
}

View 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
});
}

View File

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

View File

@@ -3,6 +3,8 @@
import { toState } from '../base/redux';
import { getServerURL } from '../base/settings';
export * from './functions.any';
declare var interfaceConfig: Object;
/**

View File

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

View File

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

View File

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

View 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)';

View File

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

View File

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

View File

@@ -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) {

View File

@@ -216,6 +216,7 @@ export default [
'testing',
'toolbarButtons',
'toolbarConfig',
'tileView',
'transcribingEnabled',
'useHostPageLocalStorage',
'useTurnUdp',

View File

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

View File

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

View File

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

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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.
*

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*

View File

@@ -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.
*

View File

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

View File

@@ -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 ] }));
}
}

View File

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

View File

@@ -0,0 +1,6 @@
/**
* Prejoin / premeeting screen.
*/
// Maps SCSS variable $prejoinDefaultContentWidth
export const PREJOIN_DEFAULT_CONTENT_WIDTH = '336px';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());
}
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
// @flow
export { default as BackButtonRegistry } from './BackButtonRegistry';

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,6 @@ const ConferenceNavigationContainer = () => {
ref = { conferenceNavigationRef }
theme = { navigationContainerTheme }>
<ConferenceStack.Navigator
initialRouteName = { screen.conference.main }
screenOptions = {{
presentation: 'modal'
}}>

View 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
}
};

View File

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

View File

@@ -12,6 +12,7 @@ export const screen = {
help: 'Help'
},
dialInSummary: 'Dial-In Info',
connecting: 'Connecting',
conference: {
root: 'Conference root',
main: 'Conference',

View File

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

View File

@@ -1,5 +1,4 @@
// @flow
export { default as LoadConfigOverlay } from './LoadConfigOverlay';
export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay';

View File

@@ -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: {

View File

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

View File

@@ -71,8 +71,8 @@ export default {
},
transparentButton: {
...baseButton,
backgroundColor: 'transparent'
backgroundColor: 'transparent',
marginTop: BaseTheme.spacing[3]
},
leaveButtonLabel: {

View File

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

View File

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

View File

@@ -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 } />
{

View File

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