This commit is contained in:
242
react/features/base/premeeting/components/web/ActionButton.tsx
Normal file
242
react/features/base/premeeting/components/web/ActionButton.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { IconArrowDown } from '../../../icons/svg';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Icon to display in the options section.
|
||||
*/
|
||||
OptionsIcon?: Function;
|
||||
|
||||
/**
|
||||
* The Label of the child element.
|
||||
*/
|
||||
ariaDropDownLabel?: string;
|
||||
|
||||
/**
|
||||
* The Label of the current element.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
|
||||
/**
|
||||
* To give a aria-pressed to the icon.
|
||||
*/
|
||||
ariaPressed?: boolean;
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* Text css class of the button.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* If the button is disabled or not.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* If the button has options.
|
||||
*/
|
||||
hasOptions?: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Click handler for options.
|
||||
*/
|
||||
onOptionsClick?: (e?: React.KeyboardEvent | React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* To give a role to the icon.
|
||||
*/
|
||||
role?: string;
|
||||
|
||||
/**
|
||||
* To navigate with the keyboard.
|
||||
*/
|
||||
tabIndex?: number;
|
||||
|
||||
/**
|
||||
* TestId of the button. Can be used to locate element when testing UI.
|
||||
*/
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* The type of th button: primary, secondary, text.
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
actionButton: {
|
||||
...theme.typography.bodyLongBold,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxSizing: 'border-box',
|
||||
color: theme.palette.text01,
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
marginBottom: '16px',
|
||||
padding: '7px 16px',
|
||||
position: 'relative' as const,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
border: 0,
|
||||
|
||||
'&.primary': {
|
||||
background: theme.palette.action01,
|
||||
color: theme.palette.text01,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action01Hover
|
||||
}
|
||||
},
|
||||
|
||||
'&.secondary': {
|
||||
background: theme.palette.action02,
|
||||
color: theme.palette.text04,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action02Hover
|
||||
}
|
||||
},
|
||||
|
||||
'&.text': {
|
||||
width: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
margin: '0',
|
||||
padding: '0'
|
||||
},
|
||||
|
||||
'&.disabled': {
|
||||
background: theme.palette.disabled01,
|
||||
border: '1px solid #5E6D7A',
|
||||
color: '#AFB6BC',
|
||||
cursor: 'initial',
|
||||
|
||||
'.icon': {
|
||||
'& > svg': {
|
||||
fill: '#AFB6BC'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
[theme.breakpoints.down(400)]: {
|
||||
fontSize: '1rem',
|
||||
marginBottom: 8,
|
||||
padding: '11px 16px'
|
||||
}
|
||||
},
|
||||
options: {
|
||||
borderRadius: Number(theme.shape.borderRadius) / 2,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute' as const,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 36,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: '#0262B6'
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Button used for pre meeting actions.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ActionButton({
|
||||
children,
|
||||
className = '',
|
||||
disabled,
|
||||
hasOptions,
|
||||
OptionsIcon = IconArrowDown,
|
||||
testId,
|
||||
type = 'primary',
|
||||
onClick,
|
||||
onOptionsClick,
|
||||
tabIndex,
|
||||
role,
|
||||
ariaPressed,
|
||||
ariaLabel,
|
||||
ariaDropDownLabel
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
}, [ onClick, disabled ]);
|
||||
|
||||
const onOptionsKeyPressHandler = useCallback(e => {
|
||||
if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOptionsClick(e);
|
||||
}
|
||||
}, [ onOptionsClick, disabled ]);
|
||||
|
||||
const containerClasses = cx(
|
||||
classes.actionButton,
|
||||
className && className,
|
||||
type,
|
||||
disabled && 'disabled'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-disabled = { disabled }
|
||||
aria-label = { ariaLabel }
|
||||
className = { containerClasses }
|
||||
data-testid = { testId ? testId : undefined }
|
||||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyPress = { onKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 } >
|
||||
{children}
|
||||
{ hasOptions
|
||||
&& <div
|
||||
aria-disabled = { disabled }
|
||||
aria-haspopup = 'true'
|
||||
aria-label = { ariaDropDownLabel }
|
||||
aria-pressed = { ariaPressed }
|
||||
className = { classes.options }
|
||||
data-testid = 'prejoin.joinOptions'
|
||||
onClick = { disabled ? undefined : onOptionsClick }
|
||||
onKeyPress = { onOptionsKeyPressHandler }
|
||||
role = { role }
|
||||
tabIndex = { tabIndex }>
|
||||
<Icon
|
||||
className = 'icon'
|
||||
size = { 24 }
|
||||
src = { OptionsIcon } />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { IconArrowDown, IconCloseCircle, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
|
||||
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
|
||||
import Spinner from '../../../ui/components/web/Spinner';
|
||||
import { runPreCallTest } from '../../actions.web';
|
||||
import { CONNECTION_TYPE } from '../../constants';
|
||||
import { getConnectionData } from '../../functions';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
connectionStatus: {
|
||||
color: '#fff',
|
||||
...theme.typography.bodyShortRegular,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
|
||||
[theme.breakpoints.down(400)]: {
|
||||
margin: 0,
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
margin: `${theme.spacing(4)} auto`,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
width: PREJOIN_DEFAULT_CONTENT_WIDTH
|
||||
},
|
||||
|
||||
// mobile phone landscape
|
||||
'@media (max-height: 420px)': {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
'& .con-status-header': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
padding: '12px 16px',
|
||||
borderRadius: theme.shape.borderRadius
|
||||
},
|
||||
|
||||
'& .con-status-circle': {
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
padding: theme.spacing(1),
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
|
||||
'& .con-status--good': {
|
||||
background: '#31B76A'
|
||||
},
|
||||
|
||||
'& .con-status--failed': {
|
||||
background: '#E12D2D'
|
||||
},
|
||||
|
||||
'& .con-status--poor': {
|
||||
background: '#E12D2D'
|
||||
},
|
||||
|
||||
'& .con-status--non-optimal': {
|
||||
background: '#E39623'
|
||||
},
|
||||
|
||||
'& .con-status-arrow': {
|
||||
marginLeft: 'auto',
|
||||
transition: 'background-color 0.16s ease-out'
|
||||
},
|
||||
|
||||
'& .con-status-arrow--up': {
|
||||
transform: 'rotate(180deg)'
|
||||
},
|
||||
|
||||
'& .con-status-arrow > svg': {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
'& .con-status-arrow:hover': {
|
||||
backgroundColor: 'rgba(1, 1, 1, 0.1)'
|
||||
},
|
||||
|
||||
'& .con-status-text': {
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
'& .con-status-details': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderTop: '1px solid #5E6D7A',
|
||||
padding: theme.spacing(3),
|
||||
transition: 'opacity 0.16s ease-out'
|
||||
},
|
||||
|
||||
'& .con-status-details-visible': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .con-status-details-hidden': {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const CONNECTION_TYPE_MAP: {
|
||||
[key: string]: {
|
||||
connectionClass: string;
|
||||
connectionText: string;
|
||||
icon: Function;
|
||||
};
|
||||
} = {
|
||||
[CONNECTION_TYPE.FAILED]: {
|
||||
connectionClass: 'con-status--failed',
|
||||
icon: IconCloseCircle,
|
||||
connectionText: 'prejoin.connection.failed'
|
||||
},
|
||||
[CONNECTION_TYPE.POOR]: {
|
||||
connectionClass: 'con-status--poor',
|
||||
icon: IconWifi1Bar,
|
||||
connectionText: 'prejoin.connection.poor'
|
||||
},
|
||||
[CONNECTION_TYPE.NON_OPTIMAL]: {
|
||||
connectionClass: 'con-status--non-optimal',
|
||||
icon: IconWifi2Bars,
|
||||
connectionText: 'prejoin.connection.nonOptimal'
|
||||
},
|
||||
[CONNECTION_TYPE.GOOD]: {
|
||||
connectionClass: 'con-status--good',
|
||||
icon: IconWifi3Bars,
|
||||
connectionText: 'prejoin.connection.good'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Component displaying information related to the connection & audio/video quality.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const ConnectionStatus = () => {
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { connectionType, connectionDetails } = useSelector(getConnectionData);
|
||||
const [ showDetails, toggleDetails ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(runPreCallTest());
|
||||
}, []);
|
||||
|
||||
const arrowClassName = showDetails
|
||||
? 'con-status-arrow con-status-arrow--up'
|
||||
: 'con-status-arrow';
|
||||
const detailsText = connectionDetails?.map(d => t(d)).join(' ');
|
||||
const detailsClassName = showDetails
|
||||
? 'con-status-details-visible'
|
||||
: 'con-status-details-hidden';
|
||||
|
||||
const onToggleDetails = useCallback(e => {
|
||||
e.preventDefault();
|
||||
toggleDetails(!showDetails);
|
||||
}, [ showDetails, toggleDetails ]);
|
||||
|
||||
const onKeyPressToggleDetails = useCallback(e => {
|
||||
if (toggleDetails && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
toggleDetails(!showDetails);
|
||||
}
|
||||
}, [ showDetails, toggleDetails ]);
|
||||
|
||||
if (connectionType === CONNECTION_TYPE.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionType === CONNECTION_TYPE.RUNNING) {
|
||||
return (
|
||||
<div className = { classes.connectionStatus }>
|
||||
<div
|
||||
aria-level = { 1 }
|
||||
className = 'con-status-header'
|
||||
role = 'heading'>
|
||||
<div className = 'con-status-circle'>
|
||||
<Spinner
|
||||
color = { 'green' }
|
||||
size = 'medium' />
|
||||
</div>
|
||||
<span
|
||||
className = 'con-status-text'
|
||||
id = 'connection-status-description'>{t('prejoin.connection.running')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
|
||||
|
||||
return (
|
||||
<div className = { classes.connectionStatus }>
|
||||
<div
|
||||
aria-level = { 1 }
|
||||
className = 'con-status-header'
|
||||
role = 'heading'>
|
||||
<div className = { `con-status-circle ${connectionClass}` }>
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { icon } />
|
||||
</div>
|
||||
<span
|
||||
aria-hidden = { !showDetails }
|
||||
className = 'con-status-text'
|
||||
id = 'connection-status-description'>{t(connectionText)}</span>
|
||||
<Icon
|
||||
ariaDescribedBy = 'connection-status-description'
|
||||
ariaPressed = { showDetails }
|
||||
className = { arrowClassName }
|
||||
onClick = { onToggleDetails }
|
||||
onKeyPress = { onKeyPressToggleDetails }
|
||||
role = 'button'
|
||||
size = { 24 }
|
||||
src = { IconArrowDown }
|
||||
tabIndex = { 0 } />
|
||||
</div>
|
||||
<div
|
||||
aria-level = { 2 }
|
||||
className = { `con-status-details ${detailsClassName}` }
|
||||
role = 'heading'>
|
||||
{detailsText}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatus;
|
||||
@@ -0,0 +1,294 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import DeviceStatus from '../../../../prejoin/components/web/preview/DeviceStatus';
|
||||
import { isRoomNameEnabled } from '../../../../prejoin/functions.web';
|
||||
import Toolbox from '../../../../toolbox/components/web/Toolbox';
|
||||
import { isButtonEnabled } from '../../../../toolbox/functions.web';
|
||||
import { getConferenceName } from '../../../conference/functions';
|
||||
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
|
||||
import Tooltip from '../../../tooltip/components/Tooltip';
|
||||
import { isPreCallTestEnabled } from '../../functions';
|
||||
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
import Preview from './Preview';
|
||||
import RecordingWarning from './RecordingWarning';
|
||||
import UnsafeRoomWarning from './UnsafeRoomWarning';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The list of toolbar buttons to render.
|
||||
*/
|
||||
_buttons: Array<string>;
|
||||
|
||||
/**
|
||||
* Determine if pre call test is enabled.
|
||||
*/
|
||||
_isPreCallTestEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* The branding background of the premeeting screen(lobby/prejoin).
|
||||
*/
|
||||
_premeetingBackground: string;
|
||||
|
||||
/**
|
||||
* The name of the meeting that is about to be joined.
|
||||
*/
|
||||
_roomName: string;
|
||||
|
||||
/**
|
||||
* Children component(s) to be rendered on the screen.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
|
||||
/**
|
||||
* Additional CSS class names to set on the icon container.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The name of the participant.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether the copy url button should be shown.
|
||||
*/
|
||||
showCopyUrlButton?: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the device status should be shown.
|
||||
*/
|
||||
showDeviceStatus: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether to display the recording warning.
|
||||
*/
|
||||
showRecordingWarning?: boolean;
|
||||
|
||||
/**
|
||||
* If should show unsafe room warning when joining.
|
||||
*/
|
||||
showUnsafeRoomWarning?: boolean;
|
||||
|
||||
/**
|
||||
* The 'Skip prejoin' button to be rendered (if any).
|
||||
*/
|
||||
skipPrejoinButton?: ReactNode;
|
||||
|
||||
/**
|
||||
* Whether it's used in the 3rdParty prejoin screen or not.
|
||||
*/
|
||||
thirdParty?: boolean;
|
||||
|
||||
/**
|
||||
* Title of the screen.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* True if the preview overlay should be muted, false otherwise.
|
||||
*/
|
||||
videoMuted?: boolean;
|
||||
|
||||
/**
|
||||
* The video track to render as preview (if omitted, the default local track will be rendered).
|
||||
*/
|
||||
videoTrack?: Object;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
inset: '0 0 0 0',
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
flexDirection: 'column-reverse'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
boxSizing: 'border-box',
|
||||
margin: '0 48px',
|
||||
padding: '24px 0 16px',
|
||||
position: 'relative',
|
||||
width: '300px',
|
||||
height: '100%',
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
height: 'auto',
|
||||
margin: '0 auto'
|
||||
},
|
||||
|
||||
// mobile phone landscape
|
||||
'@media (max-width: 420px)': {
|
||||
padding: '16px 16px 0 16px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'@media (max-width: 400px)': {
|
||||
padding: '16px'
|
||||
}
|
||||
},
|
||||
contentControls: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
margin: 'auto',
|
||||
width: '100%'
|
||||
},
|
||||
title: {
|
||||
...theme.typography.heading4,
|
||||
color: `${theme.palette.text01}!important`,
|
||||
marginBottom: theme.spacing(3),
|
||||
textAlign: 'center',
|
||||
|
||||
'@media (max-width: 400px)': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
roomNameContainer: {
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
roomName: {
|
||||
...theme.typography.heading5,
|
||||
color: theme.palette.text01,
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const PreMeetingScreen = ({
|
||||
_buttons,
|
||||
_isPreCallTestEnabled,
|
||||
_premeetingBackground,
|
||||
_roomName,
|
||||
children,
|
||||
className,
|
||||
showDeviceStatus,
|
||||
showRecordingWarning,
|
||||
showUnsafeRoomWarning,
|
||||
skipPrejoinButton,
|
||||
title,
|
||||
videoMuted,
|
||||
videoTrack
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const style = _premeetingBackground ? {
|
||||
background: _premeetingBackground,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover'
|
||||
} : {};
|
||||
|
||||
const roomNameRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [ isOverflowing, setIsOverflowing ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (roomNameRef.current) {
|
||||
const element = roomNameRef.current;
|
||||
const elementStyles = window.getComputedStyle(element);
|
||||
const elementWidth = Math.floor(parseFloat(elementStyles.width));
|
||||
|
||||
setIsOverflowing(element.scrollWidth > elementWidth + 1);
|
||||
}
|
||||
}, [ _roomName ]);
|
||||
|
||||
return (
|
||||
<div className = { clsx('premeeting-screen', classes.container, className) }>
|
||||
<div style = { style }>
|
||||
<div className = { classes.content }>
|
||||
{_isPreCallTestEnabled && <ConnectionStatus />}
|
||||
|
||||
<div className = { classes.contentControls }>
|
||||
<h1 className = { classes.title }>
|
||||
{title}
|
||||
</h1>
|
||||
{_roomName && (
|
||||
<span className = { classes.roomNameContainer }>
|
||||
{isOverflowing ? (
|
||||
<Tooltip content = { _roomName }>
|
||||
<span
|
||||
className = { classes.roomName }
|
||||
ref = { roomNameRef }>
|
||||
{_roomName}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className = { classes.roomName }
|
||||
ref = { roomNameRef }>
|
||||
{_roomName}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
{_buttons.length && <Toolbox toolbarButtons = { _buttons } />}
|
||||
{skipPrejoinButton}
|
||||
{showUnsafeRoomWarning && <UnsafeRoomWarning />}
|
||||
{showDeviceStatus && <DeviceStatus />}
|
||||
{showRecordingWarning && <RecordingWarning />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Preview
|
||||
videoMuted = { videoMuted }
|
||||
videoTrack = { videoTrack } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { hiddenPremeetingButtons } = state['features/base/config'];
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const premeetingButtons = (ownProps.thirdParty
|
||||
? THIRD_PARTY_PREJOIN_BUTTONS
|
||||
: PREMEETING_BUTTONS).filter((b: any) => !(hiddenPremeetingButtons || []).includes(b));
|
||||
|
||||
const { premeetingBackground } = state['features/dynamic-branding'];
|
||||
|
||||
return {
|
||||
// For keeping backwards compat.: if we pass an empty hiddenPremeetingButtons
|
||||
// array through external api, we have all prejoin buttons present on premeeting
|
||||
// screen regardless of passed values into toolbarButtons config overwrite.
|
||||
// If hiddenPremeetingButtons is missing, we hide the buttons according to
|
||||
// toolbarButtons config overwrite.
|
||||
_buttons: hiddenPremeetingButtons
|
||||
? premeetingButtons
|
||||
: premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
|
||||
_isPreCallTestEnabled: isPreCallTestEnabled(state),
|
||||
_premeetingBackground: premeetingBackground,
|
||||
_roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PreMeetingScreen);
|
||||
|
||||
99
react/features/base/premeeting/components/web/Preview.tsx
Normal file
99
react/features/base/premeeting/components/web/Preview.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import Avatar from '../../../avatar/components/Avatar';
|
||||
import Video from '../../../media/components/web/Video';
|
||||
import { getLocalParticipant } from '../../../participants/functions';
|
||||
import { getDisplayName } from '../../../settings/functions.web';
|
||||
import { getLocalVideoTrack } from '../../../tracks/functions.web';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* Local participant id.
|
||||
*/
|
||||
_participantId: string;
|
||||
|
||||
/**
|
||||
* Flag controlling whether the video should be flipped or not.
|
||||
*/
|
||||
flipVideo: boolean;
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
videoMuted: boolean;
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
videoTrack?: Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component showing the video preview and device status.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Preview(props: IProps) {
|
||||
const { _participantId, flipVideo, name, videoMuted, videoTrack } = props;
|
||||
const className = flipVideo ? 'flipVideoX' : '';
|
||||
|
||||
useEffect(() => {
|
||||
APP.API.notifyPrejoinVideoVisibilityChanged(Boolean(!videoMuted && videoTrack));
|
||||
}, [ videoMuted, videoTrack ]);
|
||||
|
||||
useEffect(() => {
|
||||
APP.API.notifyPrejoinLoaded();
|
||||
|
||||
return () => APP.API.notifyPrejoinVideoVisibilityChanged(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id = 'preview'>
|
||||
{!videoMuted && videoTrack
|
||||
? (
|
||||
<Video
|
||||
className = { className }
|
||||
id = 'prejoinVideo'
|
||||
videoTrack = {{ jitsiTrack: videoTrack }} />
|
||||
)
|
||||
: (
|
||||
<Avatar
|
||||
className = 'premeeting-screen-avatar'
|
||||
displayName = { name }
|
||||
participantId = { _participantId }
|
||||
size = { 200 } />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const name = getDisplayName(state);
|
||||
const { id: _participantId } = getLocalParticipant(state) ?? {};
|
||||
|
||||
return {
|
||||
_participantId: _participantId ?? '',
|
||||
flipVideo: Boolean(state['features/base/settings'].localFlipX),
|
||||
name,
|
||||
videoMuted: ownProps.videoTrack ? ownProps.videoMuted : state['features/base/media'].video.muted,
|
||||
videoTrack: ownProps.videoTrack || getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Preview);
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
warning: {
|
||||
bottom: 0,
|
||||
color: theme.palette.text03,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
...theme.typography.bodyShortRegular,
|
||||
marginBottom: theme.spacing(3),
|
||||
marginTop: theme.spacing(2),
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RecordingWarning = () => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.warning }>
|
||||
{t('prejoin.recordingWarning')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingWarning;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import Checkbox from '../../../ui/components/web/Checkbox';
|
||||
import getUnsafeRoomText from '../../../util/getUnsafeRoomText.web';
|
||||
import { setUnsafeRoomConsent } from '../../actions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
warning: {
|
||||
backgroundColor: theme.palette.warning01,
|
||||
color: theme.palette.text04,
|
||||
...theme.typography.bodyShortRegular,
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
consent: {
|
||||
padding: `0 ${theme.spacing(3)}`,
|
||||
'@media (max-width: 720px)': {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const UnsafeRoomWarning = () => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { unsafeRoomConsent } = useSelector((state: IReduxState) => state['features/base/premeeting']);
|
||||
const toggleConsent = useCallback(
|
||||
() => dispatch(setUnsafeRoomConsent(!unsafeRoomConsent))
|
||||
, [ unsafeRoomConsent, dispatch ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = { classes.warning }>
|
||||
{getUnsafeRoomText(t, 'prejoin')}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked = { unsafeRoomConsent }
|
||||
className = { classes.consent }
|
||||
label = { t('prejoin.unsafeRoomConsent') }
|
||||
onChange = { toggleConsent } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsafeRoomWarning;
|
||||
Reference in New Issue
Block a user