feat(prejoin_page): Add prejoin page

This commit is contained in:
Vlad Piersec
2020-04-16 13:47:10 +03:00
committed by Saúl Ibarra Corretgé
parent 5b53232964
commit a45cbf41ef
36 changed files with 2274 additions and 147 deletions

View 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));

View 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;

View 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));

View 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));

View 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);

View 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);