Files
jitsi-meet/react/features/prejoin/components/web/Prejoin.tsx
Robert Pintilii d7cad9d560 feat(prejoin) Update design & refactor (#13089)
Update Dial Out Dialog design
Update Country Picker design
Convert some files to TS
Move styles from SCSS to JSS
Replace atlaskit InlineDialog with Popover in CountryPIcker and Prejoin components
2023-03-23 11:45:29 +02:00

455 lines
14 KiB
TypeScript

import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { isNameReadOnly } from '../../../base/config/functions.web';
import { translate } from '../../../base/i18n/functions';
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg';
import { isVideoMutedByUser } from '../../../base/media/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import Popover from '../../../base/popover/components/Popover.web';
import ActionButton from '../../../base/premeeting/components/web/ActionButton';
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
import { updateSettings } from '../../../base/settings/actions';
import { getDisplayName } from '../../../base/settings/functions.web';
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import {
joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
} from '../../actions.web';
import {
isDeviceStatusVisible,
isDisplayNameRequired,
isJoinByPhoneButtonVisible,
isJoinByPhoneDialogVisible,
isPrejoinDisplayNameVisible
} from '../../functions';
// @ts-ignore
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
interface IProps extends WithTranslation {
/**
* Indicates whether the display name is editable.
*/
canEditDisplayName: boolean;
/**
* Flag signaling if the device status is visible or not.
*/
deviceStatusVisible: boolean;
/**
* If join by phone button should be visible.
*/
hasJoinByPhoneButton: boolean;
/**
* Joins the current meeting.
*/
joinConference: Function;
/**
* Joins the current meeting without audio.
*/
joinConferenceWithoutAudio: Function;
/**
* Whether conference join is in progress.
*/
joiningInProgress?: boolean;
/**
* The name of the user that is about to join.
*/
name: string;
/**
* Local participant id.
*/
participantId?: string;
/**
* The prejoin config.
*/
prejoinConfig?: any;
/**
* Whether the name input should be read only or not.
*/
readOnlyName: boolean;
/**
* Sets visibility of the 'JoinByPhoneDialog'.
*/
setJoinByPhoneDialogVisiblity: Function;
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean;
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
showDialog: boolean;
/**
* If should show an error when joining without a name.
*/
showErrorOnJoin: boolean;
/**
* Updates settings.
*/
updateSettings: Function;
/**
* The JitsiLocalTrack to display.
*/
videoTrack?: Object;
}
interface IState {
/**
* Flag controlling the visibility of the 'join by phone' buttons.
*/
showJoinByPhoneButtons: boolean;
}
/**
* This component is displayed before joining a meeting.
*/
class Prejoin extends Component<IProps, IState> {
showDisplayNameField: boolean;
/**
* Initializes a new {@code Prejoin} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
showJoinByPhoneButtons: false
};
this._closeDialog = this._closeDialog.bind(this);
this._showDialog = this._showDialog.bind(this);
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
this._onDropdownClose = this._onDropdownClose.bind(this);
this._onOptionsClick = this._onOptionsClick.bind(this);
this._setName = this._setName.bind(this);
this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
this._getExtraJoinButtons = this._getExtraJoinButtons.bind(this);
this._onInputKeyPress = this._onInputKeyPress.bind(this);
this.showDisplayNameField = props.canEditDisplayName || props.showErrorOnJoin;
}
/**
* Handler for the join button.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onJoinButtonClick() {
if (this.props.showErrorOnJoin) {
return;
}
this.props.joinConference();
}
/**
* Closes the dropdown.
*
* @returns {void}
*/
_onDropdownClose() {
this.setState({
showJoinByPhoneButtons: false
});
}
/**
* Displays the join by phone buttons dropdown.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onOptionsClick(e?: React.KeyboardEvent | React.MouseEvent | undefined) {
e?.stopPropagation();
this.setState({
showJoinByPhoneButtons: !this.state.showJoinByPhoneButtons
});
}
/**
* Sets the guest participant name.
*
* @param {string} displayName - Participant name.
* @returns {void}
*/
_setName(displayName: string) {
this.props.updateSettings({
displayName
});
}
/**
* Closes the join by phone dialog.
*
* @returns {undefined}
*/
_closeDialog() {
this.props.setJoinByPhoneDialogVisiblity(false);
}
/**
* Displays the dialog for joining a meeting by phone.
*
* @returns {undefined}
*/
_showDialog() {
this.props.setJoinByPhoneDialogVisiblity(true);
this._onDropdownClose();
}
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_showDialogKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._showDialog();
}
}
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onJoinConferenceWithoutAudioKeyPress(e: React.KeyboardEvent) {
if (this.props.joinConferenceWithoutAudio
&& (e.key === ' '
|| e.key === 'Enter')) {
e.preventDefault();
this.props.joinConferenceWithoutAudio();
}
}
/**
* Gets the list of extra join buttons.
*
* @returns {Object} - The list of extra buttons.
*/
_getExtraJoinButtons() {
const { joinConferenceWithoutAudio, t } = this.props;
const noAudio = {
key: 'no-audio',
testId: 'prejoin.joinWithoutAudio',
icon: IconVolumeOff,
label: t('prejoin.joinWithoutAudio'),
onClick: joinConferenceWithoutAudio,
onKeyPress: this._onJoinConferenceWithoutAudioKeyPress
};
const byPhone = {
key: 'by-phone',
testId: 'prejoin.joinByPhone',
icon: IconPhoneRinging,
label: t('prejoin.joinAudioByPhone'),
onClick: this._showDialog,
onKeyPress: this._showDialogKeyPress
};
return {
noAudio,
byPhone
};
}
/**
* Handle keypress on input.
*
* @param {KeyboardEvent} e - Keyboard event.
* @returns {void}
*/
_onInputKeyPress(e: React.KeyboardEvent) {
const { joinConference } = this.props;
if (e.key === 'Enter') {
joinConference();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
deviceStatusVisible,
hasJoinByPhoneButton,
joinConferenceWithoutAudio,
joiningInProgress,
name,
participantId,
prejoinConfig,
readOnlyName,
showCameraPreview,
showDialog,
showErrorOnJoin,
t,
videoTrack
} = this.props;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick,
_onOptionsClick, _setName, _onInputKeyPress } = this;
const extraJoinButtons = this._getExtraJoinButtons();
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: any) =>
!(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
);
if (!hasJoinByPhoneButton) {
extraButtonsToRender = extraButtonsToRender.filter((btn: any) => btn.key !== 'by-phone');
}
const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
const { showJoinByPhoneButtons } = this.state;
return (
<PreMeetingScreen
showDeviceStatus = { deviceStatusVisible }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack }>
<div
className = 'prejoin-input-area'
data-testid = 'prejoin.screen'>
{this.showDisplayNameField ? (<Input
autoComplete = { 'name' }
autoFocus = { true }
className = 'prejoin-input'
error = { showErrorOnJoin }
onChange = { _setName }
onKeyPress = { _onInputKeyPress }
placeholder = { t('dialog.enterDisplayName') }
readOnly = { readOnlyName }
value = { name } />
) : (
<div className = 'prejoin-avatar-container'>
<Avatar
className = 'prejoin-avatar'
displayName = { name }
participantId = { participantId }
size = { 72 } />
<div className = 'prejoin-avatar-name'>{name}</div>
</div>
)}
{showErrorOnJoin && <div
className = 'prejoin-error'
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
<div className = 'prejoin-preview-dropdown-container'>
<Popover
content = { hasExtraJoinButtons && <div className = 'prejoin-preview-dropdown-btns'>
{extraButtonsToRender.map(({ key, ...rest }) => (
<Button
disabled = { joiningInProgress }
fullWidth = { true }
key = { key }
type = { BUTTON_TYPES.SECONDARY }
{ ...rest } />
))}
</div> }
onPopoverClose = { _onDropdownClose }
position = 'bottom'
trigger = 'click'
visible = { showJoinByPhoneButtons }>
<ActionButton
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
disabled = { joiningInProgress }
hasOptions = { hasExtraJoinButtons }
onClick = { _onJoinButtonClick }
onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
testId = 'prejoin.joinMeeting'
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</Popover>
</div>
</div>
{ showDialog && (
<JoinByPhoneDialog
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
onClose = { _closeDialog } />
)}
</PreMeetingScreen>
);
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const name = getDisplayName(state);
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
const { id: participantId } = getLocalParticipant(state) ?? {};
const { joiningInProgress } = state['features/prejoin'];
return {
canEditDisplayName: isPrejoinDisplayNameVisible(state),
deviceStatusVisible: isDeviceStatusVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
joiningInProgress,
name,
participantId,
prejoinConfig: state['features/base/config'].prejoinConfig,
readOnlyName: isNameReadOnly(state),
showCameraPreview: !isVideoMutedByUser(state),
showDialog: isJoinByPhoneDialogVisible(state),
showErrorOnJoin,
videoTrack: getLocalJitsiVideoTrack(state)
};
}
const mapDispatchToProps = {
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
joinConference: joinConferenceAction,
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
updateSettings
};
export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin));