mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-17 15:47:51 +00:00
feat(prejoin_page): Add prejoin page
This commit is contained in:
committed by
Saúl Ibarra Corretgé
parent
5b53232964
commit
a45cbf41ef
@@ -246,6 +246,16 @@ export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
|
||||
return selectedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored room name.
|
||||
*
|
||||
* @param {Object} state - The current state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRoomName(state: Object): string {
|
||||
return state['features/base/conference'].room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
|
||||
* manipulating a conference participant (e.g. Pin or select participant).
|
||||
|
||||
@@ -131,6 +131,7 @@ export default [
|
||||
'p2p',
|
||||
'pcStatsInterval',
|
||||
'preferH264',
|
||||
'prejoinPageEnabled',
|
||||
'requireDisplayName',
|
||||
'remoteVideoMenu',
|
||||
'resolution',
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
} from './actionTypes';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { updateSettings } from '../settings';
|
||||
import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
|
||||
@@ -98,10 +100,18 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceAudioTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
}
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceVideoTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
}
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
@@ -111,7 +121,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
|
||||
3
react/features/base/icons/svg/arrow-left.svg
Normal file
3
react/features/base/icons/svg/arrow-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5223 18.2701C16.9328 18.6395 16.9661 19.2718 16.5966 19.6823C16.2272 20.0928 15.5949 20.1261 15.1844 19.7567L7.31769 12.6767C6.87631 12.2794 6.87631 11.5873 7.31769 11.1901L15.1844 4.11007C15.5949 3.74061 16.2272 3.77389 16.5966 4.1844C16.9661 4.59491 16.9328 5.2272 16.5223 5.59666L9.4815 11.9334L16.5223 18.2701Z" fill="#A4B8D1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 489 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z" fill="#5E6D7A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 509 B After Width: | Height: | Size: 494 B |
3
react/features/base/icons/svg/close-x.svg
Normal file
3
react/features/base/icons/svg/close-x.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.41416 8L15.071 2.34315C15.4615 1.95262 15.4615 1.31946 15.071 0.928933C14.6805 0.538409 14.0473 0.538409 13.6568 0.928933L7.99995 6.58579L2.34309 0.928933C1.95257 0.538409 1.3194 0.538409 0.92888 0.928933C0.538355 1.31946 0.538355 1.95262 0.92888 2.34315L6.58573 8L0.92888 13.6569C0.538355 14.0474 0.538355 14.6805 0.92888 15.0711C1.3194 15.4616 1.95257 15.4616 2.34309 15.0711L7.99995 9.41421L13.6568 15.0711C14.0473 15.4616 14.6805 15.4616 15.071 15.0711C15.4615 14.6805 15.4615 14.0474 15.071 13.6569L9.41416 8Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 674 B |
@@ -4,6 +4,7 @@ export { default as IconAdd } from './add.svg';
|
||||
export { default as IconAddPeople } from './link.svg';
|
||||
export { default as IconArrowBack } from './arrow_back.svg';
|
||||
export { default as IconArrowDown } from './arrow_down.svg';
|
||||
export { default as IconArrowLeft } from './arrow-left.svg';
|
||||
export { default as IconAudioOnly } from './visibility.svg';
|
||||
export { default as IconAudioOnlyOff } from './visibility-off.svg';
|
||||
export { default as IconAudioRoute } from './volume.svg';
|
||||
@@ -16,6 +17,7 @@ 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 IconClose } from './close.svg';
|
||||
export { default as IconCloseX } from './close-x.svg';
|
||||
export { default as IconClosedCaption } from './closed_caption.svg';
|
||||
export { default as IconConnectionActive } from './gsm-bars.svg';
|
||||
export { default as IconConnectionInactive } from './ninja.svg';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Chat } from '../../../chat';
|
||||
import { Filmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
|
||||
import {
|
||||
@@ -84,6 +85,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_roomName: string,
|
||||
|
||||
/**
|
||||
* If prejoin page is visible or not.
|
||||
*/
|
||||
_showPrejoin: boolean,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
@@ -178,16 +184,22 @@ class Conference extends AbstractConference<Props, *> {
|
||||
// interfaceConfig is obsolete but legacy support is required.
|
||||
filmStripOnly: filmstripOnly
|
||||
} = interfaceConfig;
|
||||
const {
|
||||
_iAmRecorder,
|
||||
_layoutClassName,
|
||||
_showPrejoin
|
||||
} = this.props;
|
||||
const hideVideoQualityLabel
|
||||
= filmstripOnly
|
||||
|| VIDEO_QUALITY_LABEL_DISABLED
|
||||
|| this.props._iAmRecorder;
|
||||
|| _iAmRecorder;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { this.props._layoutClassName }
|
||||
className = { _layoutClassName }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { this._onShowToolbar }>
|
||||
|
||||
<Notice />
|
||||
<Subject />
|
||||
<div id = 'videospace'>
|
||||
@@ -197,11 +209,13 @@ class Conference extends AbstractConference<Props, *> {
|
||||
<Filmstrip filmstripOnly = { filmstripOnly } />
|
||||
</div>
|
||||
|
||||
{ filmstripOnly || <Toolbox /> }
|
||||
{ filmstripOnly || _showPrejoin || <Toolbox /> }
|
||||
{ filmstripOnly || <Chat /> }
|
||||
|
||||
{ this.renderNotificationsContainer() }
|
||||
|
||||
{ !filmstripOnly && _showPrejoin && <Prejoin />}
|
||||
|
||||
<CalleeInfoContainer />
|
||||
</div>
|
||||
);
|
||||
@@ -268,7 +282,8 @@ function _mapStateToProps(state) {
|
||||
...abstractMapStateToProps(state),
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
|
||||
_roomName: getConferenceNameForTitle(state)
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -298,9 +298,8 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
||||
*/
|
||||
export function isAddPeopleEnabled(state: Object): boolean {
|
||||
const { peopleSearchUrl } = state['features/base/config'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
|
||||
return !isGuest && Boolean(peopleSearchUrl);
|
||||
return !isGuest(state) && Boolean(peopleSearchUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,6 +315,16 @@ export function isDialOutEnabled(state: Object): boolean {
|
||||
&& conference && conference.isSIPCallingSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user is guest or not.
|
||||
*
|
||||
* @param {Object} state - Current state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGuest(state: Object): boolean {
|
||||
return state['features/base/jwt'].isGuest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string looks like it could be for a phone number.
|
||||
*
|
||||
|
||||
65
react/features/prejoin/actionTypes.js
Normal file
65
react/features/prejoin/actionTypes.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Action type to add a video track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_VIDEO_TRACK = 'ADD_PREJOIN_VIDEO_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to add an audio track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_AUDIO_TRACK = 'ADD_PREJOIN_AUDIO_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to add a content sharing track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_CONTENT_SHARING_TRACK
|
||||
= 'ADD_PREJOIN_CONTENT_SHARING_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to signal the start of the conference.
|
||||
*/
|
||||
export const PREJOIN_START_CONFERENCE = 'PREJOIN_START_CONFERENCE';
|
||||
|
||||
/**
|
||||
* Action type to set the status of the device.
|
||||
*/
|
||||
export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
|
||||
|
||||
/**
|
||||
* Action type to set the visiblity of the 'JoinByPhone' dialog.
|
||||
*/
|
||||
export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
|
||||
|
||||
/**
|
||||
* Action type to disable the audio while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_AUDIO_DISABLED = 'SET_PREJOIN_AUDIO_DISABLED';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the audio while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_AUDIO_MUTED = 'SET_PREJOIN_AUDIO_MUTED';
|
||||
|
||||
/**
|
||||
* Action type to set the errors while creating the prejoin streams.
|
||||
*/
|
||||
export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS';
|
||||
|
||||
/**
|
||||
* Action type to set the name of the user.
|
||||
*/
|
||||
export const SET_PREJOIN_NAME = 'SET_PREJOIN_NAME';
|
||||
|
||||
/**
|
||||
* Action type to set the visibility of the prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the video while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_VIDEO_DISABLED = 'SET_PREJOIN_VIDEO_DISABLED';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the video while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_VIDEO_MUTED = 'SET_PREJOIN_VIDEO_MUTED';
|
||||
338
react/features/prejoin/actions.js
Normal file
338
react/features/prejoin/actions.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
PREJOIN_START_CONFERENCE,
|
||||
SET_DEVICE_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PREJOIN_AUDIO_DISABLED,
|
||||
SET_PREJOIN_AUDIO_MUTED,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_NAME,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
SET_PREJOIN_VIDEO_DISABLED,
|
||||
SET_PREJOIN_VIDEO_MUTED
|
||||
} from './actionTypes';
|
||||
import { createLocalTrack } from '../base/lib-jitsi-meet';
|
||||
import { getAudioTrack, getVideoTrack } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Action used to add an audio track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinAudioTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_AUDIO_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to add a video track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinVideoTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_VIDEO_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to add a content sharing track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinContentSharingTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all the newly created tracks to store on init.
|
||||
*
|
||||
* @param {Object[]} tracks - The newly created tracks.
|
||||
* @param {Object} errors - The errors from creating the tracks.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function initPrejoin(tracks: Object[], errors: Object) {
|
||||
return async function(dispatch: Function) {
|
||||
const audioTrack = tracks.find(t => t.isAudioTrack());
|
||||
const videoTrack = tracks.find(t => t.isVideoTrack());
|
||||
|
||||
dispatch(setPrejoinDeviceErrors(errors));
|
||||
|
||||
if (audioTrack) {
|
||||
dispatch(addPrejoinAudioTrack(audioTrack));
|
||||
} else {
|
||||
dispatch(setAudioDisabled());
|
||||
}
|
||||
|
||||
if (videoTrack) {
|
||||
if (videoTrack.videoType === 'desktop') {
|
||||
dispatch(addPrejoinContentSharingTrack(videoTrack));
|
||||
dispatch(setPrejoinVideoDisabled(true));
|
||||
} else {
|
||||
dispatch(addPrejoinVideoTrack(videoTrack));
|
||||
}
|
||||
} else {
|
||||
dispatch(setPrejoinVideoDisabled(true));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the conference.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference() {
|
||||
return function(dispatch: Function) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
dispatch(startConference());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the conference without audio.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConferenceWithoutAudio() {
|
||||
return async function(dispatch: Function, getState: Function) {
|
||||
const audioTrack = getAudioTrack(getState());
|
||||
|
||||
if (audioTrack) {
|
||||
await dispatch(replacePrejoinAudioTrack(null));
|
||||
}
|
||||
dispatch(setAudioDisabled());
|
||||
dispatch(joinConference());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the existing audio track with a new one.
|
||||
*
|
||||
* @param {Object} track - The new track.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replacePrejoinAudioTrack(track: Object) {
|
||||
return async (dispatch: Function, getState: Function) => {
|
||||
const oldTrack = getAudioTrack(getState());
|
||||
|
||||
oldTrack && await oldTrack.dispose();
|
||||
dispatch(addPrejoinAudioTrack(track));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new audio track based on a device id and replaces the current one.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId of the microphone.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replaceAudioTrackById(deviceId: string) {
|
||||
return async (dispatch: Function) => {
|
||||
try {
|
||||
const track = await createLocalTrack('audio', deviceId);
|
||||
|
||||
dispatch(replacePrejoinAudioTrack(track));
|
||||
} catch (err) {
|
||||
dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
|
||||
logger.log('Error replacing audio track', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the existing video track with a new one.
|
||||
*
|
||||
* @param {Object} track - The new track.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replacePrejoinVideoTrack(track: Object) {
|
||||
return async (dispatch: Function, getState: Function) => {
|
||||
const oldTrack = getVideoTrack(getState());
|
||||
|
||||
oldTrack && await oldTrack.dispose();
|
||||
dispatch(addPrejoinVideoTrack(track));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new video track based on a device id and replaces the current one.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId of the camera.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replaceVideoTrackById(deviceId: Object) {
|
||||
return async (dispatch: Function) => {
|
||||
try {
|
||||
const track = await createLocalTrack('video', deviceId);
|
||||
|
||||
dispatch(replacePrejoinVideoTrack(track));
|
||||
} catch (err) {
|
||||
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
|
||||
logger.log('Error replacing video track', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to mark audio muted.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinAudioMuted(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_AUDIO_MUTED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark video disabled.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinVideoDisabled(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_VIDEO_DISABLED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to mark video muted.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinVideoMuted(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_VIDEO_MUTED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark audio as disabled.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setAudioDisabled() {
|
||||
return {
|
||||
type: SET_PREJOIN_AUDIO_DISABLED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the device status as OK with the corresponding text.
|
||||
*
|
||||
* @param {string} deviceStatusText - The text to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDeviceStatusOk(deviceStatusText: string) {
|
||||
return {
|
||||
type: SET_DEVICE_STATUS,
|
||||
value: {
|
||||
deviceStatusText,
|
||||
deviceStatusType: 'ok'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the device status as 'warning' with the corresponding text.
|
||||
*
|
||||
* @param {string} deviceStatusText - The text to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDeviceStatusWarning(deviceStatusText: string) {
|
||||
return {
|
||||
type: SET_DEVICE_STATUS,
|
||||
value: {
|
||||
deviceStatusText,
|
||||
deviceStatusType: 'warning'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to set the visiblitiy of the 'JoinByPhoneDialog'.
|
||||
*
|
||||
* @param {boolean} value - The value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setJoinByPhoneDialogVisiblity(value: boolean) {
|
||||
return {
|
||||
type: SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the initial errors after creating the tracks.
|
||||
*
|
||||
* @param {Object} value - The track errors.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinDeviceErrors(value: Object) {
|
||||
return {
|
||||
type: SET_PREJOIN_DEVICE_ERRORS,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the name of the guest user.
|
||||
*
|
||||
* @param {string} value - The name.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinName(value: string) {
|
||||
return {
|
||||
type: SET_PREJOIN_NAME,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the visiblity of the prejoin page.
|
||||
*
|
||||
* @param {boolean} value - The value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinPageVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_PAGE_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark the start of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function startConference() {
|
||||
return {
|
||||
type: PREJOIN_START_CONFERENCE
|
||||
};
|
||||
}
|
||||
197
react/features/prejoin/components/Prejoin.js
Normal file
197
react/features/prejoin/components/Prejoin.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
joinConference as joinConferenceAction,
|
||||
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
||||
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction,
|
||||
setPrejoinName
|
||||
} from '../actions';
|
||||
import { getRoomName } from '../../base/conference';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import ActionButton from './buttons/ActionButton';
|
||||
import {
|
||||
areJoinByPhoneButtonsVisible,
|
||||
getPrejoinName,
|
||||
isDeviceStatusVisible,
|
||||
isJoinByPhoneDialogVisible
|
||||
} from '../functions';
|
||||
import { isGuest } from '../../invite';
|
||||
import CopyMeetingUrl from './preview/CopyMeetingUrl';
|
||||
import DeviceStatus from './preview/DeviceStatus';
|
||||
import ParticipantName from './preview/ParticipantName';
|
||||
import Preview from './preview/Preview';
|
||||
import { VideoSettingsButton, AudioSettingsButton } from '../../toolbox';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
deviceStatusVisible: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling if a user is logged in or not.
|
||||
*/
|
||||
isAnonymousUser: boolean,
|
||||
|
||||
/**
|
||||
* Joins the current meeting.
|
||||
*/
|
||||
joinConference: Function,
|
||||
|
||||
/**
|
||||
* Joins the current meeting without audio.
|
||||
*/
|
||||
joinConferenceWithoutAudio: Function,
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Sets the name for the joining user.
|
||||
*/
|
||||
setName: Function,
|
||||
|
||||
/**
|
||||
* The name of the meeting that is about to be joined.
|
||||
*/
|
||||
roomName: string,
|
||||
|
||||
/**
|
||||
* Sets visibilit of the 'JoinByPhoneDialog'.
|
||||
*/
|
||||
setJoinByPhoneDialogVisiblity: Function,
|
||||
|
||||
/**
|
||||
* If 'JoinByPhoneDialog' is visible or not.
|
||||
*/
|
||||
showDialog: boolean,
|
||||
|
||||
/**
|
||||
* If join by phone buttons should be visible.
|
||||
*/
|
||||
showJoinByPhoneButtons: boolean,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is displayed before joining a meeting.
|
||||
*/
|
||||
class Prejoin extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
}
|
||||
|
||||
_showDialog: () => void;
|
||||
|
||||
/**
|
||||
* Displays the dialog for joining a meeting by phone.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_showDialog() {
|
||||
this.props.setJoinByPhoneDialogVisiblity(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
deviceStatusVisible,
|
||||
isAnonymousUser,
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
name,
|
||||
setName,
|
||||
showJoinByPhoneButtons,
|
||||
t
|
||||
} = this.props;
|
||||
const { _showDialog } = this;
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-full-page'>
|
||||
<Preview />
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<div className = 'prejoin-title'>
|
||||
{t('prejoin.joinMeeting')}
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
<ParticipantName
|
||||
isEditable = { isAnonymousUser }
|
||||
setName = { setName }
|
||||
value = { name } />
|
||||
<ActionButton
|
||||
onClick = { joinConference }
|
||||
type = 'primary'>
|
||||
{ t('calendarSync.join') }
|
||||
</ActionButton>
|
||||
{showJoinByPhoneButtons
|
||||
&& <div className = 'prejoin-text-btns'>
|
||||
<ActionButton
|
||||
onClick = { joinConferenceWithoutAudio }
|
||||
type = 'text'>
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick = { _showDialog }
|
||||
type = 'text'>
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</ActionButton>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'prejoin-preview-btn-container'>
|
||||
<AudioSettingsButton visible = { true } />
|
||||
<VideoSettingsButton visible = { true } />
|
||||
</div>
|
||||
{ deviceStatusVisible && <DeviceStatus /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state): Object {
|
||||
return {
|
||||
isAnonymousUser: isGuest(state),
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
name: getPrejoinName(state),
|
||||
roomName: getRoomName(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
showJoinByPhoneButtons: areJoinByPhoneButtonsVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
||||
joinConference: joinConferenceAction,
|
||||
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
|
||||
setName: setPrejoinName
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin));
|
||||
51
react/features/prejoin/components/buttons/ActionButton.js
Normal file
51
react/features/prejoin/components/buttons/ActionButton.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
const classNameByType = {
|
||||
primary: 'prejoin-btn--primary',
|
||||
secondary: 'prejoin-btn--secondary',
|
||||
text: 'prejoin-btn--text'
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Text css class of the button.
|
||||
*/
|
||||
className?: string,
|
||||
|
||||
/**
|
||||
* The type of th button: primary, secondary, text.
|
||||
*/
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Button used for prejoin actions: Join/Join without audio/Join by phone.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ActionButton({ children, className, type, onClick }: Props) {
|
||||
const ownClassName = `prejoin-btn ${classNameByType[type]}`;
|
||||
const cls = className ? `${className} ${ownClassName}` : ownClassName;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cls }
|
||||
onClick = { onClick }>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionButton;
|
||||
197
react/features/prejoin/components/preview/CopyMeetingUrl.js
Normal file
197
react/features/prejoin/components/preview/CopyMeetingUrl.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { getCurrentConferenceUrl } from '../../../base/connection';
|
||||
import { Icon, IconCopy, IconCheck } from '../../../base/icons';
|
||||
import logger from '../../logger';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The meeting url.
|
||||
*/
|
||||
url: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* If true it shows the 'copy link' message.
|
||||
*/
|
||||
showCopyLink: boolean,
|
||||
|
||||
/**
|
||||
* If true it shows the 'link copied' message.
|
||||
*/
|
||||
showLinkCopied: boolean,
|
||||
};
|
||||
|
||||
const COPY_TIMEOUT = 2000;
|
||||
|
||||
/**
|
||||
* Component used to copy meeting url on prejoin page.
|
||||
*/
|
||||
class CopyMeetingUrl extends Component<Props, State> {
|
||||
|
||||
textarea: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.textarea = React.createRef();
|
||||
this.state = {
|
||||
showCopyLink: false,
|
||||
showLinkCopied: false
|
||||
};
|
||||
this._copyUrl = this._copyUrl.bind(this);
|
||||
this._hideCopyLink = this._hideCopyLink.bind(this);
|
||||
this._hideLinkCopied = this._hideLinkCopied.bind(this);
|
||||
this._showCopyLink = this._showCopyLink.bind(this);
|
||||
this._showLinkCopied = this._showLinkCopied.bind(this);
|
||||
}
|
||||
|
||||
_copyUrl: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to copy the url to clipboard.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_copyUrl() {
|
||||
const textarea = this.textarea.current;
|
||||
|
||||
try {
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
textarea.blur();
|
||||
this._showLinkCopied();
|
||||
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
|
||||
} catch (err) {
|
||||
logger.error('error when copying the meeting url');
|
||||
}
|
||||
}
|
||||
|
||||
_hideLinkCopied: () => void;
|
||||
|
||||
/**
|
||||
* Hides the 'Link copied' message.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_hideLinkCopied() {
|
||||
this.setState({
|
||||
showLinkCopied: false
|
||||
});
|
||||
}
|
||||
|
||||
_hideCopyLink: () => void;
|
||||
|
||||
/**
|
||||
* Hides the 'Copy link' text.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_hideCopyLink() {
|
||||
this.setState({
|
||||
showCopyLink: false
|
||||
});
|
||||
}
|
||||
|
||||
_showCopyLink: () => void;
|
||||
|
||||
/**
|
||||
* Shows the dark 'Copy link' text on hover.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_showCopyLink() {
|
||||
this.setState({
|
||||
showCopyLink: true
|
||||
});
|
||||
}
|
||||
|
||||
_showLinkCopied: () => void;
|
||||
|
||||
/**
|
||||
* Shows the green 'Link copied' message.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_showLinkCopied() {
|
||||
this.setState({
|
||||
showLinkCopied: true,
|
||||
showCopyLink: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { showCopyLink, showLinkCopied } = this.state;
|
||||
const { url, t } = this.props;
|
||||
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
|
||||
const src = showLinkCopied ? IconCheck : IconCopy;
|
||||
const iconCls = showCopyLink || showCopyLink ? 'prejoin-copy-icon--white' : 'prejoin-copy-icon--light';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'prejoin-copy-meeting'
|
||||
onMouseEnter = { _showCopyLink }
|
||||
onMouseLeave = { _hideCopyLink }>
|
||||
<div className = 'prejoin-copy-url'>{url}</div>
|
||||
{showCopyLink && <div
|
||||
className = 'prejoin-copy-badge prejoin-copy-badge--hover'
|
||||
onClick = { _copyUrl }>
|
||||
{t('prejoin.copyAndShare')}
|
||||
</div>}
|
||||
{showLinkCopied && <div
|
||||
className = 'prejoin-copy-badge prejoin-copy-badge--done'>
|
||||
{t('prejoin.linkCopied')}
|
||||
</div>}
|
||||
<Icon
|
||||
className = { `prejoin-copy-icon ${iconCls}` }
|
||||
size = { 24 }
|
||||
src = { src } />
|
||||
<textarea
|
||||
className = 'prejoin-copy-textarea'
|
||||
readOnly = { true }
|
||||
ref = { this.textarea }
|
||||
tabIndex = '-1'
|
||||
value = { 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) {
|
||||
return {
|
||||
url: getCurrentConferenceUrl(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(translate(CopyMeetingUrl));
|
||||
83
react/features/prejoin/components/preview/DeviceStatus.js
Normal file
83
react/features/prejoin/components/preview/DeviceStatus.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconCheck, IconExclamation } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
getDeviceStatusType,
|
||||
getDeviceStatusText,
|
||||
getRawError
|
||||
} from '../../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The text to be displayed in relation to the status of the audio/video devices.
|
||||
*/
|
||||
deviceStatusText: string,
|
||||
|
||||
/**
|
||||
* The type of status for current devices, controlling the background color of the text.
|
||||
* Can be `ok` or `warning`.
|
||||
*/
|
||||
deviceStatusType: string,
|
||||
|
||||
/**
|
||||
* The error coming from device configuration.
|
||||
*/
|
||||
rawError: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
const iconMap = {
|
||||
warning: {
|
||||
src: IconExclamation,
|
||||
className: 'prejoin-preview-status--warning'
|
||||
},
|
||||
ok: {
|
||||
src: IconCheck,
|
||||
className: 'prejoin-preview-status--ok'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip showing the current status of the devices.
|
||||
* User is informed if there are missing or malfunctioning devices.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props) {
|
||||
const { src, className } = iconMap[deviceStatusType];
|
||||
|
||||
return (
|
||||
<div className = { `prejoin-preview-status ${className}` }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-icon'
|
||||
size = { 16 }
|
||||
src = { src } />
|
||||
<span className = 'prejoin-preview-error-desc'>{t(deviceStatusText)}</span>
|
||||
<span>{rawError}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{ deviceStatusText: string, deviceStatusText: string }}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
deviceStatusText: getDeviceStatusText(state),
|
||||
deviceStatusType: getDeviceStatusType(state),
|
||||
rawError: getRawError(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(DeviceStatus));
|
||||
80
react/features/prejoin/components/preview/ParticipantName.js
Normal file
80
react/features/prejoin/components/preview/ParticipantName.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the name is ediable or not.
|
||||
*/
|
||||
isEditable: boolean,
|
||||
|
||||
/**
|
||||
* Sets the name for the joining user.
|
||||
*/
|
||||
setName: Function,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The text to be displayed.
|
||||
*/
|
||||
value: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Participant name - can be an editable input or just the text name.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class ParticipantName extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ParticipantName} instance.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onNameChange = this._onNameChange.bind(this);
|
||||
}
|
||||
|
||||
_onNameChange: () => void;
|
||||
|
||||
/**
|
||||
* Handler used for changing the guest user name.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_onNameChange({ target: { value } }) {
|
||||
this.props.setName(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { value, isEditable, t } = this.props;
|
||||
|
||||
return isEditable ? (
|
||||
<input
|
||||
className = 'prejoin-preview-name prejoin-preview-name--editable'
|
||||
onChange = { this._onNameChange }
|
||||
placeholder = { t('dialog.enterDisplayName') }
|
||||
value = { value } />
|
||||
)
|
||||
: <div className = 'prejoin-preview-name'>{value}</div>
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default translate(ParticipantName);
|
||||
75
react/features/prejoin/components/preview/Preview.js
Normal file
75
react/features/prejoin/components/preview/Preview.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { Video } from '../../../base/media';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getActiveVideoTrack, getPrejoinName, isPrejoinVideoMuted } from '../../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
showCameraPreview: boolean,
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
videoTrack: ?Object,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component showing the video preview and device status.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Preview(props: Props) {
|
||||
const {
|
||||
name,
|
||||
showCameraPreview,
|
||||
videoTrack
|
||||
} = props;
|
||||
|
||||
if (showCameraPreview && videoTrack) {
|
||||
return (
|
||||
<div className = 'prejoin-preview'>
|
||||
<div className = 'prejoin-preview-overlay' />
|
||||
<Video
|
||||
className = 'flipVideoX prejoin-preview-video'
|
||||
videoTrack = {{ jitsiTrack: videoTrack }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-preview prejoin-preview--no-video'>
|
||||
<Avatar
|
||||
className = 'prejoin-preview-avatar'
|
||||
displayName = { name }
|
||||
size = { 200 } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
name: getPrejoinName(state),
|
||||
videoTrack: getActiveVideoTrack(state),
|
||||
showCameraPreview: !isPrejoinVideoMuted(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Preview);
|
||||
228
react/features/prejoin/functions.js
Normal file
228
react/features/prejoin/functions.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// @flow
|
||||
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a track.
|
||||
*
|
||||
* @param {Object} track - The track to be configured.
|
||||
* @param {boolean} shouldMute - If it should mute or not.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function applyMuteOptionsToTrack(track, shouldMute) {
|
||||
if (track.isMuted() === shouldMute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldMute) {
|
||||
return track.mute();
|
||||
}
|
||||
|
||||
return track.unmute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for the visibility of the 'join by phone' buttons.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function areJoinByPhoneButtonsVisible(state: Object): boolean {
|
||||
return state['features/prejoin'].buttonsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the device status strip is visible or not.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDeviceStatusVisible(state: Object): boolean {
|
||||
return !((isAudioDisabled(state) && isPrejoinVideoDisabled(state))
|
||||
|| (isPrejoinAudioMuted(state) && isPrejoinVideoMuted(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the active video/content sharing track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getActiveVideoTrack(state: Object): Object {
|
||||
const track = getVideoTrack(state) || getContentSharingTrack(state);
|
||||
|
||||
if (track && track.isActive()) {
|
||||
return track;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list with all the prejoin tracks configured according to
|
||||
* user's preferences.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
export async function getAllPrejoinConfiguredTracks(state: Object): Promise<Object[]> {
|
||||
const tracks = [];
|
||||
const audioTrack = getAudioTrack(state);
|
||||
const videoTrack = getVideoTrack(state);
|
||||
const csTrack = getContentSharingTrack(state);
|
||||
|
||||
if (csTrack) {
|
||||
tracks.push(csTrack);
|
||||
} else if (videoTrack) {
|
||||
await applyMuteOptionsToTrack(videoTrack, isPrejoinVideoMuted(state));
|
||||
tracks.push(videoTrack);
|
||||
}
|
||||
|
||||
if (audioTrack) {
|
||||
await applyMuteOptionsToTrack(audioTrack, isPrejoinAudioMuted(state));
|
||||
isPrejoinAudioMuted(state) && audioTrack.mute();
|
||||
tracks.push(audioTrack);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin audio track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getAudioTrack(state: Object): Object {
|
||||
return state['features/prejoin'].audioTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin content sharing track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getContentSharingTrack(state: Object): Object {
|
||||
return state['features/prejoin'].contentSharingTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text for the prejoin status bar.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDeviceStatusText(state: Object): string {
|
||||
return state['features/prejoin'].deviceStatusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the prejoin status bar: 'ok'|'warning'.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDeviceStatusType(state: Object): string {
|
||||
return state['features/prejoin'].deviceStatusType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin video track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getVideoTrack(state: Object): Object {
|
||||
return state['features/prejoin'].videoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the mute status of the prejoin audio.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinAudioMuted(state: Object): boolean {
|
||||
return state['features/prejoin'].audioMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the name that the user filled while configuring.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getPrejoinName(state: Object): string {
|
||||
return state['features/prejoin'].name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the mute status of the prejoin video.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinVideoMuted(state: Object): boolean {
|
||||
return state['features/prejoin'].videoMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the error if any while creating streams.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRawError(state: Object): string {
|
||||
return state['features/prejoin'].rawError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting state of the prejoin audio.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAudioDisabled(state: Object): Object {
|
||||
return state['features/prejoin'].audioDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting state of the prejoin video.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinVideoDisabled(state: Object): Object {
|
||||
return state['features/prejoin'].videoDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the visiblity state for the 'JoinByPhoneDialog'.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJoinByPhoneDialogVisible(state: Object): boolean {
|
||||
return state['features/prejoin'].showJoinByPhoneDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin page is enabled and no flag
|
||||
* to bypass showing the page is present.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinPageEnabled(state: Object): boolean {
|
||||
return state['features/base/config'].prejoinPageEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin page is visible & active.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinPageVisible(state: Object): boolean {
|
||||
return isPrejoinPageEnabled(state) && state['features/prejoin'].showPrejoin;
|
||||
}
|
||||
7
react/features/prejoin/index.js
Normal file
7
react/features/prejoin/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
export { default as Prejoin } from './components/Prejoin';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
5
react/features/prejoin/logger.js
Normal file
5
react/features/prejoin/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/prejoin');
|
||||
95
react/features/prejoin/middleware.js
Normal file
95
react/features/prejoin/middleware.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
PREJOIN_START_CONFERENCE
|
||||
} from './actionTypes';
|
||||
import { setPrejoinAudioMuted, setPrejoinVideoMuted } from './actions';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
|
||||
import { participantUpdated, getLocalParticipant } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { getAllPrejoinConfiguredTracks, getPrejoinName } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* The redux middleware for {@link PrejoinPage}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
switch (action.type) {
|
||||
case ADD_PREJOIN_AUDIO_TRACK: {
|
||||
const { value: audioTrack } = action;
|
||||
|
||||
if (audioTrack) {
|
||||
store.dispatch(
|
||||
updateSettings({
|
||||
micDeviceId: audioTrack.getDeviceId()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_VIDEO_TRACK: {
|
||||
const { value: videoTrack } = action;
|
||||
|
||||
if (videoTrack) {
|
||||
store.dispatch(
|
||||
updateSettings({
|
||||
cameraDeviceId: videoTrack.getDeviceId()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PREJOIN_START_CONFERENCE: {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
_syncParticipantName(dispatch, getState);
|
||||
const tracks = await getAllPrejoinConfiguredTracks(getState());
|
||||
|
||||
APP.conference.prejoinStart(tracks);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_MUTED: {
|
||||
store.dispatch(setPrejoinAudioMuted(Boolean(action.muted)));
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED: {
|
||||
store.dispatch(setPrejoinVideoMuted(Boolean(action.muted)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the local participant name if one is present.
|
||||
*
|
||||
* @param {Function} dispatch - The redux dispatch function.
|
||||
* @param {Function} getState - Gets the current state.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function _syncParticipantName(dispatch, getState) {
|
||||
const state = getState();
|
||||
const name = getPrejoinName(state);
|
||||
|
||||
name && dispatch(
|
||||
participantUpdated({
|
||||
...getLocalParticipant(state),
|
||||
name
|
||||
}),
|
||||
);
|
||||
}
|
||||
168
react/features/prejoin/reducer.js
Normal file
168
react/features/prejoin/reducer.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
SET_DEVICE_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PREJOIN_AUDIO_DISABLED,
|
||||
SET_PREJOIN_AUDIO_MUTED,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_NAME,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
SET_PREJOIN_VIDEO_DISABLED,
|
||||
SET_PREJOIN_VIDEO_MUTED
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
audioDisabled: false,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
videoDisabled: false,
|
||||
deviceStatusText: 'prejoin.configuringDevices',
|
||||
deviceStatusType: 'ok',
|
||||
showPrejoin: true,
|
||||
showJoinByPhoneDialog: false,
|
||||
videoTrack: null,
|
||||
audioTrack: null,
|
||||
contentSharingTrack: null,
|
||||
rawError: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the prejoin state
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/prejoin', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ADD_PREJOIN_AUDIO_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
audioTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_CONTENT_SHARING_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
contentSharingTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_VIDEO_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
videoTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_NAME: {
|
||||
return {
|
||||
...state,
|
||||
name: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_PAGE_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
showPrejoin: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_VIDEO_DISABLED: {
|
||||
return {
|
||||
...state,
|
||||
videoDisabled: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_VIDEO_MUTED:
|
||||
return {
|
||||
...state,
|
||||
videoMuted: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_AUDIO_MUTED:
|
||||
return {
|
||||
...state,
|
||||
audioMuted: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_DEVICE_ERRORS: {
|
||||
const status = getStatusFromErrors(action.value);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...status
|
||||
};
|
||||
}
|
||||
|
||||
case SET_DEVICE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
deviceStatusText: action.text,
|
||||
deviceStatusType: action.type
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_AUDIO_DISABLED: {
|
||||
return {
|
||||
...state,
|
||||
audioDisabled: true
|
||||
};
|
||||
}
|
||||
|
||||
case SET_JOIN_BY_PHONE_DIALOG_VISIBLITY: {
|
||||
return {
|
||||
...state,
|
||||
showJoinByPhoneDialog: action.value
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a suitable error object based on the track errors.
|
||||
*
|
||||
* @param {Object} errors - The errors got while creating local tracks.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getStatusFromErrors(errors) {
|
||||
const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
|
||||
|
||||
if (audioAndVideoError) {
|
||||
if (audioOnlyError) {
|
||||
if (videoOnlyError) {
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.audioAndVideoError',
|
||||
rawError: audioAndVideoError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.audioOnlyError',
|
||||
rawError: audioOnlyError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.videoOnlyError',
|
||||
rawError: audioAndVideoError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'ok',
|
||||
deviceStatusText: 'prejoin.lookGood',
|
||||
rawError: ''
|
||||
};
|
||||
}
|
||||
@@ -211,8 +211,10 @@ class VideoSettingsContent extends Component<Props, State> {
|
||||
const { trackData } = this.state;
|
||||
|
||||
return (
|
||||
<div className = 'video-preview'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
<div className = 'video-preview-container'>
|
||||
<div className = 'video-preview'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ import { connect } from '../../base/redux';
|
||||
import { AbstractAudioMuteButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
import { isLocalTrackMuted } from '../../base/tracks';
|
||||
import {
|
||||
isPrejoinAudioMuted,
|
||||
isAudioDisabled,
|
||||
isPrejoinPageVisible
|
||||
} from '../../prejoin';
|
||||
import { muteLocal } from '../../remote-video-menu/actions';
|
||||
|
||||
declare var APP: Object;
|
||||
@@ -144,15 +149,27 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioMuted: boolean
|
||||
* _audioMuted: boolean,
|
||||
* _disabled: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const tracks = state['features/base/tracks'];
|
||||
let _audioMuted;
|
||||
let _disabled;
|
||||
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
_audioMuted = isPrejoinAudioMuted(state);
|
||||
_disabled = state['features/base/config'].startSilent;
|
||||
} else {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
_audioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
_disabled = state['features/base/config'].startSilent || isAudioDisabled(state);
|
||||
}
|
||||
|
||||
return {
|
||||
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
|
||||
_disabled: state['features/base/config'].startSilent
|
||||
_audioMuted,
|
||||
_disabled
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import { connect } from '../../base/redux';
|
||||
import { AbstractVideoMuteButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
|
||||
import {
|
||||
isPrejoinPageVisible,
|
||||
isPrejoinVideoDisabled,
|
||||
isPrejoinVideoMuted
|
||||
} from '../../prejoin';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
declare var APP: Object;
|
||||
@@ -41,6 +46,11 @@ type Props = AbstractButtonProps & {
|
||||
*/
|
||||
_videoMuted: boolean,
|
||||
|
||||
/**
|
||||
* Whether video button is disabled or not.
|
||||
*/
|
||||
_videoDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
@@ -96,6 +106,17 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
|
||||
|| APP.keyboardshortcut.unregisterShortcut('V');
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if video is currently disabled or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return this.props._videoDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if video is currently muted ot nor.
|
||||
*
|
||||
@@ -170,11 +191,19 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
|
||||
function _mapStateToProps(state): Object {
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
let _videoMuted = isLocalVideoTrackMuted(tracks);
|
||||
let _videoDisabled = false;
|
||||
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
_videoMuted = isPrejoinVideoMuted(state);
|
||||
_videoDisabled = isPrejoinVideoDisabled(state);
|
||||
}
|
||||
|
||||
return {
|
||||
_audioOnly: Boolean(audioOnly),
|
||||
_videoDisabled,
|
||||
_videoMediaType: getLocalVideoType(tracks),
|
||||
_videoMuted: isLocalVideoTrackMuted(tracks)
|
||||
_videoMuted
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { isAudioSettingsButtonDisabled } from '../../functions';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
@@ -25,9 +25,9 @@ type Props = {
|
||||
permissionPromptVisibility: boolean,
|
||||
|
||||
/**
|
||||
* If the user has audio input or audio output devices.
|
||||
* If the button should be disabled.
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
@@ -49,6 +49,8 @@ type State = {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class AudioSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioSettingsButton} instance.
|
||||
*
|
||||
@@ -58,6 +60,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
@@ -73,7 +76,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
'audio',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
@@ -98,14 +101,23 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
const { isDisabled, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions || isDisabled;
|
||||
|
||||
return visible ? (
|
||||
<AudioSettingsPopup>
|
||||
@@ -128,9 +140,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices:
|
||||
hasAvailableDevices(state, 'audioInput')
|
||||
|| hasAvailableDevices(state, 'audioOutput'),
|
||||
isDisabled: isAudioSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isVideoSettingsButtonDisabled } from '../../functions';
|
||||
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
|
||||
import VideoMuteButton from '../VideoMuteButton';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
@@ -25,9 +24,9 @@ type Props = {
|
||||
permissionPromptVisibility: boolean,
|
||||
|
||||
/**
|
||||
* If the user has any video devices.
|
||||
* If the button should be disabled
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
@@ -49,6 +48,8 @@ type State = {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class VideoSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoSettingsButton} instance.
|
||||
*
|
||||
@@ -58,6 +59,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
@@ -73,7 +75,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
'video',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
@@ -98,14 +100,23 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onVideoOptionsClick, visible } = this.props;
|
||||
const iconDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
const { isDisabled, onVideoOptionsClick, visible } = this.props;
|
||||
const iconDisabled = !this.state.hasPermissions || isDisabled;
|
||||
|
||||
return visible ? (
|
||||
<VideoSettingsPopup>
|
||||
@@ -128,7 +139,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices: hasAvailableDevices(state, 'videoInput'),
|
||||
isDisabled: isVideoSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as AudioSettingsButton } from './AudioSettingsButton';
|
||||
export { default as VideoSettingsButton } from './VideoSettingsButton';
|
||||
export { default as ToolbarButton } from './ToolbarButton';
|
||||
export { default as Toolbox } from './Toolbox';
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
isAudioDisabled,
|
||||
isPrejoinPageVisible,
|
||||
isPrejoinVideoDisabled
|
||||
} from '../prejoin';
|
||||
import { hasAvailableDevices } from '../base/devices';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
@@ -45,3 +52,32 @@ export function isToolboxVisible(state: Object) {
|
||||
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
|
||||
|| audioSettingsVisible || videoSettingsVisible));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio settings button is disabled or not.
|
||||
*
|
||||
* @param {string} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAudioSettingsButtonDisabled(state: Object) {
|
||||
const devicesMissing = !hasAvailableDevices(state, 'audioInput')
|
||||
&& !hasAvailableDevices(state, 'audioOutput');
|
||||
|
||||
return isPrejoinPageVisible(state)
|
||||
? devicesMissing || isAudioDisabled(state)
|
||||
: devicesMissing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the video settings button is disabled or not.
|
||||
*
|
||||
* @param {string} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoSettingsButtonDisabled(state: Object) {
|
||||
const devicesMissing = !hasAvailableDevices(state, 'videoInput');
|
||||
|
||||
return isPrejoinPageVisible(state)
|
||||
? devicesMissing || isPrejoinVideoDisabled(state)
|
||||
: devicesMissing;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user