feat(prejoin) Expose prejoin app

This commit is contained in:
Tudor-Ovidiu Avram 2020-06-29 10:45:58 +03:00 committed by Saúl Ibarra Corretgé
parent f376542441
commit 0e5091adba
11 changed files with 400 additions and 80 deletions

View File

@ -1,17 +1,21 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
.premeeting-screen,
.preview-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.premeeting-screen {
align-items: stretch;
background: #1C2025;
bottom: 0;
background: radial-gradient(50% 50% at 50% 50%, #5D95C7 0%, #376288 100%), #FFFFFF;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
.action-btn {
@ -74,9 +78,13 @@
}
}
.preview-overlay {
background-image: linear-gradient(transparent, black);
z-index: $toolbarZ + 1;
}
.content {
align-items: center;
background-image: linear-gradient(transparent, black);
display: flex;
flex: 1;
flex-direction: column;

View File

@ -8,7 +8,17 @@
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="stylesheet" href="css/all.css">
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!JitsiMeetJS.app) {
return;
}
JitsiMeetJS.app.renderEntryPoint({
Component: JitsiMeetJS.app.entryPoints.APP
})
})
</script>
<script>
// IE11 and earlier can be identified via their user agent and be
// redirected to a page that is known to have no newer js syntax.

View File

@ -122,14 +122,14 @@ export default class BaseApp extends Component<*, State> {
* @returns {ReactElement}
*/
render() {
const { route: { component }, store } = this.state;
const { route: { component, props }, store } = this.state;
if (store) {
return (
<I18nextProvider i18n = { i18next }>
<Provider store = { store }>
<Fragment>
{ this._createMainElement(component) }
{ this._createMainElement(component, props) }
<SoundCollection />
{ this._createExtraElement() }
{ this._renderDialogContainer() }

View File

@ -24,6 +24,16 @@ type Props = {
*/
name?: string,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Indicates whether the label and copy url action should be shown
*/
showConferenceInfo: boolean,
/**
* Title of the screen.
*/
@ -45,13 +55,23 @@ type Props = {
* on the prejoin screen (pre-connection) or lobby (post-connection).
*/
export default class PreMeetingScreen extends PureComponent<Props> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showAvatar: true,
showConferenceInfo: true
};
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { name, title, videoMuted, videoTrack } = this.props;
const { name, showAvatar, showConferenceInfo, title, videoMuted, videoTrack } = this.props;
return (
<div
@ -59,13 +79,19 @@ export default class PreMeetingScreen extends PureComponent<Props> {
id = 'lobby-screen'>
<Preview
name = { name }
showAvatar = { showAvatar }
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
{!videoMuted && <div className = 'preview-overlay' />}
<div className = 'content'>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
{showConferenceInfo && (
<>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
</>
)}
{ this.props.children }
<div className = 'media-btn-container'>
<AudioSettingsButton visible = { true } />

View File

@ -14,6 +14,11 @@ export type Props = {
*/
name: string,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
@ -32,7 +37,7 @@ export type Props = {
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { name, videoMuted, videoTrack } = props;
const { name, showAvatar, videoMuted, videoTrack } = props;
if (!videoMuted && videoTrack) {
return (
@ -44,19 +49,27 @@ function Preview(props: Props) {
);
}
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { 200 } />
</div>
);
if (showAvatar) {
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { 200 } />
</div>
);
}
return null;
}
Preview.defaultProps = {
showAvatar: true
};
/**
* Maps part of the Redux state to the props of this component.
*

View File

@ -1,7 +1,7 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import { MEDIA_TYPE, setAudioMuted } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
@ -125,6 +125,89 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
}));
}
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
*
* @returns {Promise<JitsiLocalTrack>}
*
* @todo Refactor to not use APP
*/
export function createPrejoinTracks() {
const errors = {};
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (startWithAudioMuted) {
APP.store.dispatch(setAudioMuted(true));
}
if (!startWithVideoMuted && !startAudioOnly) {
initialDevices.push('video');
requestedVideo = true;
}
let tryCreateLocalTracks;
if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
return (
createLocalTracksF({ devices: [ 'audio' ] }, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
return [];
}
logger.error('Should never happen');
})
.catch(err => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
// Try video only...
return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true)
: [];
})
.catch(err => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
return [];
});
}
return {
tryCreateLocalTracks,
errors
};
}
/**
* Returns local audio track.
*

View File

@ -143,6 +143,10 @@ MiddlewareRegistry.register(store => next => action => {
if (typeof APP !== 'undefined') {
const result = next(action);
if (isPrejoinPageVisible(store.getState())) {
return result;
}
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();

View File

@ -79,11 +79,21 @@ type Props = {
*/
setJoinByPhoneDialogVisiblity: Function,
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
@ -112,6 +122,15 @@ type State = {
* This component is displayed before joining a meeting.
*/
class Prejoin extends Component<Props, State> {
/**
* Default values for {@code Prejoin} component's properties.
*
* @static
*/
static defaultProps = {
showJoinActions: true
};
/**
* Initializes a new {@code Prejoin} instance.
*
@ -223,8 +242,10 @@ class Prejoin extends Component<Props, State> {
joinConference,
joinConferenceWithoutAudio,
name,
showAvatar,
showCameraPreview,
showDialog,
showJoinActions,
t,
videoTrack
} = this.props;
@ -236,61 +257,65 @@ class Prejoin extends Component<Props, State> {
<PreMeetingScreen
footer = { this._renderFooter() }
name = { name }
showAvatar = { showAvatar }
showConferenceInfo = { showJoinActions }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack }>
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<InputField
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
{showJoinActions && (
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<InputField
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
onClick = { joinConferenceWithoutAudio }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
disabled = { joinButtonDisabled }
hasOptions = { true }
onClick = { joinConference }
onOptionsClick = { _onOptionsClick }
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
onClick = { joinConferenceWithoutAudio }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconVolumeOff } />
{ t('prejoin.joinWithoutAudio') }
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
</div>}
</div> }
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
disabled = { joinButtonDisabled }
hasOptions = { true }
onClick = { joinConference }
onOptionsClick = { _onOptionsClick }
type = 'primary'>
{ t('prejoin.joinMeeting') }
</ActionButton>
</InlineDialog>
</div>
</div>
<div className = 'prejoin-checkbox-container'>
<input
className = 'prejoin-checkbox'
onChange = { _onCheckboxChange }
type = 'checkbox' />
<span>{t('prejoin.doNotShow')}</span>
</div>
</div>
<div className = 'prejoin-checkbox-container'>
<input
className = 'prejoin-checkbox'
onChange = { _onCheckboxChange }
type = 'checkbox' />
<span>{t('prejoin.doNotShow')}</span>
</div>
</div>
)}
{ showDialog && (
<JoinByPhoneDialog
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }

View File

@ -0,0 +1,93 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import { BaseApp } from '../../../features/base/app';
import { setConfig } from '../../base/config';
import { createPrejoinTracks } from '../../base/tracks';
import { initPrejoin } from '../actions';
import Prejoin from './Prejoin';
type Props = {
/**
* Indicates whether the avatar should be shown when video is off
*/
showAvatar: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
showJoinActions: boolean,
};
/**
* Wrapper application for prejoin.
*
* @extends BaseApp
*/
export default class PrejoinApp extends BaseApp<Props> {
_init: Promise<*>;
/**
* Navigates to {@link Prejoin} upon mount.
*
* @returns {void}
*/
componentDidMount() {
super.componentDidMount();
this._init.then(async () => {
const { store } = this.state;
const { dispatch } = store;
const { showAvatar, showJoinActions } = this.props;
super._navigate({
component: Prejoin,
props: {
showAvatar,
showJoinActions
}
});
const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
dispatch(setConfig({
prejoinPageEnabled: true,
startWithAudioMuted,
startWithVideoMuted
}));
const { tryCreateLocalTracks, errors } = createPrejoinTracks();
const tracks = await tryCreateLocalTracks;
dispatch(initPrejoin(tracks, errors));
});
}
/**
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
* the top most component.
*
* @override
*/
_createMainElement(component, props) {
return (
<AtlasKitThemeProvider mode = 'dark'>
{ super._createMainElement(component, props) }
</AtlasKitThemeProvider>
);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return null;
}
}

View File

@ -8,6 +8,8 @@ import { getJitsiMeetTransport } from '../modules/transport';
import { App } from './features/app/components';
import { getLogger } from './features/base/logging/functions';
import { Platform } from './features/base/react';
import { getJitsiMeetGlobalNS } from './features/base/util';
import PrejoinApp from './features/prejoin/components/PrejoinApp';
const logger = getLogger('index.web');
const OS = Platform.OS;
@ -20,9 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
APP.connectionTimes['document.ready'] = now;
logger.log('(TIME) document ready:\t', now);
// Render the main/root Component.
ReactDOM.render(<App />, document.getElementById('react'));
});
// Workaround for the issue when returning to a page with the back button and
@ -56,3 +55,21 @@ window.addEventListener('beforeunload', () => {
APP.API.dispose();
getJitsiMeetTransport().dispose();
});
const globalNS = getJitsiMeetGlobalNS();
globalNS.entryPoints = {
APP: App,
PREJOIN: PrejoinApp
};
globalNS.renderEntryPoint = ({
Component,
props = {},
elementId = 'react'
}) => {
ReactDOM.render(
<Component { ...props } />,
document.getElementById(elementId)
);
};

41
static/prejoin.html Normal file
View File

@ -0,0 +1,41 @@
<html xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="/base.html" -->
<link rel="stylesheet" href="../css/all.css">
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!JitsiMeetJS.app) {
return;
}
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const showAvatar = params.get('showAvatar') === 'true';
const showJoinActions = params.get('showJoinActions') === 'true';
const css = params.get('style');
const style = document.createElement('style');
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
JitsiMeetJS.app.renderEntryPoint({
Component: JitsiMeetJS.app.entryPoints.PREJOIN,
props: {
showAvatar,
showJoinActions
}
})
})
</script>
<!--#include virtual="/title.html" -->
<script>var config = {}</script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script>var interfaceConfig = {}</script>
<script src="../libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="../libs/app.bundle.min.js?v=139"></script>
</head>
<body>
<div id="react"></div>
</body>
</html>