Compare commits

...

28 Commits

Author SHA1 Message Date
Hristo Terezov
b8314aa6b0 fix(Filmstrip): scroll 2021-09-16 17:33:33 -05:00
Jaya Allamsetty
3f7858f043 fix(device-selection) Do not create preview when mic selection is disabled.
This fixes an issue on mobile Safari when audio is lost after the user opens the device selection menu.
2021-09-16 15:58:14 -04:00
hmuresan
6338a712a1 fix(toolbox) fix crash 2021-09-15 16:50:49 +03:00
Jaya Allamsetty
e2be359661 chore(deps) update lib-jitsi-meet.
fix(LocalSdpMunger): do not fake video sdp when screen sharing.
2021-09-15 09:40:14 -04:00
Jaya Allamsetty
932fcd0a6d fix(settings) Disable mic/camera selection on mobile safari.
Creating a preview of the same audio/video track kills the tracks that is already being shared in the conference. Therefore, disable camera/mic selection in the settings dialog while the user is in the call. The devices are selectable from the prejoin screen settings dialog.
2021-09-14 10:16:48 -04:00
Jaya Allamsetty
bff11628e5 fix(tracks) Fix mobile safari issue with startMuted.
On mobile Safari, when a user joins both audio and video muted, browser doesn't playout the remote audio because of a webkit bug. As a workaround, always add the audio track to peerconnection and then mute the track if needed.
2021-09-14 10:15:50 -04:00
Jaya Allamsetty
9add2c3b09 chore(deps) update lib-jitsi-meet.
fix(RTCUtils) Return false for device change checks on mobile Safari.
fix(browser-support): Add audio track to pc always on mobile Safari.
2021-09-14 10:15:00 -04:00
Horatiu Muresan
98018bc4c5 fix(context-menus) Fix participant context menus/toolbar overflow menu
- on ipads, long touch open dialog now opens the context menu to the left of the thumbnail as expected
- on ipads, now we close context menus on tap out
- fix case when participant context menu's height > tileview videos' height causing scroll on videos pane
- keep toolbox open while the overflow menu is shown
- keep remote participant video thumbnail in filmstrip visible even if toolbox is hidden, if context menu is opened
- Fix bug where toolbox could be completely disabled
2021-09-14 12:10:58 +03:00
Horatiu Muresan
ae3d8faf70 feat(toolbar-buttons): Add event for notify overwritten toolbar buttons 2021-09-14 12:10:58 +03:00
Horatiu Muresan
dbecd8cdb2 fix(context-menu) Hide toolbars when participant context menu opened (#9842)
- hide toolbars only when in tile view
- fix community issue: https://github.com/jitsi/jitsi-meet/issues/9818
2021-09-14 12:10:58 +03:00
vp8x8
5140425963 feat(responsive-ui): Keep aspect ratio for filmstrip self view on mobile web (#9848)
* feat(responsive-ui): Keep aspect ratio for filmstrip self view on mobile web

Right now filmstrip displays self view in landscape mode.
With these changes the aspect ratio of the self view will be maintained
so on portrait mode the thumbnail will be displayed vertically.
Of course this makes sense only on mobile web.

* Code review

* Fix height
2021-09-14 09:32:06 +03:00
hmuresan
62fad36120 fix(chat) remote extra line 2021-09-09 17:53:44 +03:00
Horatiu Muresan
212a912d34 feat(config) Add config for disabled sound id's
- unify naming for sound id values
2021-09-09 17:49:12 +03:00
Vlad Piersec
da377e866c fix(config): Add separate entries for the e2ee labels 2021-09-09 13:43:17 +03:00
Vlad Piersec
6186be68e3 fix(e2ee): Remove second warning 2021-09-08 17:29:20 +03:00
Vlad Piersec
6e30a225cc feat(config): Add config option for e2ee label 2021-09-08 17:12:52 +03:00
hmuresan
68cfb46eb6 fix(prejoin) Fix buttons positioning for 3rd party 2021-09-08 16:49:22 +03:00
hmuresan
85522c5d9d fix(external-api): Avoid naming event 'error'
- EventEmmitter treats 'error' as a special case and throws error.
2021-09-02 19:41:22 +03:00
hmuresan
69f0977362 feat(branding) Add premeeting background image overwrite 2021-08-26 17:54:19 +03:00
Horatiu Muresan
df34d7f1f5 feat(errors) Expose errors through Iframe API (#9801) 2021-08-25 16:24:36 +03:00
hmuresan
b72ca4e973 fix(pre-meeting) Hide invite button for JaaS 2021-08-25 14:21:29 +03:00
Avram Tudor
45e51c1e8c Improve premeeting screens ux (#9726)
* feat(prejoin) move invite to toolbar section

* feat(premeeting) redesign prejoin and lobby screens

* code review changes

* fix prejoin flicker and avatar id

* fix password error message and native lobby dialog close position
2021-08-25 14:21:21 +03:00
hmuresan
068cf132bd feat(lobby) Add sound for participant knocking 2021-08-25 14:19:20 +03:00
hmuresan
147f27fa67 fix(notifications) Add timeout for video/audio lost notifs 2021-08-25 14:19:01 +03:00
Дамян Минков
4ae9bc858e fix: Fixes dot shown left of logo.
Empty list with border is showing 2px dot.
2021-08-24 19:12:30 -05:00
robertpin
33bd880116 Added default state to persistance 2021-08-24 10:38:02 +03:00
robertpin
30802be752 feat(reaction-sounds) Added sounds for reactions (#9775) (#9776)
* Added sounds for reactions

* Updated reactions list

* Added reactions to sound settings

* Added support for multiple sounds

* Added feature flag for sounds

* Updated sound settings

Moved reactions toggle at the top of the list

* Added disable reaction sounds notification

* Added reaction button zoom for burst intensity

* Fixed raise hand sound

* Fixed register sounds for reactions

* Changed boo emoji

* Updated sounds

* Fixed lint errors

* Fixed reaction sounds file names

* Fix raise hand sound

Play sound only on raise hand not on lower hand

* Fixed types for sound constants

* Fixed type for raise hand sound constant
2021-08-23 13:07:05 +03:00
hmuresan
ac1990df3b fix(drawer-menu) Allow scroll on drawer menu items 2021-08-20 16:48:08 +03:00
152 changed files with 2303 additions and 1130 deletions

View File

@@ -56,6 +56,7 @@ import {
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
import { isIosMobileBrowser } from './react/features/base/environment/utils';
import {
browser,
isFatalJitsiConnectionError,
@@ -834,7 +835,13 @@ export default {
this._initDeviceList(true);
if (initialOptions.startWithAudioMuted) {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
// Always add the audio track to the peer connection and then mute the track on mobile Safari
// because of a known issue where audio playout doesn't happen if the user joins audio and video muted.
if (isIosMobileBrowser()) {
this.muteAudio(true, true);
} else {
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
}
}
return this.startConference(con, localTracks);
@@ -1322,8 +1329,8 @@ export default {
APP.store.dispatch(conferenceWillJoin(room));
// Filter out the tracks that are muted.
const tracks = localTracks.filter(track => !track.isMuted());
// Filter out the tracks that are muted (except on mobile Safari).
const tracks = isIosMobileBrowser() ? localTracks : localTracks.filter(track => !track.isMuted());
this._setLocalAudioVideoStreams(tracks);
this._room = room; // FIXME do not use this
@@ -2261,7 +2268,9 @@ export default {
// Remove the tracks from the peerconnection.
for (const track of localTracks) {
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO) {
// Always add the track on mobile Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted.
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO && !isIosMobileBrowser()) {
promises.push(this.useAudioStream(null));
}
if (videoMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.VIDEO) {

View File

@@ -507,6 +507,47 @@ var config = {
// '__end'
// ],
// Toolbar buttons which have their click event exposed through the API on
// `toolbarButtonClicked` event instead of executing the normal click routine.
// buttonsWithNotifyClick: [
// 'camera',
// 'chat',
// 'closedcaptions',
// 'desktop',
// 'download',
// 'embedmeeting',
// 'etherpad',
// 'feedback',
// 'filmstrip',
// 'fullscreen',
// 'hangup',
// 'help',
// 'invite',
// 'livestreaming',
// 'microphone',
// 'mute-everyone',
// 'mute-video-everyone',
// 'participants-pane',
// 'profile',
// 'raisehand',
// 'recording',
// 'security',
// 'select-background',
// 'settings',
// 'shareaudio',
// 'sharedvideo',
// 'shortcuts',
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'videoquality',
// '__end'
// ],
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [],
// Stats
//
@@ -640,13 +681,38 @@ var config = {
// userRegion: "asia"
},
// Array<string> of disabled sounds.
// Possible values:
// - 'E2EE_OFF_SOUND'
// - 'E2EE_ON_SOUND'
// - 'INCOMING_MSG_SOUND'
// - 'KNOCKING_PARTICIPANT_SOUND'
// - 'LIVE_STREAMING_OFF_SOUND'
// - 'LIVE_STREAMING_ON_SOUND'
// - 'NO_AUDIO_SIGNAL_SOUND'
// - 'NOISY_AUDIO_INPUT_SOUND'
// - 'OUTGOING_CALL_EXPIRED_SOUND'
// - 'OUTGOING_CALL_REJECTED_SOUND'
// - 'OUTGOING_CALL_RINGING_SOUND'
// - 'OUTGOING_CALL_START_SOUND'
// - 'PARTICIPANT_JOINED_SOUND'
// - 'PARTICIPANT_LEFT_SOUND'
// - 'RAISE_HAND_SOUND'
// - 'RECORDING_OFF_SOUND'
// - 'RECORDING_ON_SOUND'
// - 'TALK_WHILE_MUTED_SOUND'
// disabledSounds: [],
// DEPRECATED! Use `disabledSounds` instead.
// Decides whether the start/stop recording audio notifications should play on record.
// disableRecordAudioNotification: false,
// DEPRECATED! Use `disabledSounds` instead.
// Disables the sounds that play when other participants join or leave the
// conference (if set to true, these sounds will not be played).
// disableJoinLeaveSounds: false,
// DEPRECATED! Use `disabledSounds` instead.
// Disables the sounds that play when a chat message is received.
// disableIncomingMessageSound: false,
@@ -760,7 +826,11 @@ var config = {
// The anchor url used when clicking the logo image
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png'
logoImageUrl: 'https://example.com/logo-img.png',
// Overwrite for pool of background images for avatars
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
// The lobby/prejoin screen background
premeetingBackground: 'url(https://example.com/premeeting-background.png)'
}
*/
// dynamicBrandingUrl: '',
@@ -821,6 +891,7 @@ var config = {
disableRemoteControl
displayJids
externalConnectUrl
e2eeLabels
firefox_fake_device
googleApiApplicationClientID
iAmRecorder

View File

@@ -23,7 +23,7 @@
max-height: calc(80vh - 64px);
background: #242528;
border-radius: 16px 16px 0 0;
overflow-y: hidden;
overflow-y: scroll;
margin-bottom: env(safe-area-inset-bottom, 0);
width: 100%;

View File

@@ -206,13 +206,3 @@
bottom: 0;
width: 35%;
}
/**
* Resizes elements width to fill the whole screen width with some margin
*/
@mixin adjust-for-max-width($width, $margin) {
@media (max-width: $width) {
margin: 0 $margin;
width: $width - 2 * $margin;
}
}

View File

@@ -1,153 +0,0 @@
.prejoin {
&-input-area {
margin: 0 auto;
text-align: center;
&-label {
display: block;
margin-bottom: 5px;
color: #ffffff;
font-weight: 300;
font-size: 15px;
line-height: 24px;
}
}
&-title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
&-text-btns {
display: flex;
justify-content: space-between;
}
&-input-label {
color: #A4B8D1;
font-size: 13px;
line-height: 20px;
margin-top: 32px 0 8px 0;
text-align: center;
width: 100%;
}
&-checkbox {
border: 0;
height: 16px;
margin-right: 8px;
padding: 0;
width: 16px;
}
&-checkbox-container {
margin-bottom: 14px;
width: 100%;
}
&-error {
color: white;
background-color: rgba(225, 45, 45, 0.6);
border-radius: 3px;
width: 100%;
padding: 2px;
box-sizing: border-box;
margin-top: 4px;
font-size: 13px;
text-align: center;
}
}
@mixin name-placeholder {
color: #fff;
font-weight: 300;
opacity: 0.6;
}
.prejoin-preview {
&-status {
align-items: center;
align-self: stretch;
bottom: 0;
color: #fff;
display: flex;
font-size: 13px;
min-height: 24px;
justify-content: center;
position: absolute;
text-align: center;
width: 100%;
z-index: 1;
&--warning {
background: rgba(241, 173, 51, 1);
}
&--ok {
background: rgba(49, 183, 106, 1);
}
}
&-icon {
background-position: center;
background-repeat: no-repeat;
display: inline-block;
height: 16px;
margin-right: 8px;
width: 16px;
}
&-error-desc {
margin-right: 4px;
color: #fff;
font-weight: bold;
}
.settings-button-container {
width: 49px;
margin: 0 8px;
}
&-dropdown-btns {
width: 320px;
padding: 8px 0;
@include adjust-for-max-width(320px, 8px);
}
&-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 {
margin-top: 16px;
& > div:nth-child(2) {
background: #fff;
padding: 0;
}
}
}

View File

@@ -48,6 +48,13 @@
display: flex;
align-items: center;
justify-content: center;
transition: font-size ease .1s;
@for $i from 1 through 12 {
&.increase-#{$i}{
font-size: calc(20px + #{$i}px);
}
}
}
}

View File

@@ -334,7 +334,7 @@
border-radius: 0;
display: flex;
justify-content: space-evenly;
padding: 6px 0;
padding: 8px 0;
width: 100%;
}

View File

@@ -264,3 +264,9 @@ $chromeExtensionBannerRightInMeeeting: 10px;
*/
$smallScreen: 700px;
$verySmallScreen: 500px;
/**
* Prejoin / premeeting screen
*/
$prejoinDefaultContentWidth: 336px;

View File

@@ -79,7 +79,6 @@ $flagsImagePath: "../images/";
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'labels';
@import 'lobby';
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@@ -95,15 +94,12 @@ $flagsImagePath: "../images/";
@import 'meter';
@import 'audio-preview';
@import 'video-preview';
@import 'prejoin';
@import 'prejoin-dialog';
@import 'premeeting/main';
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
@import 'drawer';
@import 'participants-pane';
@import 'reactions-menu';

View File

@@ -1,32 +1,24 @@
.con-status {
border-radius: 6px;
color: #fff;
font-size: 12px;
letter-spacing: 0.16px;
line-height: 16px;
position: absolute;
top: 24px;
width: 100%;
z-index: $toolbarZ + 3;
&-container {
border-radius: 3px;
color: #fff;
font-size: 13px;
line-height: 13px;
margin: 0 auto;
width: 320px;
@include adjust-for-max-width(320px, 8px);
}
&-header {
background: rgba(28, 32, 37, .5);
background-color: rgba(0, 0, 0, 0.7);
align-items: center;
display: flex;
justify-content: space-between;
padding: 8px 12px;
}
&-circle {
border-radius: 50%;
display: inline-block;
padding: 4px;
margin: 8px;
margin-right: 16px;
}
&--good {
@@ -42,14 +34,7 @@
}
&-arrow {
height: 36px;
width: 36px;
border-radius: 3px;
margin-left: 8px;
margin-right: 2px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
transition: background-color 0.16s ease-out;
&--up {
@@ -70,7 +55,7 @@
}
&-details {
background: rgba(28, 32, 37, .5);
background-color: rgba(0, 0, 0, 0.7);
border-top: 1px solid #5E6D7A;
padding: 16px;
transition: opacity 0.16s ease-out;

View File

@@ -0,0 +1,35 @@
.device {
&-status {
align-items: center;
align-self: stretch;
color: #fff;
display: flex;
font-size: 14px;
font-weight: 400;
justify-content: center;
line-height: 20px;
margin-top: 8px;
padding: 6px;
text-align: center;
}
&-icon {
background-position: center;
background-repeat: no-repeat;
display: inline-block;
height: 16px;
margin-right: 10px;
width: 16px;
&--warning {
svg path {
fill: rgba(241, 173, 51, 1);
}
}
&--ok {
svg path {
fill: #189b55;
}
}
}
}

View File

@@ -1,18 +1,21 @@
#lobby-screen {
.content {
.lobby-screen {
font-size: 16px;
font-weight: 400;
line-height: 26px;
.container {
align-items: center;
display: flex;
flex-direction: column;
&-content {
align-items: center;
display: flex;
flex-direction: column;
.spinner {
margin: 30px;
}
.spinner {
margin: 8px;
}
.joining-message {
margin: 10px;
}
.joining-message {
color: white;
margin: 24px auto;
text-align: center;
}
}
}
@@ -51,6 +54,10 @@
top: 30px;
z-index: $toolbarZ + 1;
&:empty {
border: none;
}
&.toolbox-visible {
// Same as toolbox subject position
top: 120px;
@@ -64,7 +71,7 @@
button {
align-self: stretch;
margin: 8px 0;
margin-bottom: 8px 0;
padding: 12px;
transition: .2s transform ease;

View File

@@ -0,0 +1,7 @@
@import 'connection-status';
@import 'device-status';
@import 'lobby';
@import 'premeeting-screens';
@import 'prejoin';
@import 'prejoin-dialog';
@import 'prejoin-third-party';

View File

@@ -0,0 +1,40 @@
$sidePanelWidth: 300px;
.prejoin-third-party {
flex-direction: column-reverse;
.content {
height: auto;
margin: 0 auto;
width: auto;
.new-toolbox {
width: auto;
}
}
#preview {
background-color: transparent;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
.avatar {
display: none;
}
}
&.splash {
.content {
margin-left: calc((100% - #{$prejoinDefaultContentWidth} + #{$sidePanelWidth}) / 2)
}
}
&.guest {
.content {
margin-bottom: auto;
}
}
}

View File

@@ -0,0 +1,73 @@
.prejoin {
&-input-area {
width: 100%;
}
&-checkbox-container {
margin-bottom: 16px;
width: 100%;
text-align: center;
}
&-error {
color: white;
background-color: #E04757;
border-radius: 6px;
padding: 4px;
box-sizing: border-box;
margin-bottom: 16px;
margin-top: -8px;
font-size: 12px;
text-align: center;
width: 100%;
}
}
.prejoin-preview {
&-dropdown-btns {
padding: 8px 0;
width: calc(100% - 48px);
}
&-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%;
/**
* Override default InlineDialog behaviour, since it does not play nicely with relative widths
*/
& > div:nth-child(2) {
background: #fff;
padding: 0;
position: absolute !important;
top: 48px !important;
transform: none !important;
width: 100%;
}
}
}

View File

@@ -1,47 +1,27 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.premeeting-screen {
align-items: stretch;
background: radial-gradient(50% 50% at 50% 50%, #2A3A4B 20.83%, #1E2A36 100%);
background: #292929;
bottom: 0;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
&-avatar {
background-color: #A4B8D1;
margin-bottom: 24px;
text {
fill: black;
font-size: 26px;
font-weight: 400;
}
}
.action-btn {
border-radius: 3px;
border-radius: 6px;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
font-size: 14px;
line-height: 24px;
margin-bottom: 16px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 320px;
@include adjust-for-max-width(320px, 8px);
width: 100%;
&.primary {
background: #0376DA;
@@ -49,8 +29,8 @@
}
&.secondary {
background: transparent;
border: 1px solid #5E6D7A;
background: #3D3D3D;
border: 1px solid transparent;
}
&.text {
@@ -96,130 +76,150 @@
.content {
align-items: center;
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 24px;
flex-shrink: 0;
height: 100%;
margin: 0 110px;
padding: 24px 0 16px;
position: relative;
width: $prejoinDefaultContentWidth;
z-index: $toolbarZ + 2;
.title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
.copy-meeting {
&-controls {
align-items: center;
cursor: pointer;
color: #fff;
display: flex;
flex-direction: column;
font-size: 15px;
font-weight: 300;
justify-content: center;
line-height: 24px;
margin-bottom: 16px;
margin: auto;
width: 100%;
.url {
background: rgba(28, 32, 37, 0.5);
border-radius: 4px;
display: flex;
padding: 8px 10px;
transition: background 0.16s ease-out;
&:hover {
background: #1C2025;
.title {
color: #fff;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.015;
line-height: 36px;
margin-bottom: 32px;
text-align: center;
}
input.field {
background-color: white;
border: none;
outline: none;
border-radius: 6px;
font-size: 14px;
line-height: 20px;
margin-bottom: 16px;
color: #1C2025;
padding: 10px 16px;
text-align: center;
width: 100%;
&.error {
border: 1px solid #E04757;
}
&.done {
background: #31B76A;
}
.jitsi-icon {
margin-left: 10px;
&.focused {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
}
}
.copy-button{
width: 298px;
}
.copy-meeting-text {
width: 266px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#new-toolbox {
bottom: 0;
margin-bottom: 16px;
position: relative;
transition: none;
&:hover {
align-self: stretch;
}
textarea {
border-width: 0;
height: 0;
opacity: 0;
padding: 0;
width: 0;
}
}
input.field {
background-color: white;
border: none;
outline: none;
border-radius: 3px;
font-size: 15px;
line-height: 24px;
color: #1C2025;
padding: 8px 0;
text-align: center;
width: 320px;
@include adjust-for-max-width(320px, 8px);
&.error {
box-shadow: 0px 0px 4px 3px rgba(225, 45, 45, 0.4);
}
&.focused {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
.toolbox-content,
.toolbox-content-wrapper,
.toolbox-content-items {
box-sizing: border-box;
width: 100%;
}
}
}
}
.media-btn-container {
display: flex;
justify-content: center;
margin: 24px 0 16px 0;
width: 100%;
&> div {
margin: 0 12px;
@media (max-width: 1000px) {
flex-direction: column-reverse;
.content {
height: auto;
margin: 0 auto;
}
.con-status {
margin: 24px auto;
position: fixed;
top: 0;
width: $prejoinDefaultContentWidth;
}
}
@media (max-width: 400px) {
.content {
padding: 16px;
width: 100%;
.title {
font-size: 20px;
line-height: 28px;
letter-spacing: -0.012;
margin-bottom: 24px;
}
}
.con-status {
margin: 16px;
width: calc(100% - 32px);
}
input.field {
font-size: 16px;
padding: 14px 16px;
}
.action-btn {
font-size: 16px;
padding: 11px 16px;
}
.toolbox-content-items {
border-radius: 0;
display: flex;
justify-content: space-evenly;
padding: 8px 0;
}
}
input::placeholder {
color: #040404;
}
}
#preview {
background: #040404;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: absolute;
width: 100%;
&.no-video {
background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
text-align: center;
}
.avatar {
background: #A4B8D1;
margin: 0 auto;
background: #0045B3;
text {
fill: white;
font-size: 26px;
font-weight: 400;
}
}
video {
height: 100%;
object-fit: cover;
position: absolute;
width: 100%;
}
}
@@ -241,16 +241,14 @@
}
.toggle-button {
border-radius: 3px;
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 13px;
height: 40px;
margin: 0 auto;
transition: background 0.16s ease-out;
width: 320px;
@include adjust-for-max-width(320px, 8px);
@include flex-centered();
svg {

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
"embedMeeting": "Besprechung einbetten",
"error": "Fehler",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",

View File

@@ -179,7 +179,7 @@
"e2eeLabel": "Ŝlosilo",
"e2eeTitle": "Tutvoja ĉifrado",
"e2eeWarning": "<br /><p><strong>ATENTIGO:</strong> Ne ĉiuj partoprenantoj en ĉi tiu kunveno ŝajnas havi subtenon de tutvoja ĉifrado. Se vi ŝaltos ĝin, ili ne povos vidi aŭ aŭdi vin.</p>",
"enterDisplayName": "Please enter your name here",
"enterDisplayName": "Enter your name here",
"error": "Eraro",
"externalInstallationMsg": "Vi devas instali nian ekranvidadan kromprogramon.",
"externalInstallationTitle": "Kromprogramo bezonata",

View File

@@ -203,7 +203,6 @@
"e2eeLabel": "Aktibatu puntutik punturako zifratzea",
"e2eeWarning": "OHARRA: bileraren partaide guztiek ezin dute puntutik punturako zifratzea erabili. Aukera hau aktibatzen baduzu, batzuk ezingo zaituzte ikusi eta entzun.",
"enterDisplayName": "Sartu zure izena hemen",
"enterDisplayNameToJoin": "Mesedez idatzi zure izena bileran sartzeko",
"embedMeeting": "Kapsulatu bilera",
"error": "Errorea",
"gracefulShutdown": "Zerbitzua ez dago erabilgarri mantentze-lanak direla eta. Saiatu berriro beranduago.",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Activer le chiffrement de Bout-en-Bout",
"e2eeWarning": "ATTENTION : Tous les participants de cette réunion ne semblent pas prendre en charge le chiffrement de Bout-en-Bout. Si vous activez le chiffrement, ils ne pourront ni vous voir, ni vous entendre.",
"enterDisplayName": "Merci de saisir votre nom ici",
"enterDisplayNameToJoin": "Merci de saisir votre nom pour rejoindre",
"embedMeeting": "Intégrer la réunion",
"error": "Erreur",
"gracefulShutdown": "Notre service est actuellement en maintenance. Veuillez réessayer plus tard.",

View File

@@ -175,7 +175,7 @@
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"done": "Done",
"enterDisplayName": "Please enter your name here",
"enterDisplayName": "Enter your name here",
"error": "Error",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
"externalInstallationTitle": "Extension required",

View File

@@ -188,7 +188,6 @@
"e2eeLabel": "Habilitar encriptação de ponta a ponta",
"e2eeWarning": "AVISO: Nem todos os participantes neste encontro parecem ter apoio para a encriptação de ponta a ponta. Se o permitir, eles não o poderão ver nem ouvir.",
"enterDisplayName": "Digite o seu nome aqui",
"enterDisplayNameToJoin": "Por favor, digite o seu nome para participar",
"embedMeeting": "Embutir reunião",
"error": "Erro",
"gracefulShutdown": "O nosso serviço está atualmente em manutenção. Por favor, tente novamente mais tarde.",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Enable End-to-End Encryption",
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
"enterDisplayName": "Digite seu nome aqui",
"enterDisplayNameToJoin": "Digite seu nome para participar",
"embedMeeting": "Reunião em formato compacto",
"error": "Erro",
"gracefulShutdown": "Nosso serviço está em manutenção. Tente novamente mais tarde.",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Aktivizo Fshehtëzim Skaj-më-Skaj",
"e2eeWarning": "KUJDES: Jo të gjithë pjesëmarrësit në këtë takim duket të kenë mbulim për fshehtëzim Skaj-më-Skaj. Në e aktivizofshi, ata sdo të jenë në gjendje tju shohin apo dëgjojnë.",
"enterDisplayName": "Ju lutemi, jepni këtu emrin tuaj",
"enterDisplayNameToJoin": "Që të merrni pjesë, ju lutemi, jepni emrin tuaj",
"embedMeeting": "Trupëzoni takim",
"error": "Gabim",
"gracefulShutdown": "Shërbimi ynë është aktualisht i ndërprerë, për punë mirëmbajtjeje. Ju lutemi, riprovoni më vonë.",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "啟用端對端加密",
"e2eeWarning": "警告:看來不是每位此會議的參與者都有啟用端對端加密,如果您啟用了,他們可能無法看/聽到您。",
"enterDisplayName": "請在此輸入您自己的名字",
"enterDisplayNameToJoin": "請輸入您的名字以加入",
"embedMeeting": "嵌入會議",
"error": "錯誤",
"gracefulShutdown": "我們的服務目前關閉維護中,請稍後再試。",

View File

@@ -205,11 +205,10 @@
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"done": "Done",
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
"e2eeLabel": "Enable End-to-End Encryption",
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
"enterDisplayName": "Please enter your name here",
"enterDisplayNameToJoin": "Please enter your name to join",
"enterDisplayName": "Enter your name here",
"embedMeeting": "Embed meeting",
"error": "Error",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
@@ -588,6 +587,7 @@
"moderationStoppedTitle": "Moderation stopped",
"moderationToggleDescription": "by {{participantDisplayName}}",
"raiseHandAction": "Raise hand",
"reactionSounds": "Disable sounds",
"groupTitle": "Notifications"
},
"participantsPane": {
@@ -662,7 +662,7 @@
"joinWithoutAudio": "Join without audio",
"initiated": "Call initiated",
"linkCopied": "Link copied to clipboard",
"lookGood": "It sounds like your microphone is working properly",
"lookGood": "Your microphone is working properly",
"or": "or",
"premeeting": "Pre meeting",
"showScreen": "Enable pre meeting screen",
@@ -763,6 +763,7 @@
"participantJoined": "Participant Joined",
"participantLeft": "Participant Left",
"playSounds": "Play sound on",
"reactions": "Meeting reactions",
"sameAsSystem": "Same as system ({{label}})",
"selectAudioOutput": "Audio output",
"selectCamera": "Camera",
@@ -852,7 +853,6 @@
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"participants": "Participants",
"party": "Party Popper",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
@@ -869,6 +869,7 @@
"shareYourScreen": "Start / Stop sharing your screen",
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"silence": "Silence",
"speakerStats": "Toggle speaker statistics",
"surprised": "Surprised",
"tileView": "Toggle tile view",
@@ -893,6 +894,7 @@
"clap": "Clap",
"closeChat": "Close chat",
"closeReactionsMenu": "Close reactions menu",
"disableReactionSounds": "You can disable reaction sounds for this meeting",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
@@ -928,7 +930,6 @@
"openChat": "Open chat",
"openReactionsMenu": "Open reactions menu",
"participants": "Participants",
"party": "Celebration",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
@@ -938,7 +939,7 @@
"reactionClap": "Send clap reaction",
"reactionLaugh": "Send laugh reaction",
"reactionLike": "Send thumbs up reaction",
"reactionParty": "Send party popper reaction",
"reactionSilence": "Send silence reaction",
"reactionSurprised": "Send surprised reaction",
"security": "Security options",
"Settings": "Settings",
@@ -946,6 +947,7 @@
"sharedvideo": "Share video",
"shareRoom": "Invite someone",
"shortcuts": "View shortcuts",
"silence": "Silence",
"speakerStats": "Speaker stats",
"startScreenSharing": "Start screen sharing",
"startSubtitles": "Start subtitles",
@@ -1088,7 +1090,7 @@
"admitAll": "Admit all",
"knockingParticipantList": "Knocking participant list",
"allow": "Allow",
"backToKnockModeButton": "No password, ask to join instead",
"backToKnockModeButton": "Ask to join",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
@@ -1098,6 +1100,7 @@
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "Enter meeting password",
"enterPasswordTitle": "Enter password to join meeting",
"errorMissingPassword": "Please enter the meeting password",
"invalidPassword": "Invalid password",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
"joinWithPasswordMessage": "Trying to join with password, please wait...",

View File

@@ -1325,6 +1325,32 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that an error occured.
*
* @param {Object} error - The error.
* @returns {void}
*/
notifyError(error: Object) {
this._sendEvent({
name: 'error-occurred',
error
});
}
/**
* Notify external application ( if API is enabled) that a toolbar button was clicked.
*
* @param {string} key - The key of the toolbar button.
* @returns {void}
*/
notifyToolbarButtonClicked(key: string) {
this._sendEvent({
name: 'toolbar-button-clicked',
key
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -81,6 +81,7 @@ const events = {
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
'error-occurred': 'errorOccurred',
'endpoint-text-message-received': 'endpointTextMessageReceived',
'feedback-submitted': 'feedbackSubmitted',
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
@@ -110,7 +111,8 @@ const events = {
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged'
'tile-view-changed': 'tileViewChanged',
'toolbar-button-clicked': 'toolbarButtonClicked'
};
/**

4
package-lock.json generated
View File

@@ -11087,8 +11087,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#6eaffc4b11b42d851a17ed941a19a9513265d3bb",
"from": "github:jitsi/lib-jitsi-meet#6eaffc4b11b42d851a17ed941a19a9513265d3bb",
"version": "github:jitsi/lib-jitsi-meet#a074f0459db4ce3ef631655f7f5a61f51442354d",
"from": "github:jitsi/lib-jitsi-meet#a074f0459db4ce3ef631655f7f5a61f51442354d",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6eaffc4b11b42d851a17ed941a19a9513265d3bb",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a074f0459db4ce3ef631655f7f5a61f51442354d",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

@@ -43,9 +43,11 @@ export class App extends AbstractApp {
*/
_renderDialogContainer() {
return (
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
<JitsiThemeProvider>
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
</JitsiThemeProvider>
);
}
}

View File

@@ -1,21 +1,20 @@
// @flow
import { setPrejoinPageVisibility, setSkipPrejoinOnReload } from '../../prejoin';
import { PREJOIN_SCREEN_STATES } from '../../prejoin/constants';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from './actionTypes';
import './middleware.any';
declare var APP: Object;
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { enableForcedReload } = getState()['features/base/config'];
switch (action.type) {
case CONFERENCE_JOINED: {
if (enableForcedReload) {
dispatch(setPrejoinPageVisibility(false));
dispatch(setPrejoinPageVisibility(PREJOIN_SCREEN_STATES.HIDDEN));
dispatch(setSkipPrejoinOnReload(false));
}

View File

@@ -19,6 +19,7 @@ export default [
'apiLogLevels',
'avgRtpStatsN',
'backgroundAlpha',
'buttonsWithNotifyClick',
/**
* The display name of the CallKit call representing the conference/meeting
@@ -82,6 +83,7 @@ export default [
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disabledSounds',
'disableFilmstripAutohiding',
'disableInitialGUM',
'disableH264',
@@ -105,6 +107,7 @@ export default [
'doNotStoreRoom',
'doNotFlipLocalVideo',
'dropbox',
'e2eeLabels',
'e2eping',
'enableDisplayNameInStats',
'enableEmailInStats',
@@ -130,6 +133,7 @@ export default [
'forceTurnRelay',
'gatherStats',
'googleApiApplicationClientID',
'hiddenPremeetingButtons',
'hideConferenceSubject',
'hideRecordingLabel',
'hideParticipantsStats',

View File

@@ -46,3 +46,13 @@ export const TOOLBAR_BUTTONS = [
'toggle-camera',
'videoquality'
];
/**
* The toolbar buttons to show on premeeting screens.
*/
export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background', 'invite', 'settings' ];
/**
* The toolbar buttons to show on 3rdParty prejoin screen.
*/
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];

View File

@@ -66,11 +66,11 @@ ReducerRegistry.register('features/base/config', (state = _getInitialState(), ac
error: undefined,
/**
* The URL of the location associated with/configured by this
* configuration.
*
* @type URL
*/
* The URL of the location associated with/configured by this
* configuration.
*
* @type URL
*/
locationURL: action.locationURL
};
@@ -84,11 +84,11 @@ ReducerRegistry.register('features/base/config', (state = _getInitialState(), ac
if (state.locationURL === action.locationURL) {
return {
/**
* The {@link Error} which prevented the loading of the
* configuration of the associated {@code locationURL}.
*
* @type Error
*/
* The {@link Error} which prevented the loading of the
* configuration of the associated {@code locationURL}.
*
* @type Error
*/
error: action.error
};
}
@@ -194,6 +194,25 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
}
newValue.disabledSounds = newValue.disabledSounds || [];
if (oldValue.disableJoinLeaveSounds) {
newValue.disabledSounds.unshift('PARTICIPANT_LEFT_SOUND', 'PARTICIPANT_JOINED_SOUND');
}
if (oldValue.disableRecordAudioNotification) {
newValue.disabledSounds.unshift(
'RECORDING_ON_SOUND',
'RECORDING_OFF_SOUND',
'LIVE_STREAMING_ON_SOUND',
'LIVE_STREAMING_OFF_SOUND'
);
}
if (oldValue.disableIncomingMessageSound) {
newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
}
if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
newValue.audioQuality = {
opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,

View File

@@ -54,4 +54,13 @@ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
*/
export const SET_LOCATION_URL = 'SET_LOCATION_URL';
/**
* The type of (redux) action which tells whether connection info should be displayed
* on context menu.
*
* {
* type: SHOW_CONNECTION_INFO,
* showConnectionInfo: boolean
* }
*/
export const SHOW_CONNECTION_INFO = 'SHOW_CONNECTION_INFO';

View File

@@ -50,6 +50,7 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
}
};
const WARNING_DISPLAY_TIMER = 4000;
/**
* A listener for device permissions changed reported from lib-jitsi-meet.
@@ -133,7 +134,7 @@ MiddlewareRegistry.register(store => next => action => {
description: additionalCameraErrorMsg,
descriptionKey: cameraErrorMsg,
titleKey
}));
}, WARNING_DISPLAY_TIMER));
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(setDeviceStatusWarning(titleKey));
@@ -162,7 +163,7 @@ MiddlewareRegistry.register(store => next => action => {
description: additionalMicErrorMsg,
descriptionKey: micErrorMsg,
titleKey
}));
}, WARNING_DISPLAY_TIMER));
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(setDeviceStatusWarning(titleKey));

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666687 9.00002C0.666687 13.6024 4.39765 17.3334 9.00002 17.3334C13.6024 17.3334 17.3334 13.6024 17.3334 9.00002C17.3334 4.39765 13.6024 0.666687 9.00002 0.666687C4.39765 0.666687 0.666687 4.39765 0.666687 9.00002ZM13.7119 5.86983C13.3639 5.56869 12.8376 5.60672 12.5365 5.95477L7.55616 11.711L5.42261 9.57743C5.09717 9.25199 4.56954 9.25199 4.2441 9.57743C3.91866 9.90287 3.91866 10.4305 4.2441 10.7559L7.01102 13.5229C7.35319 13.865 7.91386 13.8448 8.23047 13.4789L13.7969 7.04527C14.098 6.69722 14.06 6.17096 13.7119 5.86983Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@@ -26,6 +26,7 @@ export { default as IconChat } from './chat.svg';
export { default as IconChatSend } from './send.svg';
export { default as IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg';
export { default as IconCheckSolid } from './check-solid.svg';
export { default as IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseX } from './close-x.svg';

View File

@@ -370,14 +370,9 @@ function _localParticipantLeft({ dispatch }, next, action) {
*/
function _maybePlaySounds({ getState, dispatch }, action) {
const state = getState();
const { startAudioMuted, disableJoinLeaveSounds } = state['features/base/config'];
const { startAudioMuted } = state['features/base/config'];
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
// If we have join/leave sounds disabled, don't play anything.
if (disableJoinLeaveSounds) {
return;
}
// We're not playing sounds for local participant
// nor when the user is joining past the "startAudioMuted" limit.
// The intention there was to not play user joined notification in big

View File

@@ -4,7 +4,6 @@ import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
import { isMobileBrowser } from '../../environment/utils';
/**
* A map of dialog positions, relative to trigger, to css classes used to
@@ -115,9 +114,10 @@ class Popover extends Component<Props, State> {
};
/**
* Reference to the Popover that is meant to open as a drawer.
* Reference to the dialog container.
*/
_drawerContainerRef: Object;
_containerRef: Object;
/**
* Initializes a new {@code Popover} instance.
@@ -136,8 +136,10 @@ class Popover extends Component<Props, State> {
this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._drawerContainerRef = React.createRef();
this._containerRef = React.createRef();
this._onEscKey = this._onEscKey.bind(this);
this._onThumbClick = this._onThumbClick.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
}
/**
@@ -147,23 +149,17 @@ class Popover extends Component<Props, State> {
* @public
*/
showDialog() {
this.setState({ showDialog: true });
this._onShowDialog();
}
/**
* Sets up an event listener to open a drawer when clicking, rather than entering the
* overflow area.
*
* TODO: This should be done by setting an {@code onClick} handler on the div, but for some
* reason that doesn't seem to work whatsoever.
* Sets up a touch event listener to attach.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (this._drawerContainerRef && this._drawerContainerRef.current && !isMobileBrowser()) {
this._drawerContainerRef.current.addEventListener('click', this._onShowDialog);
}
window.addEventListener('touchstart', this._onTouchStart);
}
/**
@@ -173,25 +169,7 @@ class Popover extends Component<Props, State> {
* @returns {void}
*/
componentWillUnmount() {
if (this._drawerContainerRef && this._drawerContainerRef.current) {
this._drawerContainerRef.current.removeEventListener('click', this._onShowDialog);
}
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
if (prevProps.overflowDrawer !== this.props.overflowDrawer) {
// Make sure the listeners are set up when resizing the screen past the drawer threshold.
if (this.props.overflowDrawer) {
this.componentDidMount();
} else {
this.componentWillUnmount();
}
}
window.removeEventListener('touchstart', this._onTouchStart);
}
/**
@@ -208,7 +186,7 @@ class Popover extends Component<Props, State> {
<div
className = { className }
id = { id }
ref = { this._drawerContainerRef }>
onClick = { this._onShowDialog }>
{ children }
<DrawerPortal>
<Drawer
@@ -225,9 +203,11 @@ class Popover extends Component<Props, State> {
<div
className = { className }
id = { id }
onClick = { this._onThumbClick }
onKeyPress = { this._onKeyPress }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
onMouseLeave = { this._onHideDialog }
ref = { this._containerRef }>
<InlineDialog
content = { this._renderContent() }
isOpen = { this.state.showDialog }
@@ -238,6 +218,25 @@ class Popover extends Component<Props, State> {
);
}
_onTouchStart: (event: TouchEvent) => void;
/**
* Hide dialog on touch outside of the context menu.
*
* @param {TouchEvent} event - The touch event.
* @private
* @returns {void}
*/
_onTouchStart(event) {
if (this.state.showDialog
&& !this.props.overflowDrawer
&& this._containerRef
&& this._containerRef.current
&& !this._containerRef.current.contains(event.target)) {
this._onHideDialog();
}
}
_onHideDialog: () => void;
/**
@@ -265,7 +264,7 @@ class Popover extends Component<Props, State> {
* @returns {void}
*/
_onShowDialog(event) {
event.stopPropagation();
event && event.stopPropagation();
if (!this.props.disablePopover) {
this.setState({ showDialog: true });
@@ -275,6 +274,20 @@ class Popover extends Component<Props, State> {
}
}
_onThumbClick: (Object) => void;
/**
* Prevents switching from tile view to stage view on accidentally clicking
* the popover thumbs.
*
* @param {Object} event - The mouse event or the keypress event to intercept.
* @private
* @returns {void}
*/
_onThumbClick(event) {
event.stopPropagation();
}
_onKeyPress: (Object) => void;
/**

View File

@@ -79,37 +79,35 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
return (
<div className = 'con-status'>
<div className = 'con-status-container'>
<div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = { `con-status-circle ${connectionClass}` }>
<Icon
size = { 16 }
src = { icon } />
</div>
<span
aria-hidden = { !showDetails }
className = 'con-status-text'
id = 'connection-status-description'>{t(connectionText)}</span>
<div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = { `con-status-circle ${connectionClass}` }>
<Icon
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName }
onClick = { onToggleDetails }
onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 }
src = { IconArrowDownSmall }
tabIndex = { 0 } />
size = { 16 }
src = { icon } />
</div>
<div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
<span
aria-hidden = { !showDetails }
className = 'con-status-text'
id = 'connection-status-description'>{t(connectionText)}</span>
<Icon
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName }
onClick = { onToggleDetails }
onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 }
src = { IconArrowDownSmall }
tabIndex = { 0 } />
</div>
<div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
</div>
);
}

View File

@@ -1,67 +0,0 @@
// @flow
import React, { Component } from 'react';
import CopyMeetingLinkSection
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n';
import { connect } from '../../../redux';
type Props = {
/**
* The meeting url.
*/
url: string,
/**
* Used for translation.
*/
t: Function,
/**
* Used to determine if invitation link should be automatically copied
* after creating a meeting.
*/
_enableAutomaticUrlCopy: boolean,
};
/**
* Component used to copy meeting url on prejoin page.
*/
class CopyMeetingUrl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'copy-meeting'>
<CopyMeetingLinkSection url = { this.props.url } />
</div>
);
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
const { enableAutomaticUrlCopy } = state['features/base/config'];
const { customizationReady } = state['features/dynamic-branding'];
return {
url: customizationReady ? getCurrentConferenceUrl(state) : '',
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
};
}
export default connect(mapStateToProps)(translate(CopyMeetingUrl));

View File

@@ -2,27 +2,35 @@
import React, { PureComponent } from 'react';
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox/components/web';
import { VideoBackgroundButton } from '../../../../virtual-background';
import { checkBlurSupport } from '../../../../virtual-background/functions';
import { Avatar } from '../../../avatar';
import { allowUrlSharing } from '../../functions';
import { connect } from '../../../../base/redux';
import DeviceStatus from '../../../../prejoin/components/preview/DeviceStatus';
import { Toolbox } from '../../../../toolbox/components/web';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
import ConnectionStatus from './ConnectionStatus';
import CopyMeetingUrl from './CopyMeetingUrl';
import Preview from './Preview';
type Props = {
/**
* Children component(s) to be rendered on the screen.
* The list of toolbar buttons to render.
*/
children: React$Node,
_buttons: Array<string>,
/**
* Footer to be rendered for the page (if any).
* The branding background of the premeeting screen(lobby/prejoin).
*/
footer?: React$Node,
_premeetingBackground: string,
/**
* Children component(s) to be rendered on the screen.
*/
children?: React$Node,
/**
* Additional CSS class names to set on the icon container.
*/
className?: string,
/**
* The name of the participant.
@@ -35,25 +43,25 @@ type Props = {
showCopyUrlButton: boolean,
/**
* Indicates whether the avatar should be shown when video is off
* Indicates whether the device status should be shown
*/
showAvatar: boolean,
/**
* Indicates whether the label and copy url action should be shown
*/
showConferenceInfo: boolean,
/**
* Title of the screen.
*/
title: string,
showDeviceStatus: boolean,
/**
* The 'Skip prejoin' button to be rendered (if any).
*/
skipPrejoinButton?: React$Node,
/**
* Title of the screen.
*/
title?: string,
/**
* Whether it's used in the 3rdParty prejoin screen or not.
*/
thirdParty?: boolean,
/**
* True if the preview overlay should be muted, false otherwise.
*/
@@ -62,28 +70,22 @@ type Props = {
/**
* The video track to render as preview (if omitted, the default local track will be rendered).
*/
videoTrack?: Object,
/**
* Array with the buttons which this Toolbox should display.
*/
visibleButtons?: Array<string>
videoTrack?: Object
}
/**
* Implements a pre-meeting screen that can be used at various pre-meeting phases, for example
* on the prejoin screen (pre-connection) or lobby (post-connection).
*/
export default class PreMeetingScreen extends PureComponent<Props> {
class PreMeetingScreen extends PureComponent<Props> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showAvatar: true,
showCopyUrlButton: true,
showConferenceInfo: true
showToolbox: true
};
/**
@@ -93,58 +95,68 @@ export default class PreMeetingScreen extends PureComponent<Props> {
*/
render() {
const {
name,
showAvatar,
showConferenceInfo,
showCopyUrlButton,
_buttons,
_premeetingBackground,
children,
className,
showDeviceStatus,
skipPrejoinButton,
title,
videoMuted,
videoTrack,
visibleButtons
videoTrack
} = this.props;
const showSharingButton = allowUrlSharing() && showCopyUrlButton;
const containerClassName = `premeeting-screen ${className ? className : ''}`;
const style = _premeetingBackground ? {
background: _premeetingBackground,
backgroundPosition: 'center',
backgroundSize: 'cover'
} : {};
return (
<div
className = 'premeeting-screen'
id = 'lobby-screen'>
<ConnectionStatus />
<Preview
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
<div className = 'content'>
{showAvatar && videoMuted && (
<Avatar
className = 'premeeting-screen-avatar'
displayName = { name }
dynamicColor = { false }
participantId = 'local'
size = { 80 } />
)}
{showConferenceInfo && (
<>
<div className = { containerClassName }>
<div style = { style }>
<div className = 'content'>
<ConnectionStatus />
<div className = 'content-controls'>
<h1 className = 'title'>
{ title }
</h1>
{showSharingButton ? <CopyMeetingUrl /> : null}
</>
)}
{ this.props.children }
<div className = 'media-btn-container'>
<div className = 'toolbox-content'>
<div className = 'toolbox-content-items'>
<AudioSettingsButton visible = { true } />
<VideoSettingsButton visible = { true } />
{ ((visibleButtons && visibleButtons.includes('select-background'))
|| (visibleButtons && visibleButtons.includes('videobackgroundblur')))
&& <VideoBackgroundButton visible = { checkBlurSupport() } /> }
</div>
{ children }
{ _buttons.length && <Toolbox toolbarButtons = { _buttons } /> }
{ skipPrejoinButton }
{ showDeviceStatus && <DeviceStatus /> }
</div>
</div>
{ this.props.skipPrejoinButton }
{ this.props.footer }
</div>
<Preview
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
</div>
);
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The props passed to the component.
* @returns {Object}
*/
function mapStateToProps(state, ownProps): Object {
const hideButtons = state['features/base/config'].hiddenPremeetingButtons || [];
const premeetingButtons = ownProps.thirdParty
? THIRD_PARTY_PREJOIN_BUTTONS
: PREMEETING_BUTTONS;
const { premeetingBackground } = state['features/dynamic-branding'];
return {
_buttons: premeetingButtons.filter(b => !hideButtons.includes(b)),
_premeetingBackground: premeetingBackground
};
}
export default connect(mapStateToProps)(PreMeetingScreen);

View File

@@ -2,17 +2,30 @@
import React from 'react';
import { getDisplayName } from '../../../../base/settings';
import { Avatar } from '../../../avatar';
import { Video } from '../../../media';
import { getLocalParticipant } from '../../../participants';
import { connect } from '../../../redux';
import { getLocalVideoTrack } from '../../../tracks';
export type Props = {
/**
* Local participant id
*/
_participantId: string,
/**
* Flag controlling whether the video should be flipped or not.
*/
flipVideo: boolean,
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Flag signaling the visibility of camera preview.
*/
@@ -31,20 +44,27 @@ export type Props = {
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { videoMuted, videoTrack, flipVideo } = props;
const { _participantId, flipVideo, name, videoMuted, videoTrack } = props;
const className = flipVideo ? 'flipVideoX' : '';
if (!videoMuted && videoTrack) {
return (
<div id = 'preview'>
<Video
className = { className }
videoTrack = {{ jitsiTrack: videoTrack }} />
</div>
);
}
return null;
return (
<div id = 'preview'>
{!videoMuted && videoTrack
? (
<Video
className = { className }
videoTrack = {{ jitsiTrack: videoTrack }} />
)
: (
<Avatar
className = 'premeeting-screen-avatar'
displayName = { name }
dynamicColor = { false }
participantId = { _participantId }
size = { 180 } />
)}
</div>
);
}
/**
@@ -55,8 +75,13 @@ function Preview(props: Props) {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const name = getDisplayName(state);
const { id: _participantId } = getLocalParticipant(state);
return {
_participantId,
flipVideo: state['features/base/settings'].localFlipX,
name,
videoMuted: ownProps.videoTrack ? ownProps.videoMuted : state['features/base/media'].video.muted,
videoTrack: ownProps.videoTrack || (getLocalVideoTrack(state['features/base/tracks']) || {}).jitsiTrack
};

View File

@@ -213,14 +213,3 @@ export function getConnectionData(state: Object) {
connectionDetails: []
};
}
/**
* Returns if url sharing is enabled in interface configuration.
*
* @returns {boolean}
*/
export function allowUrlSharing() {
return typeof interfaceConfig === 'undefined'
|| typeof interfaceConfig.SHARING_FEATURES === 'undefined'
|| (interfaceConfig.SHARING_FEATURES.length && interfaceConfig.SHARING_FEATURES.indexOf('url') > -1);
}

View File

@@ -30,3 +30,15 @@ export const SET_ASPECT_RATIO = 'SET_ASPECT_RATIO';
* @public
*/
export const SET_REDUCED_UI = 'SET_REDUCED_UI';
/**
* The type of (redux) action which tells whether a local or remote participant
* context menu is open.
*
* {
* type: SET_CONTEXT_MENU_OPEN,
* showConnectionInfo: boolean
* }
*/
export const SET_CONTEXT_MENU_OPEN = 'SET_CONTEXT_MENU_OPEN';

View File

@@ -1,12 +1,13 @@
// @flow
import { batch } from 'react-redux';
import type { Dispatch } from 'redux';
import { CHAT_SIZE } from '../../chat/constants';
import { getParticipantsPaneOpen } from '../../participants-pane/functions';
import theme from '../../participants-pane/theme.json';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_CONTEXT_MENU_OPEN, SET_REDUCED_UI } from './actionTypes';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
/**
@@ -44,10 +45,13 @@ export function clientResized(clientWidth: number, clientHeight: number) {
}
}
return dispatch({
type: CLIENT_RESIZED,
clientHeight,
clientWidth: availableWidth
batch(() => {
dispatch({
type: CLIENT_RESIZED,
clientHeight,
clientWidth: availableWidth
});
dispatch(setAspectRatio(clientWidth, clientHeight));
});
};
}
@@ -105,3 +109,16 @@ export function setReducedUI(width: number, height: number): Function {
}
};
}
/**
* Sets whether the local or remote participant context menu is open.
*
* @param {boolean} isOpen - Whether local or remote context menu is open.
* @returns {Object}
*/
export function setParticipantContextMenuOpen(isOpen: boolean) {
return {
type: SET_CONTEXT_MENU_OPEN,
isOpen
};
}

View File

@@ -2,7 +2,7 @@
import { ReducerRegistry, set } from '../redux';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_CONTEXT_MENU_OPEN, SET_REDUCED_UI } from './actionTypes';
import { ASPECT_RATIO_NARROW } from './constants';
const {
@@ -17,7 +17,8 @@ const DEFAULT_STATE = {
aspectRatio: ASPECT_RATIO_NARROW,
clientHeight: innerHeight,
clientWidth: innerWidth,
reducedUI: false
reducedUI: false,
contextMenuOpened: false
};
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
@@ -34,6 +35,9 @@ ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE,
case SET_REDUCED_UI:
return set(state, 'reducedUI', action.reducedUI);
case SET_CONTEXT_MENU_OPEN:
return set(state, 'contextMenuOpened', action.isOpen);
}
return state;

View File

@@ -31,6 +31,7 @@ const DEFAULT_STATE = {
soundsParticipantJoined: true,
soundsParticipantLeft: true,
soundsTalkWhileMuted: true,
soundsReactions: true,
startAudioOnly: false,
startWithAudioMuted: false,
startWithVideoMuted: false,
@@ -61,7 +62,7 @@ filterSubtree.audioOutputDeviceId = false;
filterSubtree.cameraDeviceId = false;
filterSubtree.micDeviceId = false;
PersistenceRegistry.register(STORE_NAME, filterSubtree);
PersistenceRegistry.register(STORE_NAME, filterSubtree, DEFAULT_STATE);
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {

View File

@@ -11,6 +11,7 @@ import {
UNREGISTER_SOUND
} from './actionTypes';
import { getSoundsPath } from './functions';
import { getDisabledSounds } from './functions.any';
/**
* Adds {@link AudioElement} instance to the base/sounds feature state for the
@@ -63,15 +64,18 @@ export function _removeAudioElement(soundId: string) {
*
* @param {string} soundId - The id of the sound to be played (the same one
* which was used in {@link registerSound} to register the sound).
* @returns {{
* type: PLAY_SOUND,
* soundId: string
* }}
* @returns {Function}
*/
export function playSound(soundId: string): Object {
return {
type: PLAY_SOUND,
soundId
return (dispatch: Function, getState: Function) => {
const disabledSounds = getDisabledSounds(getState());
if (!disabledSounds.includes(soundId)) {
dispatch({
type: PLAY_SOUND,
soundId
});
}
};
}

View File

@@ -0,0 +1,11 @@
// @flow
/**
* Selector for retrieving the disabled sounds array.
*
* @param {Object} state - The Redux state.
* @returns {Array<string>} - The disabled sound id's array.
*/
export function getDisabledSounds(state: Object) {
return state['features/base/config'].disabledSounds || [];
}

View File

@@ -23,6 +23,14 @@ export default class AbstractAudioMuteButton<P: Props, S: *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setAudioMuted(!this._isAudioMuted());
}

View File

@@ -20,6 +20,11 @@ export type Props = {
*/
disabledStyles: ?Styles,
/**
* External handler for click action.
*/
handleClick?: Function,
/**
* Whether to show the label or not.
*/

View File

@@ -22,6 +22,14 @@ export default class AbstractVideoMuteButton<P : Props, S : *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setVideoMuted(!this._isVideoMuted());
}

View File

@@ -18,11 +18,6 @@ type Props = AbstractButtonProps & {
* Whether or not the chat feature is currently displayed.
*/
_chatOpen: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -61,7 +56,13 @@ class ChatButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -305,10 +305,9 @@ function _handleReceivedMessage({ dispatch, getState },
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const { disableIncomingMessageSound } = state['features/base/config'];
const { soundsIncomingMessage: soundEnabled } = state['features/base/settings'];
if (!disableIncomingMessageSound && soundEnabled && shouldPlaySound && !isChatOpen) {
if (soundEnabled && shouldPlaySound && !isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}

View File

@@ -22,6 +22,8 @@ import {
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby';
import { LobbyScreen } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { ParticipantsPane } from '../../../participants-pane/components/native';
import { Captions } from '../../../subtitles';
@@ -98,6 +100,11 @@ type Props = AbstractProps & {
*/
_toolboxVisible: boolean,
/**
* Indicates whether the lobby screen should be visible.
*/
_showLobby: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -154,7 +161,11 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const { _fullscreenEnabled } = this.props;
const { _fullscreenEnabled, _showLobby } = this.props;
if (_showLobby) {
return <LobbyScreen />;
}
return (
<Container style = { styles.conference }>
@@ -427,6 +438,7 @@ function _mapStateToProps(state) {
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI,
_showLobby: getIsLobbyVisible(state),
_toolboxVisible: isToolboxVisible(state)
};
}

View File

@@ -15,9 +15,10 @@ import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { Prejoin, isPrejoinPageVisible, isPrejoinPageLoading } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
@@ -70,11 +71,6 @@ type Props = AbstractProps & {
*/
_backgroundAlpha: number,
/**
* Returns true if the 'lobby screen' is visible.
*/
_isLobbyScreenVisible: boolean,
/**
* If participants pane is visible or not.
*/
@@ -96,6 +92,11 @@ type Props = AbstractProps & {
*/
_roomName: string,
/**
* If lobby page is visible or not.
*/
_showLobby: boolean,
/**
* If prejoin page is visible or not.
*/
@@ -207,9 +208,9 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
_isLobbyScreenVisible,
_isParticipantsPaneVisible,
_layoutClassName,
_showLobby,
_showPrejoin
} = this.props;
@@ -237,7 +238,7 @@ class Conference extends AbstractConference<Props, *> {
<Filmstrip />
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
{ _showPrejoin || _showLobby || <Toolbox /> }
<Chat />
{ this.renderNotificationsContainer() }
@@ -245,7 +246,7 @@ class Conference extends AbstractConference<Props, *> {
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
{ _showLobby && <LobbyScreen />}
</div>
<ParticipantsPane />
</div>
@@ -373,12 +374,12 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha,
_isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
_isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_roomName: getConferenceNameForTitle(state),
_showPrejoin: isPrejoinPageVisible(state)
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state) || isPrejoinPageLoading(state)
};
}

View File

@@ -40,6 +40,11 @@ export type Props = {
*/
disableDeviceChange: boolean,
/**
* Whether video input dropdown should be enabled or not.
*/
disableVideoInputSelect: boolean,
/**
* Whether or not the audio permission was granted.
*/
@@ -57,6 +62,12 @@ export type Props = {
*/
hideAudioInputPreview: boolean,
/**
* If true, the button to play a test sound on the selected speaker will not be displayed.
* This needs to be hidden on browsers that do not support selecting an audio output device.
*/
hideAudioOutputPreview: boolean,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
@@ -70,12 +81,6 @@ export type Props = {
*/
hideVideoInputPreview: boolean,
/**
* Whether video output dropdown should be displayed or not.
* (In the case of iOS Safari)
*/
hideVideoOutputSelect: boolean,
/**
* An optional callback to invoke after the component has completed its
* mount logic.
@@ -212,7 +217,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
render() {
const {
hideAudioInputPreview,
hideAudioOutputSelect,
hideAudioOutputPreview,
hideVideoInputPreview,
selectedAudioOutputId
} = this.props;
@@ -237,7 +242,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
className = 'device-selectors'>
{ this._renderSelectors() }
</div>
{ !hideAudioOutputSelect
{ !hideAudioOutputPreview
&& <AudioOutputPreview
deviceId = { selectedAudioOutputId } /> }
</div>
@@ -253,6 +258,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
const { hideAudioInputPreview } = this.props;
if (hideAudioInputPreview) {
return;
}
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => {
@@ -371,33 +382,27 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
devices: availableDevices.audioInput,
hasPermission: hasAudioPermission,
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange,
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
key: 'audioInput',
id: 'audioInput',
label: 'settings.selectMic',
onSelect: selectedAudioInputId =>
super._onChange({ selectedAudioInputId }),
onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
selectedDeviceId: this.state.previewAudioTrack
? this.state.previewAudioTrack.getDeviceId() : null
}
];
if (!this.props.hideVideoOutputSelect) {
configurations.unshift({
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
},
{
devices: availableDevices.videoInput,
hasPermission: hasVideoPermission,
icon: 'icon-camera',
isDisabled: this.props.disableDeviceChange,
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
key: 'videoInput',
id: 'videoInput',
label: 'settings.selectCamera',
onSelect: selectedVideoInputId =>
super._onChange({ selectedVideoInputId }),
onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
selectedDeviceId: this.state.previewVideoTrack
? this.state.previewVideoTrack.getDeviceId() : null
});
}
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
@@ -408,8 +413,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
key: 'audioOutput',
id: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: selectedAudioOutputId =>
super._onChange({ selectedAudioOutputId }),
onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
selectedDeviceId: this.props.selectedAudioOutputId
});
}

View File

@@ -35,10 +35,15 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
const { conference } = state['features/base/conference'];
const { permissions } = state['features/base/devices'];
const isMobileSafari = isIosMobileBrowser();
const cameraChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
const userSelectedMic = getUserSelectedMicDeviceId(state);
let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
let selectedAudioInputId = settings.micDeviceId;
let disableVideoInputSelect = !cameraChangeSupported;
let selectedAudioInputId = isMobileSafari ? userSelectedMic : settings.micDeviceId;
let selectedAudioOutputId = getAudioOutputDeviceId();
let selectedVideoInputId = settings.cameraDeviceId;
let selectedVideoInputId = isMobileSafari ? userSelectedCamera : settings.cameraDeviceId;
// audio input change will be a problem only when we are in a
// conference and this is not supported, when we open device selection on
@@ -46,9 +51,10 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
// on welcome page we also show only what we have saved as user selected devices
if (!conference) {
disableAudioInputChange = false;
selectedAudioInputId = getUserSelectedMicDeviceId(state);
disableVideoInputSelect = false;
selectedAudioInputId = userSelectedMic;
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
selectedVideoInputId = getUserSelectedCameraDeviceId(state);
selectedVideoInputId = userSelectedCamera;
}
// we fill the device selection dialog with the devices that are currently
@@ -56,16 +62,14 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
return {
availableDevices: state['features/base/devices'].availableDevices,
disableAudioInputChange,
disableDeviceChange:
!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
disableVideoInputSelect,
hasAudioPermission: permissions.audio,
hasVideoPermission: permissions.video,
hideAudioInputPreview:
!JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
.isDeviceChangeAvailable('output'),
hideVideoInputPreview: isMobileSafari,
hideVideoOutputSelect: isMobileSafari,
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputPreview: !speakerChangeSupported,
hideAudioOutputSelect: !speakerChangeSupported,
hideVideoInputPreview: !cameraChangeSupported,
selectedAudioInputId,
selectedAudioOutputId,
selectedVideoInputId

View File

@@ -99,6 +99,14 @@ const DEFAULT_STATE = {
*/
logoImageUrl: '',
/**
* The lobby/prejoin background.
*
* @public
* @type {string}
*/
premeetingBackground: '',
/**
* Flag used to signal if the app should use a custom logo or not
*
@@ -115,6 +123,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_DYNAMIC_BRANDING_DATA: {
const {
avatarBackgrounds,
backgroundColor,
backgroundImageUrl,
defaultBranding,
@@ -122,7 +131,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
avatarBackgrounds
premeetingBackground
} = action.value;
return {
@@ -134,6 +143,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
premeetingBackground,
customizationFailed: false,
customizationReady: true,
useDynamicBrandingData: true

View File

@@ -3,6 +3,11 @@
export type Props = {
/**
* Custom e2ee labels.
*/
_e2eeLabels?: Object;
/**
* True if the label needs to be rendered, false otherwise.
*/
@@ -23,6 +28,7 @@ export type Props = {
*/
export function _mapStateToProps(state: Object) {
return {
_e2eeLabels: state['features/base/config'].e2eeLabels,
_showLabel: state['features/e2ee'].everyoneEnabledE2EE
};
}

View File

@@ -28,10 +28,12 @@ class E2EELabel extends Component<Props> {
if (!this.props._showLabel) {
return null;
}
const { _e2eeLabels, t } = this.props;
const content = _e2eeLabels?.labelToolTip || t('e2ee.labelToolTip');
return (
<Tooltip
content = { this.props.t('e2ee.labelToolTip') }
content = { content }
position = { 'bottom' }>
<Label
className = 'label--green'

View File

@@ -12,6 +12,11 @@ import { toggleE2EE } from '../actions';
type Props = {
/**
* Custom e2ee labels.
*/
_e2eeLabels: Object,
/**
* Whether E2EE is currently enabled or not.
*/
@@ -95,9 +100,11 @@ class E2EESection extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _everyoneSupportE2EE, t } = this.props;
const { enabled, expand } = this.state;
const description = t('dialog.e2eeDescription');
const { _e2eeLabels, _everyoneSupportE2EE, t } = this.props;
const { enabled } = this.state;
const description = _e2eeLabels?.description || t('dialog.e2eeDescription');
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
return (
<div id = 'e2ee-section'>
@@ -105,28 +112,13 @@ class E2EESection extends Component<Props, State> {
aria-live = 'polite'
className = 'description'
id = 'e2ee-section-description'>
{ expand && description }
{ !expand && description.substring(0, 100) }
{ !expand && <span
aria-controls = 'e2ee-section-description'
aria-expanded = { expand }
className = 'read-more'
onClick = { this._onExpand }
onKeyPress = { this._onExpandKeyPress }
role = 'button'
tabIndex = { 0 }>
... { t('dialog.readMore') }
</span> }
{ description }
{ !_everyoneSupportE2EE && <br /> }
{ !_everyoneSupportE2EE && warning }
</p>
{
!_everyoneSupportE2EE
&& <span className = 'warning'>
{ t('dialog.e2eeWarning') }
</span>
}
<div className = 'control-row'>
<label htmlFor = 'e2ee-section-switch'>
{ t('dialog.e2eeLabel') }
{ label }
</label>
<Switch
id = 'e2ee-section-switch'
@@ -195,8 +187,10 @@ class E2EESection extends Component<Props, State> {
*/
function mapStateToProps(state) {
const { enabled, everyoneSupportE2EE } = state['features/e2ee'];
const { e2eeLabels } = state['features/base/config'];
return {
_e2eeLabels: e2eeLabels,
_enabled: enabled,
_everyoneSupportE2EE: everyoneSupportE2EE
};

View File

@@ -36,7 +36,13 @@ class EmbedMeetingButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('embed.meeting'));
dispatch(openDialog(EmbedMeetingDialog));

View File

@@ -59,12 +59,20 @@ class SharedDocumentButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _editing, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent(
'toggle.etherpad',
{
enable: !this.props._editing
enable: !_editing
}));
this.props.dispatch(toggleDocument());
dispatch(toggleDocument());
}
/**

View File

@@ -40,7 +40,13 @@ class FeedbackButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _conference, dispatch } = this.props;
const { _conference, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('feedback'));
dispatch(openFeedbackDialog(_conference));

View File

@@ -14,6 +14,7 @@ import {
pinParticipant
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { isTestModeEnabled } from '../../../base/testing';
import {
getLocalAudioTrack,
@@ -33,6 +34,7 @@ import {
DISPLAY_MODE_TO_STRING,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
MOBILE_FILMSTRIP_PORTRAIT_RATIO,
VIDEO_TEST_EVENTS,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
} from '../../constants';
@@ -144,6 +146,11 @@ export type Props = {|
*/
_isMobile: boolean,
/**
* Whether we are currently running in a mobile browser in portrait orientation.
*/
_isMobilePortrait: boolean,
/**
* Indicates whether the participant is screen sharing.
*/
@@ -760,7 +767,9 @@ class Thumbnail extends Component<Props, State> {
const {
_defaultLocalDisplayName,
_disableLocalVideoFlip,
_height,
_isMobile,
_isMobilePortrait,
_isScreenSharing,
_localFlipX,
_disableProfile,
@@ -774,6 +783,9 @@ class Thumbnail extends Component<Props, State> {
const videoTrackClassName
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
styles.thumbnail.height = _isMobilePortrait
? `${Math.floor(_height * MOBILE_FILMSTRIP_PORTRAIT_RATIO)}px`
: styles.thumbnail.height;
return (
<span
@@ -1039,6 +1051,7 @@ function _mapStateToProps(state, ownProps): Object {
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
let size = {};
let _isMobilePortrait = false;
const {
startSilent,
disableLocalVideoFlip,
@@ -1074,9 +1087,12 @@ function _mapStateToProps(state, ownProps): Object {
_height: height
};
_isMobilePortrait = _isMobile && state['features/base/responsive-ui'].aspectRatio === ASPECT_RATIO_NARROW;
break;
}
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
size = {
@@ -1100,6 +1116,7 @@ function _mapStateToProps(state, ownProps): Object {
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isMobile,
_isMobilePortrait,
_isScreenSharing: _videoTrack?.videoType === 'desktop',
_isTestModeEnabled: isTestModeEnabled(state),
_isVideoPlayable: id && isVideoPlayable(state, id),

View File

@@ -220,3 +220,22 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
* @type {number}
*/
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
/**
* The ratio for filmstrip self view on mobile portrait mode.
*
* @type {number}
*/
export const MOBILE_FILMSTRIP_PORTRAIT_RATIO = 2.5;
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.
*
* NOTE: Mobile specific.
*
* @private
* @type {number}
*/
export const TILE_MARGIN = 10;

View File

@@ -68,9 +68,11 @@ export function shouldRemoteVideosBeVisible(state: Object) {
const participantCount = getParticipantCountWithFake(state);
let pinnedParticipant;
const { disable1On1Mode } = state['features/base/config'];
const { contextMenuOpened } = state['features/base/responsive-ui'];
return Boolean(
participantCount > 2
contextMenuOpened
|| participantCount > 2
// Always show the filmstrip when there is another participant to
// show and the local video is pinned, or the toolbar is displayed.

View File

@@ -0,0 +1,50 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
import { translate } from '../../../../base/i18n';
import { IconAddPeople } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../../base/toolbox/components';
import { beginAddPeople } from '../../../actions.any';
/**
* The type of the React {@code Component} props of {@link EmbedMeetingButton}.
*/
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* Implementation of a button for opening invite people dialog.
*/
class InviteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.invite';
icon = IconAddPeople;
label = 'toolbar.invite';
tooltip = 'toolbar.invite';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());
}
}
export default translate(connect()(InviteButton));

View File

@@ -1,3 +1,4 @@
// @flow
export { default as AddPeopleDialog } from './AddPeopleDialog';
export { default as InviteButton } from './InviteButton';

View File

@@ -17,7 +17,7 @@ export const DIAL_IN_SUMMARY_VIEW_ID = 'DIAL_IN_SUMMARY_VIEW_ID';
* @type {string}
*/
export const OUTGOING_CALL_EXPIRED_SOUND_ID
= 'OUTGOING_CALL_EXPIRED_SOUND_ID';
= 'OUTGOING_CALL_EXPIRED_SOUND';
/**
* The identifier of the sound to be played when the status of an outgoing call
@@ -26,7 +26,7 @@ export const OUTGOING_CALL_EXPIRED_SOUND_ID
* @type {string}
*/
export const OUTGOING_CALL_REJECTED_SOUND_ID
= 'OUTGOING_CALL_REJECTED_SOUND_ID';
= 'OUTGOING_CALL_REJECTED_SOUND';
/**
* The identifier of the sound to be played when the status of an outgoing call
@@ -34,14 +34,14 @@ export const OUTGOING_CALL_REJECTED_SOUND_ID
*
* @type {string}
*/
export const OUTGOING_CALL_RINGING_SOUND_ID = 'OUTGOING_CALL_RINGING_SOUND_ID';
export const OUTGOING_CALL_RINGING_SOUND_ID = 'OUTGOING_CALL_RINGING_SOUND';
/**
* The identifier of the sound to be played when outgoing call is started.
*
* @type {string}
*/
export const OUTGOING_CALL_START_SOUND_ID = 'OUTGOING_CALL_START_SOUND_ID';
export const OUTGOING_CALL_START_SOUND_ID = 'OUTGOING_CALL_START_SOUND';
/**
* Regex for matching sip addresses.

View File

@@ -34,7 +34,13 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('shortcuts'));
dispatch(openKeyboardShortcutsDialog());

View File

@@ -20,6 +20,11 @@ export const SET_LOBBY_MODE_ENABLED = 'SET_LOBBY_MODE_ENABLED';
*/
export const SET_KNOCKING_STATE = 'SET_KNOCKING_STATE';
/**
* Action type to set the lobby visibility.
*/
export const SET_LOBBY_VISIBILITY = 'TOGGLE_LOBBY_VISIBILITY';
/**
* Action type to set the password join failed status.
*/

View File

@@ -6,6 +6,8 @@ import {
getCurrentConference
} from '../base/conference';
import { SET_LOBBY_VISIBILITY } from './actionTypes';
/**
* Action to toggle lobby mode on or off.
*
@@ -23,3 +25,27 @@ export function toggleLobbyMode(enabled: boolean) {
}
};
}
/**
* Action to open the lobby screen.
*
* @returns {openDialog}
*/
export function openLobbyScreen() {
return {
type: SET_LOBBY_VISIBILITY,
visible: true
};
}
/**
* Action to hide the lobby screen.
*
* @returns {hideDialog}
*/
export function hideLobbyScreen() {
return {
type: SET_LOBBY_VISIBILITY,
visible: false
};
}

View File

@@ -9,7 +9,6 @@ import {
sendLocalParticipant,
setPassword
} from '../base/conference';
import { hideDialog, openDialog } from '../base/dialog';
import { getLocalParticipant } from '../base/participants';
export * from './actions.any';
@@ -20,7 +19,6 @@ import {
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
import { LobbyScreen } from './components';
declare var APP: Object;
@@ -44,15 +42,6 @@ export function cancelKnocking() {
};
}
/**
* Action to hide the lobby screen.
*
* @returns {hideDialog}
*/
export function hideLobbyScreen() {
return hideDialog(LobbyScreen);
}
/**
* Tries to join with a preset password.
*
@@ -83,15 +72,6 @@ export function knockingParticipantLeft(id: string) {
};
}
/**
* Action to open the lobby screen.
*
* @returns {openDialog}
*/
export function openLobbyScreen() {
return openDialog(LobbyScreen, {}, true);
}
/**
* Action to be executed when a participant starts knocking or an already knocking participant gets updated.
*

View File

@@ -7,6 +7,7 @@ import { getFeatureFlag, INVITE_ENABLED } from '../../base/flags';
import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
import { updateSettings } from '../../base/settings';
import { isDeviceStatusVisible } from '../../prejoin/functions';
import { cancelKnocking, joinWithPassword, setPasswordJoinFailed, startKnocking } from '../actions';
export const SCREEN_STATES = {
@@ -17,6 +18,11 @@ export const SCREEN_STATES = {
export type Props = {
/**
* Indicates whether the device status should be visible.
*/
_deviceStatusVisible: boolean,
/**
* True if knocking is already happening, so we're waiting for a response.
*/
@@ -380,8 +386,10 @@ export function _mapStateToProps(state: Object): $Shape<Props> {
const { knocking, passwordJoinFailed } = state['features/lobby'];
const { iAmSipGateway } = state['features/base/config'];
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
const deviceStatusVisible = isDeviceStatusVisible(state);
return {
_deviceStatusVisible: deviceStatusVisible,
_knocking: knocking,
_meetingName: getConferenceName(state),
_participantEmail: localParticipant?.email,

View File

@@ -27,15 +27,16 @@ class LobbyScreen extends AbstractLobbyScreen {
return (
<CustomDialog
onCancel = { this._onCancel }
style = { styles.contentWrapper }>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey()) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent() }
onCancel = { this._onCancel }>
<View style = { styles.contentWrapper }>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey()) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent() }
</View>
</CustomDialog>
);
}

View File

@@ -20,11 +20,13 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
render() {
const { showCopyUrlButton, t } = this.props;
const { _deviceStatusVisible, showCopyUrlButton, t } = this.props;
return (
<PreMeetingScreen
className = 'lobby-screen'
showCopyUrlButton = { showCopyUrlButton }
showDeviceStatus = { _deviceStatusVisible }
title = { t(this._getScreenTitleKey()) }>
{ this._renderContent() }
</PreMeetingScreen>
@@ -62,7 +64,7 @@ class LobbyScreen extends AbstractLobbyScreen {
*/
_renderJoining() {
return (
<div className = 'container'>
<div className = 'lobby-screen-content'>
<div className = 'spinner'>
<LoadingIndicator size = 'large' />
</div>
@@ -113,13 +115,19 @@ class LobbyScreen extends AbstractLobbyScreen {
const { _passwordJoinFailed, t } = this.props;
return (
<InputField
className = { _passwordJoinFailed ? 'error' : '' }
onChange = { this._onChangePassword }
placeHolder = { _passwordJoinFailed ? t('lobby.invalidPassword') : t('lobby.passwordField') }
testId = 'lobby.password'
type = 'password'
value = { this.state.password } />
<>
<InputField
className = { _passwordJoinFailed ? 'error' : '' }
onChange = { this._onChangePassword }
placeHolder = { t('lobby.passwordField') }
testId = 'lobby.password'
type = 'password'
value = { this.state.password } />
{_passwordJoinFailed && <div
className = 'prejoin-error'
data-testid = 'lobby.errorMessage'>{t('lobby.invalidPassword')}</div>}
</>
);
}
@@ -134,11 +142,10 @@ class LobbyScreen extends AbstractLobbyScreen {
return (
<>
<ActionButton
disabled = { !this.state.password }
onClick = { this._onJoinWithPassword }
testId = 'lobby.passwordJoinButton'
type = 'primary'>
{ t('lobby.passwordJoinButton') }
{ t('prejoin.joinMeeting') }
</ActionButton>
<ActionButton
onClick = { this._onSwitchToKnockMode }

View File

@@ -2,3 +2,9 @@
* Hide these emails when trying to join a lobby
*/
export const HIDDEN_EMAILS = [ 'inbound-sip-jibri@jitsi.net', 'outbound-sip-jibri@jitsi.net' ];
/**
* The identifier of the sound to be played when a participant joins lobby.
* @type {string}
*/
export const KNOCKING_PARTICIPANT_SOUND_ID = 'KNOCKING_PARTICIPANT_SOUND';

View File

@@ -11,6 +11,16 @@ export function getLobbyState(state: any) {
}
/**
* Selector to return lobby visibility.
*
* @param {any} state - State object.
* @returns {any}
*/
export function getIsLobbyVisible(state: any) {
return state['features/lobby'].lobbyVisible;
}
/**
* Selector to return array with knocking participant ids.
*

View File

@@ -1,9 +1,13 @@
// @flow
import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isTestModeEnabled } from '../base/testing';
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
import { shouldAutoKnock } from '../prejoin/functions';
@@ -18,9 +22,17 @@ import {
startKnocking,
setPasswordJoinFailed
} from './actions';
import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
import { KNOCKING_PARTICIPANT_FILE } from './sounds';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE));
break;
case APP_WILL_UNMOUNT:
store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID));
break;
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
@@ -51,10 +63,13 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
dispatch(participantIsKnockingOrUpdated({
id,
name
}));
batch(() => {
dispatch(participantIsKnockingOrUpdated({
id,
name
}));
dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
});
});
conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {

View File

@@ -8,6 +8,7 @@ import {
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_LOBBY_VISIBILITY,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
@@ -15,6 +16,7 @@ const DEFAULT_STATE = {
knocking: false,
knockingParticipants: [],
lobbyEnabled: false,
lobbyVisible: false,
passwordJoinFailed: false
};
@@ -53,6 +55,11 @@ ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
...state,
lobbyEnabled: action.enabled
};
case SET_LOBBY_VISIBILITY:
return {
...state,
lobbyVisible: action.visible
};
case SET_PASSWORD:
return {
...state,

View File

@@ -0,0 +1,5 @@
/**
* The name of the bundled sound file which will be played when a
* participant enters lobby.
*/
export const KNOCKING_PARTICIPANT_FILE = 'knock.mp3';

View File

@@ -36,7 +36,13 @@ class LocalRecording extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('local.recording'));
dispatch(openDialog(LocalRecordingInfoDialog));

View File

@@ -3,4 +3,4 @@
*
* @type {string}
*/
export const NO_AUDIO_SIGNAL_SOUND_ID = 'NO_AUDIO_SIGNAL_SOUND_ID';
export const NO_AUDIO_SIGNAL_SOUND_ID = 'NO_AUDIO_SIGNAL_SOUND';

View File

@@ -3,4 +3,4 @@
*
* @type {string}
*/
export const NOISY_AUDIO_INPUT_SOUND_ID = 'NOISY_AUDIO_INPUT_SOUND_ID';
export const NOISY_AUDIO_INPUT_SOUND_ID = 'NOISY_AUDIO_INPUT_SOUND';

View File

@@ -105,13 +105,15 @@ export function showNotification(props: Object = {}, timeout: ?number) {
* Queues a warning notification for display.
*
* @param {Object} props - The props needed to show the notification component.
* @param {number} timeout - How long the notification should display before
* automatically being hidden.
* @returns {Object}
*/
export function showWarningNotification(props: Object) {
export function showWarningNotification(props: Object, timeout: ?number) {
return showNotification({
...props,
appearance: NOTIFICATION_TYPE.WARNING
});
}, timeout);
}
/**

View File

@@ -1,6 +1,10 @@
// @flow
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
JitsiConferenceErrors,
isFatalJitsiConferenceError,
isFatalJitsiConnectionError
} from '../base/lib-jitsi-meet';
import { StateListenerRegistry } from '../base/redux';
import { setFatalError } from './actions';
@@ -16,6 +20,47 @@ const NON_OVERLAY_ERRORS = [
JitsiConferenceErrors.CONNECTION_ERROR
];
const ERROR_TYPES = {
CONFIG: 'CONFIG',
CONNECTION: 'CONNECTION',
CONFERENCE: 'CONFERENCE'
};
/**
* Gets the error type and whether it's fatal or not.
*
* @param {Function} getState - The redux function for fetching the current state.
* @param {Object|string} error - The error to process.
* @returns {void}
*/
const getErrorExtraInfo = (getState, error) => {
const state = getState();
const { error: conferenceError } = state['features/base/conference'];
const { error: configError } = state['features/base/config'];
const { error: connectionError } = state['features/base/connection'];
if (error === conferenceError) {
return {
type: ERROR_TYPES.CONFERENCE,
isFatal: isFatalJitsiConferenceError(error.name || error)
};
}
if (error === configError) {
return {
type: ERROR_TYPES.CONFIG,
isFatal: true
};
}
if (error === connectionError) {
return {
type: ERROR_TYPES.CONNECTION,
isFatal: isFatalJitsiConnectionError(error.name || error)
};
}
};
/**
* State listener which emits the {@code fatalErrorOccurred} action which works
* as a catch all for critical errors which have not been claimed by any other
@@ -29,10 +74,22 @@ StateListenerRegistry.register(
return configError || connectionError || conferenceError;
},
/* listener */ (error, { dispatch }) => {
error
&& NON_OVERLAY_ERRORS.indexOf(error.name) === -1
&& typeof error.recoverable === 'undefined'
&& dispatch(setFatalError(error));
/* listener */ (error, { dispatch, getState }) => {
if (!error) {
return;
}
if (typeof APP !== 'undefined') {
const parsedError = typeof error === 'string' ? { name: error } : error;
APP.API.notifyError({
...parsedError,
...getErrorExtraInfo(getState, error)
});
}
if (NON_OVERLAY_ERRORS.indexOf(error.name) === -1 && typeof error.recoverable === 'undefined') {
dispatch(setFatalError(error));
}
}
);

View File

@@ -11,9 +11,9 @@ import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/com
type Props = AbstractButtonProps & {
/**
* External handler for click action.
* Whether or not the participants pane is open.
*/
handleClick: Function
_isOpen: boolean,
};
/**
@@ -32,7 +32,13 @@ class ParticipantsPaneButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
}

View File

@@ -33,6 +33,7 @@ import {
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY
} from './actionTypes';
import { type PREJOIN_SCREEN_STATE } from './constants';
import {
getFullDialOutNumber,
getDialOutConferenceUrl,
@@ -480,10 +481,10 @@ export function setPrejoinDeviceErrors(value: Object) {
/**
* Action used to set the visibility of the prejoin page.
*
* @param {boolean} value - The value.
* @param {string} value - The value.
* @returns {Object}
*/
export function setPrejoinPageVisibility(value: boolean) {
export function setPrejoinPageVisibility(value: PREJOIN_SCREEN_STATE) {
return {
type: SET_PREJOIN_PAGE_VISIBILITY,
value

View File

@@ -4,7 +4,6 @@ import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { getRoomName } from '../../base/conference';
import { getToolbarButtons } from '../../base/config';
import { translate } from '../../base/i18n';
import { Icon, IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
import { isVideoMutedByUser } from '../../base/media';
@@ -12,7 +11,6 @@ import { ActionButton, InputField, PreMeetingScreen, ToggleButton } from '../../
import { connect } from '../../base/redux';
import { getDisplayName, updateSettings } from '../../base/settings';
import { getLocalJitsiVideoTrack } from '../../base/tracks';
import { isButtonEnabled } from '../../toolbox/functions.web';
import {
joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
@@ -28,9 +26,6 @@ import {
} from '../functions';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
import DeviceStatus from './preview/DeviceStatus';
declare var interfaceConfig: Object;
type Props = {
@@ -84,11 +79,6 @@ type Props = {
*/
setJoinByPhoneDialogVisiblity: Function,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
@@ -99,26 +89,11 @@ type Props = {
*/
showErrorOnJoin: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
/**
* Flag signaling the visibility of the conference URL section.
*/
showConferenceInfo: boolean,
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
showDialog: boolean,
/**
* Flag signaling the visibility of the skip prejoin toggle
*/
showSkipPrejoin: boolean,
/**
* Used for translation.
*/
@@ -127,12 +102,7 @@ type Props = {
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
/**
* Array with the buttons which this Toolbox should display.
*/
visibleButtons: Array<string>
videoTrack: ?Object
};
type State = {
@@ -152,17 +122,6 @@ type State = {
* This component is displayed before joining a meeting.
*/
class Prejoin extends Component<Props, State> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showConferenceInfo: true,
showJoinActions: true,
showSkipPrejoin: true
};
/**
* Initializes a new {@code Prejoin} instance.
*
@@ -344,18 +303,15 @@ class Prejoin extends Component<Props, State> {
*/
render() {
const {
deviceStatusVisible,
hasJoinByPhoneButton,
joinConference,
joinConferenceWithoutAudio,
name,
showAvatar,
showCameraPreview,
showDialog,
showConferenceInfo,
showJoinActions,
t,
videoTrack,
visibleButtons
videoTrack
} = this.props;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress, _showDialogKeyPress,
@@ -364,89 +320,78 @@ class Prejoin extends Component<Props, State> {
return (
<PreMeetingScreen
footer = { this._renderFooter() }
name = { name }
showAvatar = { showAvatar }
showConferenceInfo = { showConferenceInfo }
showDeviceStatus = { deviceStatusVisible }
skipPrejoinButton = { this._renderSkipPrejoinButton() }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack }
visibleButtons = { visibleButtons }>
{showJoinActions && (
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<label
className = 'prejoin-input-area-label'
htmlFor = { 'Prejoin-input-field-id' } >
{ t('dialog.enterDisplayNameToJoin') }</label>
<InputField
autoComplete = { 'name' }
autoFocus = { true }
className = { showError ? 'error' : '' }
hasError = { showError }
id = { 'Prejoin-input-field-id' }
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
videoTrack = { videoTrack }>
<div
className = 'prejoin-input-area'
data-testid = 'prejoin.screen'>
<InputField
autoComplete = { 'name' }
autoFocus = { true }
className = { showError ? 'error' : '' }
hasError = { showError }
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
{showError && <div
className = 'prejoin-error'
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
{showError && <div
className = 'prejoin-error'
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }
onKeyPress = { _showDialogKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
hasOptions = { true }
onClick = { _onJoinButtonClick }
onKeyPress = { _onJoinKeyPress }
onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
testId = 'prejoin.joinMeeting'
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
</div>
</div>
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }
onKeyPress = { _showDialogKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
hasOptions = { true }
onClick = { _onJoinButtonClick }
onKeyPress = { _onJoinKeyPress }
onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
testId = 'prejoin.joinMeeting'
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
</div>
)}
</div>
{ showDialog && (
<JoinByPhoneDialog
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
@@ -456,26 +401,13 @@ class Prejoin extends Component<Props, State> {
);
}
/**
* Renders the screen footer if any.
*
* @returns {React$Element}
*/
_renderFooter() {
return this.props.deviceStatusVisible && <DeviceStatus />;
}
/**
* Renders the 'skip prejoin' button.
*
* @returns {React$Element}
*/
_renderSkipPrejoinButton() {
const { buttonIsToggled, t, showSkipPrejoin } = this.props;
if (!showSkipPrejoin) {
return null;
}
const { buttonIsToggled, t } = this.props;
return (
<div className = 'prejoin-checkbox-container'>
@@ -493,22 +425,11 @@ class Prejoin extends Component<Props, State> {
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The props passed to the component.
* @returns {Object}
*/
function mapStateToProps(state, ownProps): Object {
function mapStateToProps(state): Object {
const name = getDisplayName(state);
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
const { showJoinActions } = ownProps;
const isInviteButtonEnabled = isButtonEnabled('invite', state);
// Hide conference info when interfaceConfig is available and the invite button is disabled.
// In all other cases we want to preserve the behaviour and control the the conference info
// visibility through showJoinActions.
const showConferenceInfo
= typeof isInviteButtonEnabled === 'undefined' || isInviteButtonEnabled === true
? showJoinActions
: false;
return {
buttonIsToggled: isPrejoinSkipped(state),
@@ -519,9 +440,7 @@ function mapStateToProps(state, ownProps): Object {
showErrorOnJoin,
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
showCameraPreview: !isVideoMutedByUser(state),
showConferenceInfo,
videoTrack: getLocalJitsiVideoTrack(state),
visibleButtons: getToolbarButtons(state)
videoTrack: getLocalJitsiVideoTrack(state)
};
}

View File

@@ -9,27 +9,18 @@ import { getConferenceOptions } from '../../base/conference/functions';
import { setConfig } from '../../base/config';
import { DialogContainer } from '../../base/dialog';
import { createPrejoinTracks } from '../../base/tracks';
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider';
import { initPrejoin, makePrecallTest } from '../actions';
import Prejoin from './Prejoin';
import PrejoinThirdParty from './PrejoinThirdParty';
type Props = {
/**
* Indicates whether the avatar should be shown when video is off
* Indicates the style type that needs to be applied.
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
/**
* Flag signaling the visibility of the skip prejoin toggle
*/
showSkipPrejoin: boolean,
};
styleType: string
}
/**
* Wrapper application for prejoin.
@@ -50,14 +41,12 @@ export default class PrejoinApp extends BaseApp<Props> {
this._init.then(async () => {
const { store } = this.state;
const { dispatch } = store;
const { showAvatar, showJoinActions, showSkipPrejoin } = this.props;
const { styleType } = this.props;
super._navigate({
component: Prejoin,
component: PrejoinThirdParty,
props: {
showAvatar,
showJoinActions,
showSkipPrejoin
className: styleType
}
});
@@ -88,9 +77,11 @@ export default class PrejoinApp extends BaseApp<Props> {
*/
_createMainElement(component, props) {
return (
<AtlasKitThemeProvider mode = 'dark'>
{ super._createMainElement(component, props) }
</AtlasKitThemeProvider>
<JitsiThemeProvider>
<AtlasKitThemeProvider mode = 'dark'>
{ super._createMainElement(component, props) }
</AtlasKitThemeProvider>
</JitsiThemeProvider>
);
}
@@ -101,9 +92,11 @@ export default class PrejoinApp extends BaseApp<Props> {
*/
_renderDialogContainer() {
return (
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
<JitsiThemeProvider>
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
</JitsiThemeProvider>
);
}
}

View File

@@ -0,0 +1,86 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { isVideoMutedByUser } from '../../base/media';
import { PreMeetingScreen } from '../../base/premeeting';
import { connect } from '../../base/redux';
import { getLocalJitsiVideoTrack } from '../../base/tracks';
import { isDeviceStatusVisible } from '../functions';
type Props = {
/**
* Indicates the className that needs to be applied.
*/
className: string,
/**
* Flag signaling if the device status is visible or not.
*/
deviceStatusVisible: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* Used for translation.
*/
t: Function,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object
};
/**
* This component is displayed before joining a meeting.
*/
class PrejoinThirdParty extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
className,
deviceStatusVisible,
showCameraPreview,
videoTrack
} = this.props;
return (
<PreMeetingScreen
className = { `prejoin-third-party ${className}` }
showDeviceStatus = { deviceStatusVisible }
skipPrejoinButton = { false }
thirdParty = { true }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack } />
);
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The props passed to the component.
* @returns {Object}
*/
function mapStateToProps(state): Object {
return {
deviceStatusVisible: isDeviceStatusVisible(state),
showCameraPreview: !isVideoMutedByUser(state),
videoTrack: getLocalJitsiVideoTrack(state)
};
}
export default connect(mapStateToProps)(translate(PrejoinThirdParty));

View File

@@ -3,7 +3,7 @@
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconCheck, IconExclamation } from '../../../base/icons';
import { Icon, IconCheckSolid, IconExclamation } from '../../../base/icons';
import { connect } from '../../../base/redux';
import {
getDeviceStatusType,
@@ -38,11 +38,11 @@ export type Props = {
const iconMap = {
warning: {
src: IconExclamation,
className: 'prejoin-preview-status--warning'
className: 'device-icon--warning'
},
ok: {
src: IconCheck,
className: 'prejoin-preview-status--ok'
src: IconCheckSolid,
className: 'device-icon--ok'
}
};
@@ -57,15 +57,14 @@ function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props
return (
<div
className = { `prejoin-preview-status ${className}` }
className = 'device-status'
role = 'alert'
tabIndex = { -1 }>
<Icon
className = 'prejoin-preview-icon'
className = { `device-icon ${className}` }
size = { 16 }
src = { src } />
<span
className = 'prejoin-preview-error-desc'
role = 'heading'>
{t(deviceStatusText)}
</span>

View File

@@ -0,0 +1,19 @@
// @flow
export type PREJOIN_SCREEN_STATE = "hidden" | "loading" | true;
type PREJOIN_SCREEN_STATE_TYPE = {
HIDDEN: PREJOIN_SCREEN_STATE,
LOADING: PREJOIN_SCREEN_STATE,
VISIBLE: PREJOIN_SCREEN_STATE
}
/**
* Enum of possible prejoin screen states.
*/
export const PREJOIN_SCREEN_STATES: PREJOIN_SCREEN_STATE_TYPE = {
HIDDEN: 'hidden',
LOADING: 'loading',
VISIBLE: true // backwards compatibility with old boolean implementation
};

View File

@@ -4,6 +4,8 @@ import { getRoomName } from '../base/conference';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media';
import { PREJOIN_SCREEN_STATES } from './constants';
/**
* Selector for the visibility of the 'join by phone' button.
*
@@ -160,7 +162,17 @@ export function isPrejoinPageEnabled(state: Object): boolean {
* @returns {boolean}
*/
export function isPrejoinPageVisible(state: Object): boolean {
return isPrejoinPageEnabled(state) && state['features/prejoin']?.showPrejoin;
return isPrejoinPageEnabled(state) && state['features/prejoin']?.showPrejoin === PREJOIN_SCREEN_STATES.VISIBLE;
}
/**
* Returns true if the prejoin page is loading.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinPageLoading(state: Object): boolean {
return isPrejoinPageEnabled(state) && state['features/prejoin']?.showPrejoin === PREJOIN_SCREEN_STATES.LOADING;
}
/**

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