Compare commits

...

15 Commits

Author SHA1 Message Date
Avram Tudor 813b6fb794 feat(prejoin) allow disabling prejoin display name editing (#11575) 2022-05-26 15:39:14 +03:00
Avram Tudor 40dd3e5193 feat(undock) expose buttons for docking / undocking iframe (#11560) 2022-05-26 11:54:40 +03:00
Hristo Terezov 8a9410cfdb chore: Update LJM 2022-05-24 16:42:17 -05:00
Hristo Terezov 24da3fb532 fix(deviceSelection):prejoin-update current device 2022-05-24 16:40:24 -05:00
Hristo Terezov 066dddcf3f fix(device-selection): Handle properly on prejoin
The device selection initialization on the prejoin use case was handled
like the welcome page. This was introducing issues with selecting the
stored devices and not the ones used, enabling the device selection when
it will fail and others.
2022-05-24 16:40:08 -05:00
Hristo Terezov da723ddc0d fix(device-selection): disable preview for ios 2022-05-24 16:39:57 -05:00
Hristo Terezov 6b0bd811e8 fix(device-selection):iOS Safari disable previews
On iOS Safari when the tracks for the previews are created the old ones
are auto destroyed which leads many issues like stop sending media while
the devie selectioin is displayed, error messages, etc.
2022-05-23 15:47:05 -05:00
Robert Pintilii 9433d65995 fix: Stage filmstrip (#11495) 2022-05-05 10:13:44 -05:00
Mihaela Dumitru 2bf526afa5
feat(external-api) add breakout room configs to hide auto assign and footer menu buttons (#11443) (#11467)
* hideAutoAssignButton
* hideFooterMenu
* hideModeratorSettingsTab
* hideMoreActionsButton
* hideMuteAllButton
2022-04-29 19:51:46 +03:00
Jaya Allamsetty ab0bdaae70 fix(stageFilmstrip) Disable stage filmstrip by default. 2022-04-29 11:33:17 -05:00
Hristo Terezov 9e70cae99f fix(tile-view):Recalculate on window height change 2022-04-29 11:08:35 -05:00
Saúl Ibarra Corretgé ba76cc7544 fix(virtual-background) don't treat timeout as fatal failure
If downloading the model tiemouts, it may succeed in the (near) future,
don't just give up.
2022-04-29 10:26:44 +02:00
Mihaela Dumitru 1dc38e5be6 fix(breakout-rooms) reset rooms when conference is left or failed (#11447) 2022-04-28 13:43:48 -05:00
Jaya Allamsetty 4c693c98ce chore(deps) Update LJM.
fix(browser-support) Enable device selection in mobile Safari.
With https://bugs.webkit.org/show_bug.cgi?id=179363 being fixed, we should now be able to switch between devices in call.

fix(BrowserCapabilities) Fallback to plan-b for Electron clients that have chromium ver < 96.
2022-04-28 12:26:21 -04:00
Jaya Allamsetty ff701e6181 fix(device-selection) Enable device selection on mobile Safari. (#11427)
* fix(device-selection) Enable device selection on mobile Safari.
With https://bugs.webkit.org/show_bug.cgi?id=179363 being fixed, we should now be able to switch between devices in call. Also, before the webkit fix, we were able to continue to use the old track when a new track was created for preview in device settings before joining the call. This doesn't work anymore after the fix. Therefore, always replace the track in redux even if the selected device hasn't changed. Depends on https://github.com/jitsi/lib-jitsi-meet/pull/1993.
2022-04-28 11:49:16 -04:00
36 changed files with 459 additions and 129 deletions

View File

@ -485,6 +485,11 @@ var config = {
// Hides add breakout room button
// hideAddRoomButton: false,
// Hides the participant name editing field in the prejoin screen.
// If requireDisplayName is also set as true, a name should still be provided through
// either the jwt or the userInfo from the iframe api init object in order for this to have an effect.
// hidePrejoinDisplayName: false,
// Require users to always specify a display name.
// requireDisplayName: true,
@ -599,6 +604,7 @@ var config = {
// 'chat',
// 'closedcaptions',
// 'desktop',
// 'dock-iframe'
// 'download',
// 'embedmeeting',
// 'etherpad',
@ -627,6 +633,7 @@ var config = {
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'undock-iframe',
// 'videoquality',
// '__end'
// ],
@ -1093,8 +1100,18 @@ var config = {
// breakoutRooms: {
// // Hides the add breakout room button. This replaces `hideAddRoomButton`.
// hideAddRoomButton: false,
// // Hides the auto assign participants button.
// hideAutoAssignButton: false,
// // Hides the participants pane footer menu.
// hideFooterMenu: false,
// // Hides the join breakout room button.
// hideJoinRoomButton: false
// hideJoinRoomButton: false,
// // Hides the moderator settings tab.
// hideModeratorSettingsTab: false,
// // Hides the more actions button.
// hideMoreActionsButton: false,
// // Hides the mute all button.
// hideMuteAllButton: false
// },
// When true the user cannot add more images to be used as virtual background.

View File

@ -3,6 +3,19 @@
width: 100%;
}
&-avatar {
margin: 8px auto 16px;
&-name {
color: white;
font-size: 16px;
font-weight: 600;
line-height: 26px;
margin-bottom: 32px;
text-align: center;
}
}
&-error {
background-color: #E04757;
border-radius: 6px;

View File

@ -1013,6 +1013,7 @@
"chat": "Open / Close chat",
"clap": "Clap",
"collapse": "Collapse",
"dock": "Dock in main window",
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
@ -1063,6 +1064,7 @@
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip",
"undock": "Undock into separate window",
"videoblur": "Toggle video blur",
"videomute": "Start / Stop camera"
},
@ -1079,6 +1081,7 @@
"closeChat": "Close chat",
"closeReactionsMenu": "Close reactions menu",
"disableReactionSounds": "You can disable reaction sounds for this meeting",
"dock": "Dock in main window",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
@ -1147,6 +1150,7 @@
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
"undock": "Undock into separate window",
"videoSettings": "Video settings",
"videomute": "Start / Stop camera"
},

View File

@ -1441,6 +1441,22 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the iframe
* docked state has been changed. The responsibility for implementing
* the dock / undock functionality lies with the external application.
*
* @param {boolean} docked - Whether or not the iframe has been set to
* be docked or undocked.
* @returns {void}
*/
notifyIframeDockStateChanged(docked: boolean) {
this._sendEvent({
name: 'iframe-dock-state-changed',
docked
});
}
/**
* Notify external application of a participant, remote or local, being
* removed from the conference by another participant.

View File

@ -106,6 +106,7 @@ const events = {
'feedback-submitted': 'feedbackSubmitted',
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
'filmstrip-display-changed': 'filmstripDisplayChanged',
'iframe-dock-state-changed': 'iframeDockStateChanged',
'incoming-message': 'incomingMessage',
'knocking-participant': 'knockingParticipant',
'log': 'log',

11
package-lock.json generated
View File

@ -72,7 +72,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1415.0.0+fa916d41/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fe484f46ee88812f21e091516889802e48db6c2d",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.2",
@ -11796,8 +11796,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1415.0.0+fa916d41/lib-jitsi-meet.tgz",
"integrity": "sha512-xCDIkUykAYPLocmnOItFC1PYNYVMTp57XwJ1PXvOwHV4lZO9RBG36ln5QBUonD2P0X6di2UGiRzOi9l4FaHoLQ==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#fe484f46ee88812f21e091516889802e48db6c2d",
"integrity": "sha512-M9O/XF2itBNTklCOpKTeNT2hseZIztLkwvL7usxWmzyaU+R4BAeEdPLSBwwBe4S2CuPmnvPDtBstpRCt/WhPqg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@ -28899,8 +28899,9 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1415.0.0+fa916d41/lib-jitsi-meet.tgz",
"integrity": "sha512-xCDIkUykAYPLocmnOItFC1PYNYVMTp57XwJ1PXvOwHV4lZO9RBG36ln5QBUonD2P0X6di2UGiRzOi9l4FaHoLQ==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#fe484f46ee88812f21e091516889802e48db6c2d",
"integrity": "sha512-M9O/XF2itBNTklCOpKTeNT2hseZIztLkwvL7usxWmzyaU+R4BAeEdPLSBwwBe4S2CuPmnvPDtBstpRCt/WhPqg==",
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#fe484f46ee88812f21e091516889802e48db6c2d",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@ -77,7 +77,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1415.0.0+fa916d41/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fe484f46ee88812f21e091516889802e48db6c2d",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.2",

View File

@ -167,6 +167,7 @@ export default [
'hideConferenceSubject',
'hideDisplayName',
'hideDominantSpeakerBadge',
'hidePrejoinDisplayName',
'hideRecordingLabel',
'hideParticipantsStats',
'hideConferenceTimer',

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.3332 3.33329C18.3332 2.41282 17.587 1.66663 16.6665 1.66663H8.33317C7.4127 1.66663 6.6665 2.41282 6.6665 3.33329V4.99996H3.33317C2.4127 4.99996 1.6665 5.74615 1.6665 6.66663V16.6666C1.6665 17.5871 2.4127 18.3333 3.33317 18.3333H13.3332C14.2536 18.3333 14.9998 17.5871 14.9998 16.6666V15H16.6665C17.587 15 18.3332 14.2538 18.3332 13.3333V3.33329ZM8.33317 3.33329H16.6665L16.6665 13.3333H14.9998V6.66663C14.9998 5.74615 14.2536 4.99996 13.3332 4.99996H8.33317V3.33329ZM3.33317 6.66663V16.6666H13.3332V6.66663H3.33317ZM6.6665 12.1024V9.99996C6.6665 9.53972 6.29341 9.16663 5.83317 9.16663C5.37293 9.16663 4.99984 9.53972 4.99984 9.99996V14.1296V14.1666C4.99984 14.5693 5.28549 14.9053 5.66523 14.983C5.71947 14.9941 5.77564 15 5.83317 15L5.83356 14.9992C5.83397 14.9992 5.83439 14.9992 5.8348 14.9992L5.83445 15H5.87022H9.99984C10.4601 15 10.8332 14.6269 10.8332 14.1666C10.8332 13.7064 10.4601 13.3333 9.99984 13.3333H7.89741L11.4116 9.81913C11.7515 9.47922 11.7515 8.92813 11.4116 8.58822C11.0717 8.24832 10.5206 8.24832 10.1807 8.58822L6.6665 12.1024Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -44,6 +44,7 @@ export { default as IconDeviceBluetooth } from './bluetooth.svg';
export { default as IconDeviceEarpiece } from './phone-talk.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDeviceSpeaker } from './volume.svg';
export { default as IconDock } from './dock.svg';
export { default as IconDeviceDocument } from './document.svg';
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
@ -130,6 +131,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconTrash } from './trash.svg';
export { default as IconUndock } from './undock.svg';
export { default as IconUnpin } from './unpin.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6665 1.66663H6.6665C5.74603 1.66663 4.99984 2.41282 4.99984 3.33329V4.99996H3.33317C2.4127 4.99996 1.6665 5.74615 1.6665 6.66663V16.6666C1.6665 17.5871 2.4127 18.3333 3.33317 18.3333H13.3332C14.2536 18.3333 14.9998 17.5871 14.9998 16.6666V15H16.6665C17.587 15 18.3332 14.2538 18.3332 13.3333V3.33329C18.3332 2.41282 17.587 1.66663 16.6665 1.66663ZM13.3332 16.6666V15H6.6665C5.74603 15 4.99984 14.2538 4.99984 13.3333V6.66663H3.33317V16.6666H13.3332ZM6.6665 3.33329V13.3333H16.6665V3.33329H6.6665ZM9.99984 4.99996C9.5396 4.99996 9.1665 5.37306 9.1665 5.83329C9.1665 6.29353 9.5396 6.66663 9.99984 6.66663H12.1023L8.5881 10.1808C8.24819 10.5207 8.24819 11.0718 8.5881 11.4117C8.928 11.7516 9.4791 11.7516 9.819 11.4117L13.3332 7.89753V9.99996C13.3332 10.4602 13.7063 10.8333 14.1665 10.8333C14.6267 10.8333 14.9998 10.4602 14.9998 9.99996V5.83329C14.9998 5.37306 14.6267 4.99996 14.1665 4.99996H14.1295H9.99984Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,6 +5,7 @@ import { batch } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
@ -330,6 +331,7 @@ StateListenerRegistry.register(
for (const track of remoteTracks) {
dispatch(trackRemoved(track.jitsiTrack));
}
dispatch({ type: _RESET_BREAKOUT_ROOMS });
});
}
});

View File

@ -3,6 +3,7 @@
import _ from 'lodash';
import { getCurrentConference } from '../base/conference';
import { getParticipantCount, isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { FEATURE_KEY } from './constants';
@ -85,3 +86,40 @@ export const getBreakoutRoomsConfig = (stateful: Function | Object) => {
return breakoutRooms;
};
/**
* Returns whether the add breakout room button is visible.
*
* @param {Function | Object} stateful - Global state.
* @returns {boolean}
*/
export const isAddBreakoutRoomButtonVisible = (stateful: Function | Object) => {
const state = toState(stateful);
const isLocalModerator = isLocalParticipantModerator(state);
const { conference } = state['features/base/conference'];
const isBreakoutRoomsSupported = conference?.getBreakoutRooms()?.isSupported();
const { hideAddRoomButton } = getBreakoutRoomsConfig(state);
return isLocalModerator && isBreakoutRoomsSupported && !hideAddRoomButton;
};
/**
* Returns whether the auto assign participants to breakout rooms button is visible.
*
* @param {Function | Object} stateful - Global state.
* @returns {boolean}
*/
export const isAutoAssignParticipantsVisible = (stateful: Function | Object) => {
const state = toState(stateful);
const rooms = getBreakoutRooms(state);
const inBreakoutRoom = isInBreakoutRoom(state);
const isLocalModerator = isLocalParticipantModerator(state);
const participantsCount = getParticipantCount(state);
const { hideAutoAssignButton } = getBreakoutRoomsConfig(state);
return !inBreakoutRoom
&& isLocalModerator
&& participantsCount > 2
&& Object.keys(rooms).length > 1
&& !hideAutoAssignButton;
};

View File

@ -14,36 +14,32 @@ import logger from './logger';
* Submits the settings related to device selection.
*
* @param {Object} newState - The new settings.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Function}
*/
export function submitDeviceSelectionTab(newState) {
export function submitDeviceSelectionTab(newState, isDisplayedOnWelcomePage) {
return (dispatch, getState) => {
const currentState = getDeviceSelectionDialogProps(getState());
const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
if (newState.selectedVideoInputId
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(
setVideoInputDevice(newState.selectedVideoInputId));
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
}
if (newState.selectedAudioInputId
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
dispatch(updateSettings({
userSelectedMicDeviceId: newState.selectedAudioInputId,
userSelectedMicDeviceLabel:
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
}));
dispatch(
setAudioInputDevice(newState.selectedAudioInputId));
dispatch(setAudioInputDevice(newState.selectedAudioInputId));
}
if (newState.selectedAudioOutputId

View File

@ -27,29 +27,37 @@ import {
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Object} - The properties for the device selection dialog.
*/
export function getDeviceSelectionDialogProps(stateful: Object | Function) {
export function getDeviceSelectionDialogProps(stateful: Object | Function, isDisplayedOnWelcomePage) {
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
// by the browser when a new track is created for preview. That's why we are disabling all previews.
const disablePreviews = isIosMobileBrowser();
const state = toState(stateful);
const settings = state['features/base/settings'];
const { conference } = state['features/base/conference'];
const { permissions } = state['features/base/devices'];
const isMobileSafari = isIosMobileBrowser();
const cameraChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
const userSelectedMic = getUserSelectedMicDeviceId(state);
let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
let disableVideoInputSelect = !cameraChangeSupported;
let selectedAudioInputId = isMobileSafari ? userSelectedMic : settings.micDeviceId;
// When the previews are disabled we don't need multiple audio input support in order to chage the mic. This is the
// case for Safari on iOS.
let disableAudioInputChange
= !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
let disableVideoInputSelect = !inputDeviceChangeSupported;
let selectedAudioInputId = settings.micDeviceId;
let selectedAudioOutputId = getAudioOutputDeviceId();
let selectedVideoInputId = isMobileSafari ? userSelectedCamera : settings.cameraDeviceId;
let selectedVideoInputId = settings.cameraDeviceId;
// audio input change will be a problem only when we are in a
// conference and this is not supported, when we open device selection on
// welcome page changing input devices will not be a problem
// on welcome page we also show only what we have saved as user selected devices
if (!conference) {
if (isDisplayedOnWelcomePage) {
disableAudioInputChange = false;
disableVideoInputSelect = false;
selectedAudioInputId = userSelectedMic;
@ -66,10 +74,10 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
disableVideoInputSelect,
hasAudioPermission: permissions.audio,
hasVideoPermission: permissions.video,
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputPreview: !speakerChangeSupported,
hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
hideAudioOutputSelect: !speakerChangeSupported,
hideVideoInputPreview: !cameraChangeSupported,
hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
selectedAudioInputId,
selectedAudioOutputId,
selectedVideoInputId

View File

@ -713,5 +713,5 @@ export function shouldDisplayStageFilmstrip(state, minParticipantCount = 2) {
export function isStageFilmstripEnabled(state) {
const { filmstrip } = state['features/base/config'];
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
return !(filmstrip?.disableStageFilmstrip ?? true) && interfaceConfig.VERTICAL_FILMSTRIP;
}

View File

@ -58,7 +58,7 @@ const DEFAULT_STATE = {
* @public
* @type {Number}
*/
maxStageParticipants: 4,
maxStageParticipants: 1,
/**
* The custom audio volume levels per participant.

View File

@ -61,8 +61,13 @@ StateListenerRegistry.register(
*/
StateListenerRegistry.register(
/* selector */ state => {
return { layout: getCurrentLayout(state),
width: state['features/base/responsive-ui'].clientWidth };
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
return {
layout: getCurrentLayout(state),
height: clientHeight,
width: clientWidth
};
},
/* listener */ ({ layout }, store) => {
switch (layout) {

View File

@ -4,13 +4,14 @@ import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import useContextMenu from '../../../../../base/components/context-menu/useContextMenu';
import { getParticipantCount, isLocalParticipantModerator } from '../../../../../base/participants';
import { isLocalParticipantModerator } from '../../../../../base/participants';
import { equals } from '../../../../../base/redux';
import {
getBreakoutRooms,
isInBreakoutRoom,
getCurrentRoomId,
getBreakoutRoomsConfig
getBreakoutRoomsConfig,
isAutoAssignParticipantsVisible
} from '../../../../../breakout-rooms/functions';
import { showOverflowDrawer } from '../../../../../toolbox/functions';
@ -36,7 +37,7 @@ export const RoomList = ({ searchString }: Props) => {
.sort((p1: Object, p2: Object) => (p1?.name || '').localeCompare(p2?.name || ''));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isLocalModerator = useSelector(isLocalParticipantModerator);
const participantsCount = useSelector(getParticipantCount);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
const _overflowDrawer = useSelector(showOverflowDrawer);
const [ lowerMenu, raiseMenu, toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
@ -46,11 +47,7 @@ export const RoomList = ({ searchString }: Props) => {
return (
<>
{inBreakoutRoom && <LeaveButton />}
{!inBreakoutRoom
&& isLocalModerator
&& participantsCount > 2
&& rooms.length > 1
&& <AutoAssignButton />}
{showAutoAssign && <AutoAssignButton />}
<div id = 'breakout-rooms-list'>
{rooms.map((room: Object) => (
<React.Fragment key = { room.id }>

View File

@ -8,20 +8,19 @@ import { useDispatch, useSelector } from 'react-redux';
import { openDialog } from '../../../base/dialog';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import {
getParticipantCount,
isLocalParticipantModerator
} from '../../../base/participants';
import { isLocalParticipantModerator } from '../../../base/participants';
import { equals } from '../../../base/redux';
import {
getBreakoutRooms,
getBreakoutRoomsConfig,
getCurrentRoomId,
isAddBreakoutRoomButtonVisible,
isAutoAssignParticipantsVisible,
isInBreakoutRoom
} from '../../../breakout-rooms/functions';
import { getKnockingParticipants } from '../../../lobby/functions';
import MuteEveryoneDialog
from '../../../video-menu/components/native/MuteEveryoneDialog';
import { isFooterMenuVisible, isMoreActionsVisible, isMuteAllVisible } from '../../functions';
import {
AddBreakoutRoomButton,
AutoAssignButton,
@ -49,21 +48,18 @@ const ParticipantsPane = () => {
[ dispatch ]);
const { t } = useTranslation();
const { hideAddRoomButton } = useSelector(getBreakoutRoomsConfig);
const { conference } = useSelector(state => state['features/base/conference']);
// $FlowExpectedError
const _isBreakoutRoomsSupported = conference?.getBreakoutRooms()?.isSupported();
const currentRoomId = useSelector(getCurrentRoomId);
const rooms: Array<Object> = Object.values(useSelector(getBreakoutRooms, equals))
.filter((room: Object) => room.id !== currentRoomId)
.sort((p1: Object, p2: Object) => (p1?.name || '').localeCompare(p2?.name || ''));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const participantsCount = useSelector(getParticipantCount);
const autoAssign = !inBreakoutRoom && isLocalModerator
&& participantsCount > 2 && rooms.length > 1;
const addBreakoutRoom
= _isBreakoutRoomsSupported && !hideAddRoomButton && isLocalModerator;
const showAddBreakoutRoom = useSelector(isAddBreakoutRoomButtonVisible);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
const showFooterMenu = useSelector(isFooterMenuVisible);
const showMoreActions = useSelector(isMoreActionsVisible);
const showMuteAll = useSelector(isMuteAllVisible);
const lobbyParticipants = useSelector(getKnockingParticipants);
return (
@ -76,7 +72,7 @@ const ParticipantsPane = () => {
searchString = { searchString }
setSearchString = { setSearchString } />
{
autoAssign && <AutoAssignButton />
showAutoAssign && <AutoAssignButton />
}
{
inBreakoutRoom && <LeaveBreakoutRoomButton />
@ -89,23 +85,31 @@ const ParticipantsPane = () => {
searchString = { searchString } />))
}
{
addBreakoutRoom && <AddBreakoutRoomButton />
showAddBreakoutRoom && <AddBreakoutRoomButton />
}
{
isLocalModerator
showFooterMenu
&& <View style = { styles.participantsPaneFooter }>
{
showMuteAll && (
<Button
children = { t('participantsPane.actions.muteAll') }
labelStyle = { styles.muteAllLabel }
mode = 'contained'
onPress = { muteAll }
style = { styles.muteAllMoreButton } />
)
}
{
showMoreActions && (
<Button
icon = { HorizontalDotsIcon }
labelStyle = { styles.moreIcon }
mode = 'contained'
onPress = { openMoreMenu }
style = { styles.moreButton } />
)
}
</View>
}
</JitsiScreen>

View File

@ -249,6 +249,7 @@ export default {
bottom: 0,
flexDirection: 'row',
height: BaseTheme.spacing[12],
justifyContent: 'flex-end',
left: 0,
right: 0,
position: 'absolute',
@ -277,7 +278,8 @@ export default {
},
moreButton: {
...smallButton
...smallButton,
marginLeft: BaseTheme.spacing[3]
},
moreIcon: {
@ -291,8 +293,7 @@ export default {
},
muteAllMoreButton: {
...muteAllButton,
right: BaseTheme.spacing[3]
...muteAllButton
},
muteAllLabel: {

View File

@ -7,12 +7,17 @@ import participantsPaneTheme from '../../../base/components/themes/participantsP
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getBreakoutRoomsConfig } from '../../../breakout-rooms/functions';
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
import { MuteEveryoneDialog } from '../../../video-menu/components/';
import { close } from '../../actions';
import { findAncestorByClass, getParticipantsPaneOpen } from '../../functions';
import {
findAncestorByClass,
getParticipantsPaneOpen,
isFooterMenuVisible,
isMoreActionsVisible,
isMuteAllVisible
} from '../../functions';
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
import { RoomList } from '../breakout-rooms/components/web/RoomList';
@ -36,15 +41,25 @@ type Props = {
*/
_overflowDrawer: boolean,
/**
* Is the participants pane open.
*/
_paneOpen: boolean,
/**
* Should the add breakout room button be displayed?
*/
_showAddRoomButton: boolean,
/**
* Is the participants pane open.
* Whether to show the more actions button.
*/
_paneOpen: boolean,
_showMoreActionsButton: boolean,
/**
* Whether to show the mute all button.
*/
_showMuteAllButton: boolean,
/**
* Whether to show the footer menu.
@ -202,6 +217,8 @@ class ParticipantsPane extends Component<Props, State> {
_paneOpen,
_showAddRoomButton,
_showFooter,
_showMoreActionsButton,
_showMuteAllButton,
classes,
t
} = this.props;
@ -240,11 +257,14 @@ class ParticipantsPane extends Component<Props, State> {
</div>
{_showFooter && (
<div className = { classes.footer }>
{_showMuteAllButton && (
<FooterButton
accessibilityLabel = { t('participantsPane.actions.muteAll') }
onClick = { this._onMuteAll }>
{t('participantsPane.actions.muteAll')}
</FooterButton>
)}
{_showMoreActionsButton && (
<div className = { classes.footerMoreContainer }>
<FooterButton
accessibilityLabel = { t('participantsPane.actions.moreModerationActions') }
@ -258,6 +278,7 @@ class ParticipantsPane extends Component<Props, State> {
onDrawerClose = { this._onDrawerClose }
onMouseLeave = { this._onToggleContext } />
</div>
)}
</div>
)}
</div>
@ -374,18 +395,16 @@ class ParticipantsPane extends Component<Props, State> {
*/
function _mapStateToProps(state: Object) {
const isPaneOpen = getParticipantsPaneOpen(state);
const { hideAddRoomButton } = getBreakoutRoomsConfig(state);
const { conference } = state['features/base/conference'];
// $FlowExpectedError
const _isBreakoutRoomsSupported = conference?.getBreakoutRooms()?.isSupported();
const _isLocalParticipantModerator = isLocalParticipantModerator(state);
return {
_isBreakoutRoomsSupported,
_paneOpen: isPaneOpen,
_showAddRoomButton: _isBreakoutRoomsSupported && !hideAddRoomButton && _isLocalParticipantModerator,
_showFooter: isPaneOpen && isLocalParticipantModerator(state)
_showAddRoomButton: isAddBreakoutRoomButtonVisible(state),
_showFooter: isFooterMenuVisible(state),
_showMuteAllButton: isMuteAllVisible(state),
_showMoreActionsButton: isMoreActionsVisible(state)
};
}

View File

@ -18,7 +18,7 @@ import {
} from '../base/participants/functions';
import { toState } from '../base/redux';
import { normalizeAccents } from '../base/util/strings';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { getBreakoutRoomsConfig, isInBreakoutRoom } from '../breakout-rooms/functions';
import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants';
@ -261,3 +261,49 @@ export function participantMatchesSearch(participant: Object, searchString: stri
return false;
}
/**
* Returns whether the participants pane footer menu is visible.
*
* @param {Object} state - Global state.
* @returns {boolean}
*/
export const isFooterMenuVisible = (state: Object) => {
const isLocalModerator = isLocalParticipantModerator(state);
const inBreakoutRoom = isInBreakoutRoom(state);
const { hideFooterMenu } = getBreakoutRoomsConfig(state);
return inBreakoutRoom
? !hideFooterMenu && isLocalModerator
: isLocalModerator;
};
/**
* Returns whether the more actions button is visible.
*
* @param {Object} state - Global state.
* @returns {boolean}
*/
export const isMoreActionsVisible = (state: Object) => {
const inBreakoutRoom = isInBreakoutRoom(state);
const { hideMoreActionsButton } = getBreakoutRoomsConfig(state);
return inBreakoutRoom
? !hideMoreActionsButton
: true;
};
/**
* Returns whether the mute all button is visible.
*
* @param {Object} state - Global state.
* @returns {boolean}
*/
export const isMuteAllVisible = (state: Object) => {
const inBreakoutRoom = isInBreakoutRoom(state);
const { hideMuteAllButton } = getBreakoutRoomsConfig(state);
return inBreakoutRoom
? !hideMuteAllButton
: true;
};

View File

@ -9,6 +9,7 @@ import { getDialOutStatusUrl, getDialOutUrl, updateConfig } from '../base/config
import { browser } from '../base/lib-jitsi-meet';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { isVideoMutedByUser, MEDIA_TYPE } from '../base/media';
import { updateSettings } from '../base/settings';
import {
createLocalTracksF,
getLocalAudioTrack,
@ -359,7 +360,11 @@ export function replaceAudioTrackById(deviceId: string) {
const newTrack = await createLocalTrack('audio', deviceId);
const oldTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
dispatch(replaceLocalTrack(oldTrack, newTrack));
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
micDeviceId: newTrack.getDeviceId()
}));
});
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
logger.log('Error replacing audio track', err);
@ -386,7 +391,11 @@ export function replaceVideoTrackById(deviceId: Object) {
);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
dispatch(replaceLocalTrack(oldTrack, newTrack));
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
cameraDeviceId: newTrack.getDeviceId()
}));
});
wasVideoMuted && newTrack.mute();
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));

View File

@ -3,11 +3,13 @@
import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { Avatar } from '../../base/avatar';
import { getRoomName } from '../../base/conference';
import { isNameReadOnly } from '../../base/config';
import { translate } from '../../base/i18n';
import { IconArrowDown, IconArrowUp, IconPhone, IconVolumeOff } from '../../base/icons';
import { isVideoMutedByUser } from '../../base/media';
import { getLocalParticipant } from '../../base/participants';
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
import { connect } from '../../base/redux';
import { getDisplayName, updateSettings } from '../../base/settings';
@ -21,7 +23,8 @@ import {
isDeviceStatusVisible,
isDisplayNameRequired,
isJoinByPhoneButtonVisible,
isJoinByPhoneDialogVisible
isJoinByPhoneDialogVisible,
isPrejoinDisplayNameVisible
} from '../functions';
import DropdownButton from './DropdownButton';
@ -29,6 +32,11 @@ import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
type Props = {
/**
* Indicates whether the display name is editable.
*/
canEditDisplayName: boolean,
/**
* Flag signaling if the device status is visible or not.
*/
@ -59,6 +67,11 @@ type Props = {
*/
updateSettings: Function,
/**
* Local participant id.
*/
participantId: string,
/**
* The prejoin config.
*/
@ -145,6 +158,8 @@ class Prejoin extends Component<Props, State> {
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
this._getExtraJoinButtons = this._getExtraJoinButtons.bind(this);
this.showDisplayNameField = props.canEditDisplayName || props.showErrorOnJoin;
}
_onJoinButtonClick: () => void;
@ -330,6 +345,7 @@ class Prejoin extends Component<Props, State> {
joinConference,
joinConferenceWithoutAudio,
name,
participantId,
prejoinConfig,
readOnlyName,
showCameraPreview,
@ -360,7 +376,7 @@ class Prejoin extends Component<Props, State> {
<div
className = 'prejoin-input-area'
data-testid = 'prejoin.screen'>
<InputField
{this.showDisplayNameField ? (<InputField
autoComplete = { 'name' }
autoFocus = { true }
className = { showError ? 'error' : '' }
@ -370,6 +386,16 @@ class Prejoin extends Component<Props, State> {
placeHolder = { t('dialog.enterDisplayName') }
readOnly = { readOnlyName }
value = { name } />
) : (
<>
<Avatar
className = 'prejoin-avatar'
displayName = { name }
participantId = { participantId }
size = { 72 } />
<div className = 'prejoin-avatar-name'>{name}</div>
</>
)}
{showError && <div
className = 'prejoin-error'
@ -423,18 +449,21 @@ class Prejoin extends Component<Props, State> {
function mapStateToProps(state): Object {
const name = getDisplayName(state);
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
const { id: participantId } = getLocalParticipant(state);
return {
name,
canEditDisplayName: isPrejoinDisplayNameVisible(state),
deviceStatusVisible: isDeviceStatusVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
name,
participantId,
prejoinConfig: state['features/base/config'].prejoinConfig,
readOnlyName: isNameReadOnly(state),
roomName: getRoomName(state),
showCameraPreview: !isVideoMutedByUser(state),
showDialog: isJoinByPhoneDialogVisible(state),
showErrorOnJoin,
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
readOnlyName: isNameReadOnly(state),
showCameraPreview: !isVideoMutedByUser(state),
videoTrack: getLocalJitsiVideoTrack(state),
prejoinConfig: state['features/base/config'].prejoinConfig
videoTrack: getLocalJitsiVideoTrack(state)
};
}

View File

@ -36,6 +36,16 @@ export function isDisplayNameRequired(state: Object): boolean {
|| state['features/base/config'].requireDisplayName;
}
/**
* Selector for determining if the prejoin display name field is visible.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinDisplayNameVisible(state: Object): boolean {
return !state['features/base/config'].hidePrejoinDisplayName;
}
/**
* Returns the text for the prejoin status bar.
*

View File

@ -43,10 +43,15 @@ export function openLogoutDialog(onLogout: Function) {
*
* @param {string} defaultTab - The tab in {@code SettingsDialog} that should be
* displayed initially.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Function}
*/
export function openSettingsDialog(defaultTab: string) {
return openDialog(SettingsDialog, { defaultTab });
export function openSettingsDialog(defaultTab: string, isDisplayedOnWelcomePage: boolean) {
return openDialog(SettingsDialog, {
defaultTab,
isDisplayedOnWelcomePage
});
}
/**

View File

@ -531,7 +531,11 @@ class MoreTab extends AbstractDialogTab<Props, State> {
* @returns {ReactElement}
*/
_renderMaxStageParticipantsSelect() {
const { maxStageParticipants, t } = this.props;
const { maxStageParticipants, t, stageFilmstripEnabled } = this.props;
if (!stageFilmstripEnabled) {
return null;
}
const maxParticipantsItems = Array(MAX_ACTIVE_PARTICIPANTS).fill(0)
.map((no, index) => (
<DropdownItem

View File

@ -21,7 +21,13 @@ type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
dispatch: Function,
/**
* Indicates whether the device selection dialog is displayed on the
* welcome page or not.
*/
isDisplayedOnWelcomePage: boolean
};
/**
@ -40,10 +46,10 @@ class SettingsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { defaultTab = SETTINGS_TABS.DEVICES, dispatch } = this.props;
const { defaultTab = SETTINGS_TABS.DEVICES, dispatch, isDisplayedOnWelcomePage = false } = this.props;
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab));
dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));
}
}

View File

@ -59,7 +59,13 @@ type Props = {
/**
* Invoked to save changed settings.
*/
dispatch: Function
dispatch: Function,
/**
* Indicates whether the device selection dialog is displayed on the
* welcome page or not.
*/
isDisplayedOnWelcomePage: boolean
};
/**
@ -253,7 +259,7 @@ class SettingsDialog extends Component<Props> {
* }}
*/
function _mapStateToProps(state, ownProps) {
const { classes } = ownProps;
const { classes, isDisplayedOnWelcomePage } = ownProps;
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
// The settings sections to display.
@ -276,7 +282,7 @@ function _mapStateToProps(state, ownProps) {
component: DeviceSelection,
label: 'settings.devices',
onMount: getAvailableDevices,
props: getDeviceSelectionDialogProps(state),
props: getDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
propsUpdateFunction: (tabState, newProps) => {
// Ensure the device selection tab gets updated when new devices
// are found by taking the new props and only preserving the
@ -292,7 +298,7 @@ function _mapStateToProps(state, ownProps) {
};
},
styles: `settings-pane ${classes.settingsDialog} devices-pane`,
submit: submitDeviceSelectionTab
submit: newState => submitDeviceSelectionTab(newState, isDisplayedOnWelcomePage)
});
}

View File

@ -11,6 +11,8 @@ import {
import { toState } from '../base/redux';
import { getHideSelfView } from '../base/settings';
import { parseStandardURIString } from '../base/util';
import { getBreakoutRoomsConfig, isInBreakoutRoom } from '../breakout-rooms/functions';
import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me';
import { isReactionsEnabled } from '../reactions/functions.any';
@ -115,6 +117,7 @@ export function getMoreTabProps(stateful: Object | Function) {
const language = i18next.language || DEFAULT_LANGUAGE;
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const enabledNotifications = getNotificationsMap(stateful);
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
// when self view is controlled by the config we hide the settings
const { disableSelfView, disableSelfViewSettings } = state['features/base/config'];
@ -131,7 +134,8 @@ export function getMoreTabProps(stateful: Object | Function) {
showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled,
maxStageParticipants: state['features/filmstrip'].maxStageParticipants
maxStageParticipants: state['features/filmstrip'].maxStageParticipants,
stageFilmstripEnabled
};
}
@ -177,10 +181,16 @@ export function getModeratorTabProps(stateful: Object | Function) {
*/
export function shouldShowModeratorSettings(stateful: Object | Function) {
const state = toState(stateful);
return Boolean(
const inBreakoutRoom = isInBreakoutRoom(state);
const { hideModeratorSettingsTab } = getBreakoutRoomsConfig(state);
const hasModeratorRights = Boolean(
isSettingEnabled('moderator')
&& isLocalParticipantModerator(state));
&& isLocalParticipantModerator(state)
);
return inBreakoutRoom
? hasModeratorRights && !hideModeratorSettingsTab
: hasModeratorRights;
}
/**

View File

@ -52,14 +52,13 @@ export async function createVirtualBackgroundEffect(virtualBackground: Object, d
tflite = await timeout(tfliteTimeout, createTFLiteModule());
}
} catch (err) {
isWasmDisabled = true;
if (err?.message === '408') {
logger.error('Failed to download tflite model!');
dispatch(showWarningNotification({
titleKey: 'virtualBackground.backgroundEffectError'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} else {
isWasmDisabled = true;
logger.error('Looks like WebAssembly is disabled or not supported on this browser', err);
dispatch(showWarningNotification({
titleKey: 'virtualBackground.webAssemblyWarning',

View File

@ -0,0 +1,29 @@
// @flow
import { translate } from '../../../base/i18n';
import { IconDock } from '../../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
declare var APP: Object;
/**
* Implementation of a button for notifying integrators that iframe should be docked.
*/
class DockIframeButton extends AbstractButton<AbstractButtonProps, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.dock';
icon = IconDock;
label = 'toolbar.dock';
tooltip = 'toolbar.dock';
/**
* Handles clicking / pressing the button by triggering external api event.
*
* @protected
* @returns {void}
*/
_handleClick() {
APP.API.notifyIframeDockStateChanged(true);
}
}
export default translate(DockIframeButton);

View File

@ -89,6 +89,7 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton';
import DockIframeButton from './DockIframeButton';
import FullscreenButton from './FullscreenButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OverflowMenuButton from './OverflowMenuButton';
@ -96,6 +97,7 @@ import ProfileButton from './ProfileButton';
import Separator from './Separator';
import ShareDesktopButton from './ShareDesktopButton';
import ToggleCameraButton from './ToggleCameraButton';
import UndockIframeButton from './UndockIframeButton';
import VideoSettingsButton from './VideoSettingsButton';
/**
@ -786,6 +788,18 @@ class Toolbox extends Component<Props> {
group: 3
};
const dockIframe = {
key: 'dock-iframe',
Content: DockIframeButton,
group: 3
};
const undockIframe = {
key: 'undock-iframe',
Content: UndockIframeButton,
group: 3
};
const speakerStats = {
key: 'stats',
Content: SpeakerStatsButton,
@ -853,6 +867,8 @@ class Toolbox extends Component<Props> {
shareAudio,
etherpad,
virtualBackground,
dockIframe,
undockIframe,
speakerStats,
settings,
shortcuts,

View File

@ -0,0 +1,29 @@
// @flow
import { translate } from '../../../base/i18n';
import { IconUndock } from '../../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
declare var APP: Object;
/**
* Implementation of a button for notifying integrators that iframe should be undocked.
*/
class UndockIframeButton extends AbstractButton<AbstractButtonProps, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.undock';
icon = IconUndock;
label = 'toolbar.undock';
tooltip = 'toolbar.undock';
/**
* Handles clicking / pressing the button by triggering external api event.
*
* @protected
* @returns {void}
*/
_handleClick() {
APP.API.notifyIframeDockStateChanged(false);
}
}
export default translate(UndockIframeButton);

View File

@ -191,7 +191,8 @@ class WelcomePage extends AbstractWelcomePage {
<div className = 'header'>
<div className = 'welcome-page-settings'>
<SettingsButton
defaultTab = { SETTINGS_TABS.CALENDAR } />
defaultTab = { SETTINGS_TABS.CALENDAR }
isDisplayedOnWelcomePage = { true } />
{ showAdditionalToolbarContent
? <div
className = 'settings-toolbar-content'