Browse Source

feat(prejoin) Expose prejoin app

master^2
Tudor-Ovidiu Avram 4 years ago
parent
commit
0e5091adba

+ 16
- 8
css/_premeeting-screens.scss View File

@@ -1,17 +1,21 @@
1 1
 /**
2 2
  * Shared style for full screen local track based dialogs/modals.
3 3
  */
4
-.premeeting-screen {
4
+ .premeeting-screen,
5
+  .preview-overlay {
6
+     position: absolute;
7
+     left: 0;
8
+     right: 0;
9
+     top: 0;
10
+     bottom: 0;
11
+  }
12
+
13
+ .premeeting-screen {
5 14
     align-items: stretch;
6
-    background: #1C2025;
7
-    bottom: 0;
15
+    background: radial-gradient(50% 50% at 50% 50%, #5D95C7 0%, #376288 100%), #FFFFFF;
8 16
     display: flex;
9 17
     flex-direction: column;
10 18
     font-size: 1.3em;
11
-    left: 0;
12
-    position: absolute;
13
-    right: 0;
14
-    top: 0;
15 19
     z-index: $toolbarZ + 1;
16 20
 
17 21
     .action-btn {
@@ -74,9 +78,13 @@
74 78
         }
75 79
     }
76 80
 
81
+    .preview-overlay {
82
+        background-image: linear-gradient(transparent, black);
83
+        z-index: $toolbarZ + 1;
84
+    }
85
+
77 86
     .content {
78 87
         align-items: center;
79
-        background-image: linear-gradient(transparent, black);
80 88
         display: flex;
81 89
         flex: 1;
82 90
         flex-direction: column;

+ 10
- 0
index.html View File

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

+ 2
- 2
react/features/base/app/components/BaseApp.js View File

@@ -122,14 +122,14 @@ export default class BaseApp extends Component<*, State> {
122 122
      * @returns {ReactElement}
123 123
      */
124 124
     render() {
125
-        const { route: { component }, store } = this.state;
125
+        const { route: { component, props }, store } = this.state;
126 126
 
127 127
         if (store) {
128 128
             return (
129 129
                 <I18nextProvider i18n = { i18next }>
130 130
                     <Provider store = { store }>
131 131
                         <Fragment>
132
-                            { this._createMainElement(component) }
132
+                            { this._createMainElement(component, props) }
133 133
                             <SoundCollection />
134 134
                             { this._createExtraElement() }
135 135
                             { this._renderDialogContainer() }

+ 31
- 5
react/features/base/premeeting/components/web/PreMeetingScreen.js View File

@@ -24,6 +24,16 @@ type Props = {
24 24
      */
25 25
     name?: string,
26 26
 
27
+    /**
28
+     * Indicates whether the avatar should be shown when video is off
29
+     */
30
+    showAvatar: boolean,
31
+
32
+    /**
33
+     * Indicates whether the label and copy url action should be shown
34
+     */
35
+    showConferenceInfo: boolean,
36
+
27 37
     /**
28 38
      * Title of the screen.
29 39
      */
@@ -45,13 +55,23 @@ type Props = {
45 55
  * on the prejoin screen (pre-connection) or lobby (post-connection).
46 56
  */
47 57
 export default class PreMeetingScreen extends PureComponent<Props> {
58
+    /**
59
+     * Default values for {@code Prejoin} component's properties.
60
+     *
61
+     * @static
62
+     */
63
+    static defaultProps = {
64
+        showAvatar: true,
65
+        showConferenceInfo: true
66
+    };
67
+
48 68
     /**
49 69
      * Implements {@code PureComponent#render}.
50 70
      *
51 71
      * @inheritdoc
52 72
      */
53 73
     render() {
54
-        const { name, title, videoMuted, videoTrack } = this.props;
74
+        const { name, showAvatar, showConferenceInfo, title, videoMuted, videoTrack } = this.props;
55 75
 
56 76
         return (
57 77
             <div
@@ -59,13 +79,19 @@ export default class PreMeetingScreen extends PureComponent<Props> {
59 79
                 id = 'lobby-screen'>
60 80
                 <Preview
61 81
                     name = { name }
82
+                    showAvatar = { showAvatar }
62 83
                     videoMuted = { videoMuted }
63 84
                     videoTrack = { videoTrack } />
85
+                {!videoMuted && <div className = 'preview-overlay' />}
64 86
                 <div className = 'content'>
65
-                    <div className = 'title'>
66
-                        { title }
67
-                    </div>
68
-                    <CopyMeetingUrl />
87
+                    {showConferenceInfo && (
88
+                        <>
89
+                            <div className = 'title'>
90
+                                { title }
91
+                            </div>
92
+                            <CopyMeetingUrl />
93
+                        </>
94
+                    )}
69 95
                     { this.props.children }
70 96
                     <div className = 'media-btn-container'>
71 97
                         <AudioSettingsButton visible = { true } />

+ 25
- 12
react/features/base/premeeting/components/web/Preview.js View File

@@ -14,6 +14,11 @@ export type Props = {
14 14
      */
15 15
     name: string,
16 16
 
17
+    /**
18
+     * Indicates whether the avatar should be shown when video is off
19
+     */
20
+    showAvatar: boolean,
21
+
17 22
     /**
18 23
      * Flag signaling the visibility of camera preview.
19 24
      */
@@ -32,7 +37,7 @@ export type Props = {
32 37
  * @returns {ReactElement}
33 38
  */
34 39
 function Preview(props: Props) {
35
-    const { name, videoMuted, videoTrack } = props;
40
+    const { name, showAvatar, videoMuted, videoTrack } = props;
36 41
 
37 42
     if (!videoMuted && videoTrack) {
38 43
         return (
@@ -44,19 +49,27 @@ function Preview(props: Props) {
44 49
         );
45 50
     }
46 51
 
47
-    return (
48
-        <div
49
-            className = 'no-video'
50
-            id = 'preview'>
51
-            <Avatar
52
-                className = 'preview-avatar'
53
-                displayName = { name }
54
-                participantId = 'local'
55
-                size = { 200 } />
56
-        </div>
57
-    );
52
+    if (showAvatar) {
53
+        return (
54
+            <div
55
+                className = 'no-video'
56
+                id = 'preview'>
57
+                <Avatar
58
+                    className = 'preview-avatar'
59
+                    displayName = { name }
60
+                    participantId = 'local'
61
+                    size = { 200 } />
62
+            </div>
63
+        );
64
+    }
65
+
66
+    return null;
58 67
 }
59 68
 
69
+Preview.defaultProps = {
70
+    showAvatar: true
71
+};
72
+
60 73
 /**
61 74
  * Maps part of the Redux state to the props of this component.
62 75
  *

+ 84
- 1
react/features/base/tracks/functions.js View File

@@ -1,7 +1,7 @@
1 1
 /* global APP */
2 2
 
3 3
 import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
4
-import { MEDIA_TYPE } from '../media';
4
+import { MEDIA_TYPE, setAudioMuted } from '../media';
5 5
 import {
6 6
     getUserSelectedCameraDeviceId,
7 7
     getUserSelectedMicDeviceId
@@ -125,6 +125,89 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
125 125
         }));
126 126
 }
127 127
 
128
+/**
129
+ * Returns an object containing a promise which resolves with the created tracks &
130
+ * the errors resulting from that process.
131
+ *
132
+ * @returns {Promise<JitsiLocalTrack>}
133
+ *
134
+ * @todo Refactor to not use APP
135
+ */
136
+export function createPrejoinTracks() {
137
+    const errors = {};
138
+    const initialDevices = [ 'audio' ];
139
+    const requestedAudio = true;
140
+    let requestedVideo = false;
141
+    const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
142
+
143
+    // Always get a handle on the audio input device so that we have statistics even if the user joins the
144
+    // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
145
+    // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
146
+    // only after that point.
147
+    if (startWithAudioMuted) {
148
+        APP.store.dispatch(setAudioMuted(true));
149
+    }
150
+
151
+    if (!startWithVideoMuted && !startAudioOnly) {
152
+        initialDevices.push('video');
153
+        requestedVideo = true;
154
+    }
155
+
156
+    let tryCreateLocalTracks;
157
+
158
+    if (!requestedAudio && !requestedVideo) {
159
+        // Resolve with no tracks
160
+        tryCreateLocalTracks = Promise.resolve([]);
161
+    } else {
162
+        tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
163
+                .catch(err => {
164
+                    if (requestedAudio && requestedVideo) {
165
+
166
+                        // Try audio only...
167
+                        errors.audioAndVideoError = err;
168
+
169
+                        return (
170
+                            createLocalTracksF({ devices: [ 'audio' ] }, true));
171
+                    } else if (requestedAudio && !requestedVideo) {
172
+                        errors.audioOnlyError = err;
173
+
174
+                        return [];
175
+                    } else if (requestedVideo && !requestedAudio) {
176
+                        errors.videoOnlyError = err;
177
+
178
+                        return [];
179
+                    }
180
+                    logger.error('Should never happen');
181
+                })
182
+                .catch(err => {
183
+                    // Log this just in case...
184
+                    if (!requestedAudio) {
185
+                        logger.error('The impossible just happened', err);
186
+                    }
187
+                    errors.audioOnlyError = err;
188
+
189
+                    // Try video only...
190
+                    return requestedVideo
191
+                        ? createLocalTracksF({ devices: [ 'video' ] }, true)
192
+                        : [];
193
+                })
194
+                .catch(err => {
195
+                    // Log this just in case...
196
+                    if (!requestedVideo) {
197
+                        logger.error('The impossible just happened', err);
198
+                    }
199
+                    errors.videoOnlyError = err;
200
+
201
+                    return [];
202
+                });
203
+    }
204
+
205
+    return {
206
+        tryCreateLocalTracks,
207
+        errors
208
+    };
209
+}
210
+
128 211
 /**
129 212
  * Returns local audio track.
130 213
  *

+ 4
- 0
react/features/base/tracks/middleware.js View File

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

+ 74
- 49
react/features/prejoin/components/Prejoin.js View File

@@ -79,11 +79,21 @@ type Props = {
79 79
      */
80 80
     setJoinByPhoneDialogVisiblity: Function,
81 81
 
82
+    /**
83
+     * Indicates whether the avatar should be shown when video is off
84
+     */
85
+    showAvatar: boolean,
86
+
82 87
     /**
83 88
      * Flag signaling the visibility of camera preview.
84 89
      */
85 90
     showCameraPreview: boolean,
86 91
 
92
+    /**
93
+     * Flag signaling the visibility of join label, input and buttons
94
+     */
95
+    showJoinActions: boolean,
96
+
87 97
     /**
88 98
      * If 'JoinByPhoneDialog' is visible or not.
89 99
      */
@@ -112,6 +122,15 @@ type State = {
112 122
  * This component is displayed before joining a meeting.
113 123
  */
114 124
 class Prejoin extends Component<Props, State> {
125
+    /**
126
+     * Default values for {@code Prejoin} component's properties.
127
+     *
128
+     * @static
129
+     */
130
+    static defaultProps = {
131
+        showJoinActions: true
132
+    };
133
+
115 134
     /**
116 135
      * Initializes a new {@code Prejoin} instance.
117 136
      *
@@ -223,8 +242,10 @@ class Prejoin extends Component<Props, State> {
223 242
             joinConference,
224 243
             joinConferenceWithoutAudio,
225 244
             name,
245
+            showAvatar,
226 246
             showCameraPreview,
227 247
             showDialog,
248
+            showJoinActions,
228 249
             t,
229 250
             videoTrack
230 251
         } = this.props;
@@ -236,61 +257,65 @@ class Prejoin extends Component<Props, State> {
236 257
             <PreMeetingScreen
237 258
                 footer = { this._renderFooter() }
238 259
                 name = { name }
260
+                showAvatar = { showAvatar }
261
+                showConferenceInfo = { showJoinActions }
239 262
                 title = { t('prejoin.joinMeeting') }
240 263
                 videoMuted = { !showCameraPreview }
241 264
                 videoTrack = { videoTrack }>
242
-                <div className = 'prejoin-input-area-container'>
243
-                    <div className = 'prejoin-input-area'>
244
-                        <InputField
245
-                            onChange = { _setName }
246
-                            onSubmit = { joinConference }
247
-                            placeHolder = { t('dialog.enterDisplayName') }
248
-                            value = { name } />
249
-
250
-                        <div className = 'prejoin-preview-dropdown-container'>
251
-                            <InlineDialog
252
-                                content = { <div className = 'prejoin-preview-dropdown-btns'>
253
-                                    <div
254
-                                        className = 'prejoin-preview-dropdown-btn'
255
-                                        onClick = { joinConferenceWithoutAudio }>
256
-                                        <Icon
257
-                                            className = 'prejoin-preview-dropdown-icon'
258
-                                            size = { 24 }
259
-                                            src = { IconVolumeOff } />
260
-                                        { t('prejoin.joinWithoutAudio') }
261
-                                    </div>
262
-                                    {hasJoinByPhoneButton && <div
263
-                                        className = 'prejoin-preview-dropdown-btn'
264
-                                        onClick = { _showDialog }>
265
-                                        <Icon
266
-                                            className = 'prejoin-preview-dropdown-icon'
267
-                                            size = { 24 }
268
-                                            src = { IconPhone } />
269
-                                        { t('prejoin.joinAudioByPhone') }
270
-                                    </div>}
271
-                                </div> }
272
-                                isOpen = { showJoinByPhoneButtons }
273
-                                onClose = { _onDropdownClose }>
274
-                                <ActionButton
275
-                                    disabled = { joinButtonDisabled }
276
-                                    hasOptions = { true }
277
-                                    onClick = { joinConference }
278
-                                    onOptionsClick = { _onOptionsClick }
279
-                                    type = 'primary'>
280
-                                    { t('prejoin.joinMeeting') }
281
-                                </ActionButton>
282
-                            </InlineDialog>
265
+                {showJoinActions && (
266
+                    <div className = 'prejoin-input-area-container'>
267
+                        <div className = 'prejoin-input-area'>
268
+                            <InputField
269
+                                onChange = { _setName }
270
+                                onSubmit = { joinConference }
271
+                                placeHolder = { t('dialog.enterDisplayName') }
272
+                                value = { name } />
273
+
274
+                            <div className = 'prejoin-preview-dropdown-container'>
275
+                                <InlineDialog
276
+                                    content = { <div className = 'prejoin-preview-dropdown-btns'>
277
+                                        <div
278
+                                            className = 'prejoin-preview-dropdown-btn'
279
+                                            onClick = { joinConferenceWithoutAudio }>
280
+                                            <Icon
281
+                                                className = 'prejoin-preview-dropdown-icon'
282
+                                                size = { 24 }
283
+                                                src = { IconVolumeOff } />
284
+                                            { t('prejoin.joinWithoutAudio') }
285
+                                        </div>
286
+                                        {hasJoinByPhoneButton && <div
287
+                                            className = 'prejoin-preview-dropdown-btn'
288
+                                            onClick = { _showDialog }>
289
+                                            <Icon
290
+                                                className = 'prejoin-preview-dropdown-icon'
291
+                                                size = { 24 }
292
+                                                src = { IconPhone } />
293
+                                            { t('prejoin.joinAudioByPhone') }
294
+                                        </div>}
295
+                                    </div> }
296
+                                    isOpen = { showJoinByPhoneButtons }
297
+                                    onClose = { _onDropdownClose }>
298
+                                    <ActionButton
299
+                                        disabled = { joinButtonDisabled }
300
+                                        hasOptions = { true }
301
+                                        onClick = { joinConference }
302
+                                        onOptionsClick = { _onOptionsClick }
303
+                                        type = 'primary'>
304
+                                        { t('prejoin.joinMeeting') }
305
+                                    </ActionButton>
306
+                                </InlineDialog>
307
+                            </div>
283 308
                         </div>
284
-                    </div>
285 309
 
286
-                    <div className = 'prejoin-checkbox-container'>
287
-                        <input
288
-                            className = 'prejoin-checkbox'
289
-                            onChange = { _onCheckboxChange }
290
-                            type = 'checkbox' />
291
-                        <span>{t('prejoin.doNotShow')}</span>
310
+                        <div className = 'prejoin-checkbox-container'>
311
+                            <input
312
+                                className = 'prejoin-checkbox'
313
+                                onChange = { _onCheckboxChange }
314
+                                type = 'checkbox' />
315
+                            <span>{t('prejoin.doNotShow')}</span>
316
+                        </div>
292 317
                     </div>
293
-                </div>
318
+                )}
294 319
                 { showDialog && (
295 320
                     <JoinByPhoneDialog
296 321
                         joinConferenceWithoutAudio = { joinConferenceWithoutAudio }

+ 93
- 0
react/features/prejoin/components/PrejoinApp.js View File

@@ -0,0 +1,93 @@
1
+// @flow
2
+
3
+import { AtlasKitThemeProvider } from '@atlaskit/theme';
4
+import React from 'react';
5
+
6
+import { BaseApp } from '../../../features/base/app';
7
+import { setConfig } from '../../base/config';
8
+import { createPrejoinTracks } from '../../base/tracks';
9
+import { initPrejoin } from '../actions';
10
+
11
+import Prejoin from './Prejoin';
12
+
13
+type Props = {
14
+
15
+    /**
16
+     * Indicates whether the avatar should be shown when video is off
17
+     */
18
+    showAvatar: boolean,
19
+
20
+    /**
21
+     * Flag signaling the visibility of join label, input and buttons
22
+     */
23
+    showJoinActions: boolean,
24
+};
25
+
26
+/**
27
+ * Wrapper application for prejoin.
28
+ *
29
+ * @extends BaseApp
30
+ */
31
+export default class PrejoinApp extends BaseApp<Props> {
32
+    _init: Promise<*>;
33
+
34
+    /**
35
+     * Navigates to {@link Prejoin} upon mount.
36
+     *
37
+     * @returns {void}
38
+     */
39
+    componentDidMount() {
40
+        super.componentDidMount();
41
+
42
+        this._init.then(async () => {
43
+            const { store } = this.state;
44
+            const { dispatch } = store;
45
+            const { showAvatar, showJoinActions } = this.props;
46
+
47
+            super._navigate({
48
+                component: Prejoin,
49
+                props: {
50
+                    showAvatar,
51
+                    showJoinActions
52
+                }
53
+            });
54
+
55
+            const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
56
+
57
+            dispatch(setConfig({
58
+                prejoinPageEnabled: true,
59
+                startWithAudioMuted,
60
+                startWithVideoMuted
61
+            }));
62
+
63
+            const { tryCreateLocalTracks, errors } = createPrejoinTracks();
64
+
65
+            const tracks = await tryCreateLocalTracks;
66
+
67
+            dispatch(initPrejoin(tracks, errors));
68
+        });
69
+    }
70
+
71
+    /**
72
+     * Overrides the parent method to inject {@link AtlasKitThemeProvider} as
73
+     * the top most component.
74
+     *
75
+     * @override
76
+     */
77
+    _createMainElement(component, props) {
78
+        return (
79
+            <AtlasKitThemeProvider mode = 'dark'>
80
+                { super._createMainElement(component, props) }
81
+            </AtlasKitThemeProvider>
82
+        );
83
+    }
84
+
85
+    /**
86
+     * Renders the platform specific dialog container.
87
+     *
88
+     * @returns {React$Element}
89
+     */
90
+    _renderDialogContainer() {
91
+        return null;
92
+    }
93
+}

+ 20
- 3
react/index.web.js View File

@@ -8,6 +8,8 @@ import { getJitsiMeetTransport } from '../modules/transport';
8 8
 import { App } from './features/app/components';
9 9
 import { getLogger } from './features/base/logging/functions';
10 10
 import { Platform } from './features/base/react';
11
+import { getJitsiMeetGlobalNS } from './features/base/util';
12
+import PrejoinApp from './features/prejoin/components/PrejoinApp';
11 13
 
12 14
 const logger = getLogger('index.web');
13 15
 const OS = Platform.OS;
@@ -20,9 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
20 22
 
21 23
     APP.connectionTimes['document.ready'] = now;
22 24
     logger.log('(TIME) document ready:\t', now);
23
-
24
-    // Render the main/root Component.
25
-    ReactDOM.render(<App />, document.getElementById('react'));
26 25
 });
27 26
 
28 27
 // Workaround for the issue when returning to a page with the back button and
@@ -56,3 +55,21 @@ window.addEventListener('beforeunload', () => {
56 55
     APP.API.dispose();
57 56
     getJitsiMeetTransport().dispose();
58 57
 });
58
+
59
+const globalNS = getJitsiMeetGlobalNS();
60
+
61
+globalNS.entryPoints = {
62
+    APP: App,
63
+    PREJOIN: PrejoinApp
64
+};
65
+
66
+globalNS.renderEntryPoint = ({
67
+    Component,
68
+    props = {},
69
+    elementId = 'react'
70
+}) => {
71
+    ReactDOM.render(
72
+        <Component { ...props } />,
73
+        document.getElementById(elementId)
74
+    );
75
+};

+ 41
- 0
static/prejoin.html View File

@@ -0,0 +1,41 @@
1
+<html xmlns="http://www.w3.org/1999/html">
2
+  <head>
3
+    <meta charset="utf-8">
4
+    <meta http-equiv="content-type" content="text/html;charset=utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <!--#include virtual="/base.html" -->
7
+    <link rel="stylesheet" href="../css/all.css">
8
+    <script>
9
+      document.addEventListener('DOMContentLoaded', () => {
10
+          if (!JitsiMeetJS.app) {
11
+            return;
12
+          }
13
+
14
+          const url = new URL(window.location.href);
15
+          const params = new URLSearchParams(url.search);
16
+          const showAvatar = params.get('showAvatar') === 'true';
17
+          const showJoinActions = params.get('showJoinActions') === 'true';
18
+          const css = params.get('style');
19
+          const style = document.createElement('style');
20
+          style.appendChild(document.createTextNode(css));
21
+          document.head.appendChild(style);
22
+
23
+          JitsiMeetJS.app.renderEntryPoint({
24
+              Component: JitsiMeetJS.app.entryPoints.PREJOIN,
25
+              props: {
26
+                showAvatar,
27
+                showJoinActions
28
+              }
29
+          })
30
+      })
31
+    </script>
32
+    <!--#include virtual="/title.html" -->
33
+    <script>var config = {}</script><!-- adapt to your needs, i.e. set hosts and bosh path -->
34
+    <script>var interfaceConfig = {}</script>
35
+    <script src="../libs/lib-jitsi-meet.min.js?v=139"></script>
36
+    <script src="../libs/app.bundle.min.js?v=139"></script>
37
+  </head>
38
+  <body>
39
+    <div id="react"></div>
40
+  </body>
41
+</html>

Loading…
Cancel
Save