소스 검색

feat(prejoin_page): Add prejoin page

master
Vlad Piersec 5 년 전
부모
커밋
a45cbf41ef
36개의 변경된 파일2275개의 추가작업 그리고 148개의 파일을 삭제
  1. 200
    112
      conference.js
  2. 3
    0
      config.js
  3. 255
    0
      css/_prejoin.scss
  4. 1
    0
      css/_settings-button.scss
  5. 6
    2
      css/_video-preview.css
  6. 1
    0
      css/main.scss
  7. 27
    0
      lang/main.json
  8. 10
    0
      react/features/base/conference/functions.js
  9. 1
    0
      react/features/base/config/configWhitelist.js
  10. 12
    3
      react/features/base/devices/middleware.js
  11. 3
    0
      react/features/base/icons/svg/arrow-left.svg
  12. 1
    1
      react/features/base/icons/svg/arrow_down.svg
  13. 3
    0
      react/features/base/icons/svg/close-x.svg
  14. 2
    0
      react/features/base/icons/svg/index.js
  15. 19
    4
      react/features/conference/components/web/Conference.js
  16. 11
    2
      react/features/invite/functions.js
  17. 65
    0
      react/features/prejoin/actionTypes.js
  18. 338
    0
      react/features/prejoin/actions.js
  19. 197
    0
      react/features/prejoin/components/Prejoin.js
  20. 51
    0
      react/features/prejoin/components/buttons/ActionButton.js
  21. 197
    0
      react/features/prejoin/components/preview/CopyMeetingUrl.js
  22. 83
    0
      react/features/prejoin/components/preview/DeviceStatus.js
  23. 80
    0
      react/features/prejoin/components/preview/ParticipantName.js
  24. 75
    0
      react/features/prejoin/components/preview/Preview.js
  25. 228
    0
      react/features/prejoin/functions.js
  26. 7
    0
      react/features/prejoin/index.js
  27. 5
    0
      react/features/prejoin/logger.js
  28. 95
    0
      react/features/prejoin/middleware.js
  29. 168
    0
      react/features/prejoin/reducer.js
  30. 4
    2
      react/features/settings/components/web/video/VideoSettingsContent.js
  31. 21
    4
      react/features/toolbox/components/AudioMuteButton.js
  32. 30
    1
      react/features/toolbox/components/VideoMuteButton.js
  33. 19
    9
      react/features/toolbox/components/web/AudioSettingsButton.js
  34. 19
    8
      react/features/toolbox/components/web/VideoSettingsButton.js
  35. 2
    0
      react/features/toolbox/components/web/index.js
  36. 36
    0
      react/features/toolbox/functions.web.js

+ 200
- 112
conference.js 파일 보기

27
     redirectToStaticPage,
27
     redirectToStaticPage,
28
     reloadWithStoredParams
28
     reloadWithStoredParams
29
 } from './react/features/app';
29
 } from './react/features/app';
30
+import {
31
+    initPrejoin,
32
+    isPrejoinPageEnabled,
33
+    isPrejoinPageVisible,
34
+    replacePrejoinAudioTrack,
35
+    replacePrejoinVideoTrack
36
+} from './react/features/prejoin';
30
 
37
 
31
 import EventEmitter from 'events';
38
 import EventEmitter from 'events';
32
 
39
 
133
 let room;
140
 let room;
134
 let connection;
141
 let connection;
135
 
142
 
143
+/**
144
+ * The promise is used when the prejoin screen is shown.
145
+ * While the user configures the devices the connection can be made.
146
+ *
147
+ * @type {Promise<Object>}
148
+ * @private
149
+ */
150
+let _connectionPromise;
151
+
136
 /**
152
 /**
137
  * This promise is used for chaining mutePresenterVideo calls in order to avoid  calling GUM multiple times if it takes
153
  * This promise is used for chaining mutePresenterVideo calls in order to avoid  calling GUM multiple times if it takes
138
  * a while to finish.
154
  * a while to finish.
471
     localVideo: null,
487
     localVideo: null,
472
 
488
 
473
     /**
489
     /**
474
-     * Creates local media tracks and connects to a room. Will show error
475
-     * dialogs in case accessing the local microphone and/or camera failed. Will
476
-     * show guidance overlay for users on how to give access to camera and/or
477
-     * microphone.
478
-     * @param {string} roomName
479
-     * @param {object} options
480
-     * @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
481
-     * only audio track will be created and the audio only mode will be turned
482
-     * on.
483
-     * @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
484
-     * should start with screensharing instead of camera video.
485
-     * @param {boolean} options.startWithAudioMuted - will start the conference
486
-     * without any audio tracks.
487
-     * @param {boolean} options.startWithVideoMuted - will start the conference
488
-     * without any video tracks.
489
-     * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
490
+     * Returns an object containing a promise which resolves with the created tracks &
491
+     * the errors resulting from that process.
492
+     *
493
+     * @returns {Promise<JitsiLocalTrack[]>, Object}
490
      */
494
      */
491
-    createInitialLocalTracksAndConnect(roomName, options = {}) {
492
-        let audioAndVideoError,
493
-            audioOnlyError,
494
-            screenSharingError,
495
-            videoOnlyError;
495
+    createInitialLocalTracks(options = {}) {
496
+        const errors = {};
496
         const initialDevices = [ 'audio' ];
497
         const initialDevices = [ 'audio' ];
497
         const requestedAudio = true;
498
         const requestedAudio = true;
498
         let requestedVideo = false;
499
         let requestedVideo = false;
524
         // FIXME is there any simpler way to rewrite this spaghetti below ?
525
         // FIXME is there any simpler way to rewrite this spaghetti below ?
525
         if (options.startScreenSharing) {
526
         if (options.startScreenSharing) {
526
             tryCreateLocalTracks = this._createDesktopTrack()
527
             tryCreateLocalTracks = this._createDesktopTrack()
527
-                .then(desktopStream => {
528
+                .then(([ desktopStream ]) => {
528
                     if (!requestedAudio) {
529
                     if (!requestedAudio) {
529
                         return [ desktopStream ];
530
                         return [ desktopStream ];
530
                     }
531
                     }
533
                         .then(([ audioStream ]) =>
534
                         .then(([ audioStream ]) =>
534
                             [ desktopStream, audioStream ])
535
                             [ desktopStream, audioStream ])
535
                         .catch(error => {
536
                         .catch(error => {
536
-                            audioOnlyError = error;
537
+                            errors.audioOnlyError = error;
537
 
538
 
538
                             return [ desktopStream ];
539
                             return [ desktopStream ];
539
                         });
540
                         });
540
                 })
541
                 })
541
                 .catch(error => {
542
                 .catch(error => {
542
                     logger.error('Failed to obtain desktop stream', error);
543
                     logger.error('Failed to obtain desktop stream', error);
543
-                    screenSharingError = error;
544
+                    errors.screenSharingError = error;
544
 
545
 
545
                     return requestedAudio
546
                     return requestedAudio
546
                         ? createLocalTracksF({ devices: [ 'audio' ] }, true)
547
                         ? createLocalTracksF({ devices: [ 'audio' ] }, true)
547
                         : [];
548
                         : [];
548
                 })
549
                 })
549
                 .catch(error => {
550
                 .catch(error => {
550
-                    audioOnlyError = error;
551
+                    errors.audioOnlyError = error;
551
 
552
 
552
                     return [];
553
                     return [];
553
                 });
554
                 });
560
                     if (requestedAudio && requestedVideo) {
561
                     if (requestedAudio && requestedVideo) {
561
 
562
 
562
                         // Try audio only...
563
                         // Try audio only...
563
-                        audioAndVideoError = err;
564
+                        errors.audioAndVideoError = err;
564
 
565
 
565
                         return (
566
                         return (
566
                             createLocalTracksF({ devices: [ 'audio' ] }, true));
567
                             createLocalTracksF({ devices: [ 'audio' ] }, true));
567
                     } else if (requestedAudio && !requestedVideo) {
568
                     } else if (requestedAudio && !requestedVideo) {
568
-                        audioOnlyError = err;
569
+                        errors.audioOnlyError = err;
569
 
570
 
570
                         return [];
571
                         return [];
571
                     } else if (requestedVideo && !requestedAudio) {
572
                     } else if (requestedVideo && !requestedAudio) {
572
-                        videoOnlyError = err;
573
+                        errors.videoOnlyError = err;
573
 
574
 
574
                         return [];
575
                         return [];
575
                     }
576
                     }
580
                     if (!requestedAudio) {
581
                     if (!requestedAudio) {
581
                         logger.error('The impossible just happened', err);
582
                         logger.error('The impossible just happened', err);
582
                     }
583
                     }
583
-                    audioOnlyError = err;
584
+                    errors.audioOnlyError = err;
584
 
585
 
585
                     // Try video only...
586
                     // Try video only...
586
                     return requestedVideo
587
                     return requestedVideo
592
                     if (!requestedVideo) {
593
                     if (!requestedVideo) {
593
                         logger.error('The impossible just happened', err);
594
                         logger.error('The impossible just happened', err);
594
                     }
595
                     }
595
-                    videoOnlyError = err;
596
+                    errors.videoOnlyError = err;
596
 
597
 
597
                     return [];
598
                     return [];
598
                 });
599
                 });
603
         // cases, when auth is rquired, for instance, that won't happen until
604
         // cases, when auth is rquired, for instance, that won't happen until
604
         // the user inputs their credentials, but the dialog would be
605
         // the user inputs their credentials, but the dialog would be
605
         // overshadowed by the overlay.
606
         // overshadowed by the overlay.
606
-        tryCreateLocalTracks.then(() =>
607
-            APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false)));
607
+        tryCreateLocalTracks.then(tracks => {
608
+            APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
609
+
610
+            return tracks;
611
+        });
612
+
613
+        return {
614
+            tryCreateLocalTracks,
615
+            errors
616
+        };
617
+    },
618
+
619
+    /**
620
+     * Creates local media tracks and connects to a room. Will show error
621
+     * dialogs in case accessing the local microphone and/or camera failed. Will
622
+     * show guidance overlay for users on how to give access to camera and/or
623
+     * microphone.
624
+     * @param {string} roomName
625
+     * @param {object} options
626
+     * @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
627
+     * only audio track will be created and the audio only mode will be turned
628
+     * on.
629
+     * @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
630
+     * should start with screensharing instead of camera video.
631
+     * @param {boolean} options.startWithAudioMuted - will start the conference
632
+     * without any audio tracks.
633
+     * @param {boolean} options.startWithVideoMuted - will start the conference
634
+     * without any video tracks.
635
+     * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
636
+     */
637
+    createInitialLocalTracksAndConnect(roomName, options = {}) {
638
+        const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
639
+        const {
640
+            audioAndVideoError,
641
+            audioOnlyError,
642
+            screenSharingError,
643
+            videoOnlyError
644
+        } = errors;
608
 
645
 
609
         return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
646
         return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
610
             .then(([ tracks, con ]) => {
647
             .then(([ tracks, con ]) => {
636
             });
673
             });
637
     },
674
     },
638
 
675
 
676
+    startConference(con, tracks) {
677
+        tracks.forEach(track => {
678
+            if ((track.isAudioTrack() && this.isLocalAudioMuted())
679
+                || (track.isVideoTrack() && this.isLocalVideoMuted())) {
680
+                const mediaType = track.getType();
681
+
682
+                sendAnalytics(
683
+                    createTrackMutedEvent(mediaType, 'initial mute'));
684
+                logger.log(`${mediaType} mute: initially muted.`);
685
+                track.mute();
686
+            }
687
+        });
688
+        logger.log(`Initialized with ${tracks.length} local tracks`);
689
+
690
+        this._localTracksInitialized = true;
691
+        con.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _connectionFailedHandler);
692
+        APP.connection = connection = con;
693
+
694
+        // Desktop sharing related stuff:
695
+        this.isDesktopSharingEnabled
696
+            = JitsiMeetJS.isDesktopSharingEnabled();
697
+        eventEmitter.emit(JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled);
698
+
699
+        APP.store.dispatch(
700
+            setDesktopSharingEnabled(this.isDesktopSharingEnabled));
701
+
702
+        this._createRoom(tracks);
703
+        APP.remoteControl.init();
704
+
705
+        // if user didn't give access to mic or camera or doesn't have
706
+        // them at all, we mark corresponding toolbar buttons as muted,
707
+        // so that the user can try unmute later on and add audio/video
708
+        // to the conference
709
+        if (!tracks.find(t => t.isAudioTrack())) {
710
+            this.setAudioMuteStatus(true);
711
+        }
712
+
713
+        if (!tracks.find(t => t.isVideoTrack())) {
714
+            this.setVideoMuteStatus(true);
715
+        }
716
+
717
+        if (config.iAmRecorder) {
718
+            this.recorder = new Recorder();
719
+        }
720
+
721
+        if (config.startSilent) {
722
+            sendAnalytics(createStartSilentEvent());
723
+            APP.store.dispatch(showNotification({
724
+                descriptionKey: 'notify.startSilentDescription',
725
+                titleKey: 'notify.startSilentTitle'
726
+            }));
727
+        }
728
+
729
+        // XXX The API will take care of disconnecting from the XMPP
730
+        // server (and, thus, leaving the room) on unload.
731
+        return new Promise((resolve, reject) => {
732
+            (new ConferenceConnector(resolve, reject)).connect();
733
+        });
734
+    },
735
+
639
     /**
736
     /**
640
-     * Open new connection and join to the conference.
641
-     * @param {object} options
642
-     * @param {string} roomName - The name of the conference.
737
+     * Open new connection and join the conference when prejoin page is not enabled.
738
+     * If prejoin page is enabled open an new connection in the background
739
+     * and create local tracks.
740
+     *
741
+     * @param {{ roomName: string }} options
643
      * @returns {Promise}
742
      * @returns {Promise}
644
      */
743
      */
645
-    init(options) {
646
-        this.roomName = options.roomName;
744
+    async init({ roomName }) {
745
+        const initialOptions = {
746
+            startAudioOnly: config.startAudioOnly,
747
+            startScreenSharing: config.startScreenSharing,
748
+            startWithAudioMuted: config.startWithAudioMuted
749
+                || config.startSilent
750
+                || isUserInteractionRequiredForUnmute(APP.store.getState()),
751
+            startWithVideoMuted: config.startWithVideoMuted
752
+                || isUserInteractionRequiredForUnmute(APP.store.getState())
753
+        };
647
 
754
 
648
-        window.addEventListener('hashchange', this.onHashChange.bind(this), false);
755
+        this.roomName = roomName;
649
 
756
 
650
-        return (
757
+        window.addEventListener('hashchange', this.onHashChange.bind(this), false);
651
 
758
 
759
+        try {
652
             // Initialize the device list first. This way, when creating tracks
760
             // Initialize the device list first. This way, when creating tracks
653
             // based on preferred devices, loose label matching can be done in
761
             // based on preferred devices, loose label matching can be done in
654
             // cases where the exact ID match is no longer available, such as
762
             // cases where the exact ID match is no longer available, such as
655
             // when the camera device has switched USB ports.
763
             // when the camera device has switched USB ports.
656
             // when in startSilent mode we want to start with audio muted
764
             // when in startSilent mode we want to start with audio muted
657
-            this._initDeviceList()
658
-                .catch(error => logger.warn(
659
-                    'initial device list initialization failed', error))
660
-                .then(() => this.createInitialLocalTracksAndConnect(
661
-                options.roomName, {
662
-                    startAudioOnly: config.startAudioOnly,
663
-                    startScreenSharing: config.startScreenSharing,
664
-                    startWithAudioMuted: config.startWithAudioMuted
665
-                    || config.startSilent
666
-                    || isUserInteractionRequiredForUnmute(APP.store.getState()),
667
-                    startWithVideoMuted: config.startWithVideoMuted
668
-                    || isUserInteractionRequiredForUnmute(APP.store.getState())
669
-                }))
670
-            .then(([ tracks, con ]) => {
671
-                tracks.forEach(track => {
672
-                    if ((track.isAudioTrack() && this.isLocalAudioMuted())
673
-                        || (track.isVideoTrack() && this.isLocalVideoMuted())) {
674
-                        const mediaType = track.getType();
675
-
676
-                        sendAnalytics(
677
-                            createTrackMutedEvent(mediaType, 'initial mute'));
678
-                        logger.log(`${mediaType} mute: initially muted.`);
679
-                        track.mute();
680
-                    }
681
-                });
682
-                logger.log(`initialized with ${tracks.length} local tracks`);
683
-                this._localTracksInitialized = true;
684
-                con.addEventListener(
685
-                    JitsiConnectionEvents.CONNECTION_FAILED,
686
-                    _connectionFailedHandler);
687
-                APP.connection = connection = con;
688
-
689
-                // Desktop sharing related stuff:
690
-                this.isDesktopSharingEnabled
691
-                    = JitsiMeetJS.isDesktopSharingEnabled();
692
-                eventEmitter.emit(
693
-                    JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
694
-                    this.isDesktopSharingEnabled);
765
+            await this._initDeviceList();
766
+        } catch (error) {
767
+            logger.warn('initial device list initialization failed', error);
768
+        }
695
 
769
 
696
-                APP.store.dispatch(
697
-                    setDesktopSharingEnabled(this.isDesktopSharingEnabled));
770
+        if (isPrejoinPageEnabled(APP.store.getState())) {
771
+            _connectionPromise = connect(roomName);
698
 
772
 
699
-                this._createRoom(tracks);
700
-                APP.remoteControl.init();
773
+            const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
774
+            const tracks = await tryCreateLocalTracks;
701
 
775
 
702
-                // if user didn't give access to mic or camera or doesn't have
703
-                // them at all, we mark corresponding toolbar buttons as muted,
704
-                // so that the user can try unmute later on and add audio/video
705
-                // to the conference
706
-                if (!tracks.find(t => t.isAudioTrack())) {
707
-                    this.setAudioMuteStatus(true);
708
-                }
776
+            // Initialize device list a second time to ensure device labels
777
+            // get populated in case of an initial gUM acceptance; otherwise
778
+            // they may remain as empty strings.
779
+            this._initDeviceList(true);
709
 
780
 
710
-                if (!tracks.find(t => t.isVideoTrack())) {
711
-                    this.setVideoMuteStatus(true);
712
-                }
781
+            return APP.store.dispatch(initPrejoin(tracks, errors));
782
+        }
713
 
783
 
714
-                // Initialize device list a second time to ensure device labels
715
-                // get populated in case of an initial gUM acceptance; otherwise
716
-                // they may remain as empty strings.
717
-                this._initDeviceList(true);
784
+        const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(
785
+            roomName, initialOptions);
718
 
786
 
719
-                if (config.iAmRecorder) {
720
-                    this.recorder = new Recorder();
721
-                }
787
+        this._initDeviceList(true);
722
 
788
 
723
-                if (config.startSilent) {
724
-                    sendAnalytics(createStartSilentEvent());
725
-                    APP.store.dispatch(showNotification({
726
-                        descriptionKey: 'notify.startSilentDescription',
727
-                        titleKey: 'notify.startSilentTitle'
728
-                    }));
729
-                }
789
+        return this.startConference(con, tracks);
790
+    },
730
 
791
 
731
-                // XXX The API will take care of disconnecting from the XMPP
732
-                // server (and, thus, leaving the room) on unload.
733
-                return new Promise((resolve, reject) => {
734
-                    (new ConferenceConnector(resolve, reject)).connect();
735
-                });
736
-            })
737
-        );
792
+    /**
793
+     * Joins conference after the tracks have been configured in the prejoin screen.
794
+     *
795
+     * @param {Object[]} tracks - An array with the configured tracks
796
+     * @returns {Promise}
797
+     */
798
+    async prejoinStart(tracks) {
799
+        const con = await _connectionPromise;
800
+
801
+        return this.startConference(con, tracks);
738
     },
802
     },
739
 
803
 
740
     /**
804
     /**
1352
     useVideoStream(newStream) {
1416
     useVideoStream(newStream) {
1353
         return new Promise((resolve, reject) => {
1417
         return new Promise((resolve, reject) => {
1354
             _replaceLocalVideoTrackQueue.enqueue(onFinish => {
1418
             _replaceLocalVideoTrackQueue.enqueue(onFinish => {
1419
+                /**
1420
+                 * When the prejoin page is visible there is no conference object
1421
+                 * created. The prejoin tracks are managed separately,
1422
+                 * so this updates the prejoin video track.
1423
+                 */
1424
+                if (isPrejoinPageVisible(APP.store.getState())) {
1425
+                    return APP.store.dispatch(replacePrejoinVideoTrack(newStream))
1426
+                        .then(resolve)
1427
+                        .catch(reject)
1428
+                        .then(onFinish);
1429
+                }
1430
+
1355
                 APP.store.dispatch(
1431
                 APP.store.dispatch(
1356
                 replaceLocalTrack(this.localVideo, newStream, room))
1432
                 replaceLocalTrack(this.localVideo, newStream, room))
1357
                     .then(() => {
1433
                     .then(() => {
1405
     useAudioStream(newStream) {
1481
     useAudioStream(newStream) {
1406
         return new Promise((resolve, reject) => {
1482
         return new Promise((resolve, reject) => {
1407
             _replaceLocalAudioTrackQueue.enqueue(onFinish => {
1483
             _replaceLocalAudioTrackQueue.enqueue(onFinish => {
1484
+                /**
1485
+                 * When the prejoin page is visible there is no conference object
1486
+                 * created. The prejoin tracks are managed separately,
1487
+                 * so this updates the prejoin audio stream.
1488
+                 */
1489
+                if (isPrejoinPageVisible(APP.store.getState())) {
1490
+                    return APP.store.dispatch(replacePrejoinAudioTrack(newStream))
1491
+                        .then(resolve)
1492
+                        .catch(reject)
1493
+                        .then(onFinish);
1494
+                }
1495
+
1408
                 APP.store.dispatch(
1496
                 APP.store.dispatch(
1409
                 replaceLocalTrack(this.localAudio, newStream, room))
1497
                 replaceLocalTrack(this.localAudio, newStream, room))
1410
                     .then(() => {
1498
                     .then(() => {

+ 3
- 0
config.js 파일 보기

289
     // and microsoftApiApplicationClientID
289
     // and microsoftApiApplicationClientID
290
     // enableCalendarIntegration: false,
290
     // enableCalendarIntegration: false,
291
 
291
 
292
+    // When 'true', it shows an intermediate page before joining, where the user can  configure its devices.
293
+    // prejoinPageEnabled: false,
294
+
292
     // Stats
295
     // Stats
293
     //
296
     //
294
 
297
 

+ 255
- 0
css/_prejoin.scss 파일 보기

1
+.prejoin {
2
+    &-full-page {
3
+        background: #1C2025;
4
+        position: absolute;
5
+        width: 100%;
6
+        height: 100%;
7
+        z-index: $toolbarZ + 1;
8
+    }
9
+
10
+    &-input-area-container {
11
+        position: absolute;
12
+        bottom: 128px;
13
+        width: 100%;
14
+        z-index: 1;
15
+    }
16
+
17
+    &-input-area {
18
+        margin: 0 auto;
19
+        text-align: center;
20
+        width: 320px;
21
+    }
22
+
23
+    &-title {
24
+        color: #fff;
25
+        font-size: 24px;
26
+        line-height: 32px;
27
+        margin-bottom: 16px;
28
+    }
29
+
30
+    &-btn {
31
+        border-radius: 3px;
32
+        color: #fff;
33
+        cursor: pointer;
34
+        display: inline-block;
35
+        font-size: 15px;
36
+        line-height: 24px;
37
+        margin-bottom: 16px;
38
+        padding: 7px 16px;
39
+        text-align: center;
40
+        width: 286px;
41
+
42
+        &--primary {
43
+            background: #0376DA;
44
+            border: 1px solid #0376DA;
45
+        }
46
+
47
+        &--secondary {
48
+            background: #2A3A4B;
49
+            border: 1px solid #5E6D7A;
50
+        }
51
+
52
+        &--text {
53
+            width: auto;
54
+            margin: 0;
55
+            padding: 0;
56
+        }
57
+    }
58
+
59
+    &-text-btns {
60
+        display: flex;
61
+        justify-content: space-between;
62
+    }
63
+
64
+    &-input-label {
65
+        color: #A4B8D1;
66
+        font-size: 13px;
67
+        line-height: 20px;
68
+        margin-top: 32px 0 8px 0;
69
+        text-align: center;
70
+        width: 100%;
71
+    }
72
+}
73
+
74
+@mixin name-placeholder {
75
+    color: #fff;
76
+    font-weight: 300;
77
+    opacity: 0.6;
78
+}
79
+
80
+.prejoin-preview {
81
+    height: 100%;
82
+    position: absolute;
83
+    width: 100%;
84
+
85
+    &--no-video {
86
+        background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
87
+        text-align: center;
88
+    }
89
+
90
+    &-video {
91
+        height: 100%;
92
+        object-fit: cover;
93
+        position: absolute;
94
+        width: 100%;
95
+    }
96
+
97
+    &-name {
98
+        color: #fff;
99
+        font-size: 19px;
100
+        line-height: 28px;
101
+
102
+        &--editable {
103
+            background: none;
104
+            border: 0;
105
+            border-bottom: 1px solid #D1DBE8;
106
+            margin: 24px 0 16px 0;
107
+            outline: none;
108
+            text-align: center;
109
+            width: 100%;
110
+
111
+            &::-webkit-input-placeholder {
112
+                @include name-placeholder;
113
+            }
114
+            &::-moz-placeholder {
115
+                @include name-placeholder;
116
+            }
117
+            &:-ms-input-placeholder {
118
+                @include name-placeholder;
119
+            }
120
+        }
121
+    }
122
+
123
+    &-avatar.avatar {
124
+        background: #A4B8D1;
125
+        margin: 200px auto 0 auto;
126
+    }
127
+
128
+    &-btn-container {
129
+        display: flex;
130
+        justify-content: center;
131
+        position: absolute;
132
+        bottom: 50px;
133
+        width: 100%;
134
+        z-index: 1;
135
+
136
+        &> div {
137
+            margin: 0 12px;
138
+        }
139
+
140
+        .settings-button-small-icon {
141
+            right: -8px;
142
+
143
+            &--hovered {
144
+                right: -10px;
145
+            }
146
+        }
147
+    }
148
+
149
+    &-overlay {
150
+        height: 100%;
151
+        position: absolute;
152
+        width: 100%;
153
+        z-index: 1;
154
+        background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 54.25%);
155
+    }
156
+
157
+    &-status {
158
+        align-items: center;
159
+        bottom: 0;
160
+        color: #fff;
161
+        display: flex;
162
+        font-size: 13px;
163
+        min-height: 24px;
164
+        justify-content: center;
165
+        position: absolute;
166
+        text-align: center;
167
+        width: 100%;
168
+        z-index: 1;
169
+
170
+        &--warning {
171
+            background: rgba(241, 173, 51, 0.5)
172
+        }
173
+        &--ok {
174
+            background: rgba(49, 183, 106, 0.5);
175
+        }
176
+    }
177
+
178
+    &-icon {
179
+        background-position: center;
180
+        background-repeat: no-repeat;
181
+        display: inline-block;
182
+        height: 16px;
183
+        margin-right: 8px;
184
+        width: 16px;
185
+    }
186
+
187
+    &-error-desc {
188
+        margin-right: 4px;
189
+    }
190
+
191
+    .settings-button-container {
192
+        width: 49px;
193
+        margin: 0 8px;
194
+    }
195
+}
196
+
197
+.prejoin-copy {
198
+    &-meeting {
199
+        cursor: pointer;
200
+        color: #fff;
201
+        font-size: 15px;
202
+        font-weight: 300;
203
+        line-height: 24px;
204
+        position: relative;
205
+    }
206
+
207
+    &-url {
208
+        max-width: 278px;
209
+        padding: 8px 10px;
210
+        overflow: hidden;
211
+        text-overflow: ellipsis;
212
+    }
213
+
214
+    &-badge {
215
+        border-radius: 4px;
216
+        height: 100%;
217
+        line-height: 38px;
218
+        position: absolute;
219
+        padding-left: 10px;
220
+        text-align: left;
221
+        top: 0;
222
+        width: 100%;
223
+
224
+        &--hover {
225
+            background: #1C2025;
226
+        }
227
+
228
+        &--done {
229
+            background: #31B76A;
230
+        }
231
+    }
232
+
233
+    &-icon {
234
+        position: absolute;
235
+        right: 8px;
236
+        top: 8px;
237
+
238
+       &--white {
239
+           &> svg > path {
240
+               fill: #fff
241
+           }
242
+       }
243
+
244
+       &--light {
245
+           &> svg > path {
246
+               fill: #D1DBE8;
247
+           }
248
+       }
249
+    }
250
+
251
+    &-textarea {
252
+        position: absolute;
253
+        left: -9999px;
254
+    }
255
+}

+ 1
- 0
css/_settings-button.scss 파일 보기

57
         width: 16px;
57
         width: 16px;
58
 
58
 
59
         &> svg {
59
         &> svg {
60
+            fill: #5e6d7a;
60
             margin-top: 5px;
61
             margin-top: 5px;
61
         }
62
         }
62
 
63
 

+ 6
- 2
css/_video-preview.css 파일 보기

1
 .video-preview {
1
 .video-preview {
2
     background: none;
2
     background: none;
3
     max-height: 290px;
3
     max-height: 290px;
4
-    overflow: auto;
4
+
5
+    &-container {
6
+        overflow: auto;
7
+        padding: 16px;
8
+    }
5
 
9
 
6
     &-entry {
10
     &-entry {
7
         cursor: pointer;
11
         cursor: pointer;
61
     // Override @atlaskit/InlineDialog container which is made with styled components
65
     // Override @atlaskit/InlineDialog container which is made with styled components
62
     & > div > div:nth-child(2) > div > div {
66
     & > div > div:nth-child(2) > div > div {
63
         outline: none;
67
         outline: none;
64
-        padding: 16px;
68
+        padding: 0;
65
     }
69
     }
66
 }
70
 }

+ 1
- 0
css/main.scss 파일 보기

90
 @import 'meter';
90
 @import 'meter';
91
 @import 'audio-preview';
91
 @import 'audio-preview';
92
 @import 'video-preview';
92
 @import 'video-preview';
93
+@import 'prejoin';
93
 
94
 
94
 /* Modules END */
95
 /* Modules END */

+ 27
- 0
lang/main.json 파일 보기

476
     "passwordSetRemotely": "set by another participant",
476
     "passwordSetRemotely": "set by another participant",
477
     "passwordDigitsOnly": "Up to {{number}} digits",
477
     "passwordDigitsOnly": "Up to {{number}} digits",
478
     "poweredby": "powered by",
478
     "poweredby": "powered by",
479
+    "prejoin": {
480
+        "audioAndVideoError": "Audio and video error:",
481
+        "audioOnlyError": "Audio error:",
482
+        "audioTrackError": "Could not create audio track.",
483
+        "callMe": "Call me",
484
+        "callMeAtNumber": "Call me at this number:",
485
+        "configuringDevices": "Configuring devices...",
486
+        "connectedWithAudioQ": "You’re connected with audio?",
487
+        "copyAndShare": "Copy & share meeting link",
488
+        "dialInMeeting": "Dial into the meeting",
489
+        "dialInPin": "Dial into the meeting and enter PIN code:",
490
+        "dialing": "Dialing",
491
+        "iWantToDialIn": "I want to dial in",
492
+        "joinAudioByPhone": "Join with phone audio",
493
+        "joinMeeting": "Join meeting",
494
+        "joinWithoutAudio": "Join without audio",
495
+        "initiated": "Call initiated",
496
+        "linkCopied": "Link copied to clipboard",
497
+        "lookGood": "Speaker and microphone look good",
498
+        "or": "or",
499
+        "calling": "Calling",
500
+        "startWithPhone": "Start with phone audio",
501
+        "screenSharingError": "Screen sharing error:",
502
+        "videoOnlyError": "Video error:",
503
+        "videoTrackError": "Could not create video track.",
504
+        "viewAllNumbers": "view all numbers"
505
+    },
479
     "presenceStatus": {
506
     "presenceStatus": {
480
         "busy": "Busy",
507
         "busy": "Busy",
481
         "calling": "Calling...",
508
         "calling": "Calling...",

+ 10
- 0
react/features/base/conference/functions.js 파일 보기

246
     return selectedLevel;
246
     return selectedLevel;
247
 }
247
 }
248
 
248
 
249
+/**
250
+ * Returns the stored room name.
251
+ *
252
+ * @param {Object} state - The current state of the app.
253
+ * @returns {string}
254
+ */
255
+export function getRoomName(state: Object): string {
256
+    return state['features/base/conference'].room;
257
+}
258
+
249
 /**
259
 /**
250
  * Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
260
  * Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
251
  * manipulating a conference participant (e.g. Pin or select participant).
261
  * manipulating a conference participant (e.g. Pin or select participant).

+ 1
- 0
react/features/base/config/configWhitelist.js 파일 보기

131
     'p2p',
131
     'p2p',
132
     'pcStatsInterval',
132
     'pcStatsInterval',
133
     'preferH264',
133
     'preferH264',
134
+    'prejoinPageEnabled',
134
     'requireDisplayName',
135
     'requireDisplayName',
135
     'remoteVideoMenu',
136
     'remoteVideoMenu',
136
     'resolution',
137
     'resolution',

+ 12
- 3
react/features/base/devices/middleware.js 파일 보기

18
     SET_AUDIO_INPUT_DEVICE,
18
     SET_AUDIO_INPUT_DEVICE,
19
     SET_VIDEO_INPUT_DEVICE
19
     SET_VIDEO_INPUT_DEVICE
20
 } from './actionTypes';
20
 } from './actionTypes';
21
+import { replaceAudioTrackById, replaceVideoTrackById } from '../../prejoin/actions';
22
+import { isPrejoinPageVisible } from '../../prejoin/functions';
21
 import { showNotification, showWarningNotification } from '../../notifications';
23
 import { showNotification, showWarningNotification } from '../../notifications';
22
 import { updateSettings } from '../settings';
24
 import { updateSettings } from '../settings';
23
 import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
25
 import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
98
         break;
100
         break;
99
     }
101
     }
100
     case SET_AUDIO_INPUT_DEVICE:
102
     case SET_AUDIO_INPUT_DEVICE:
101
-        APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
103
+        if (isPrejoinPageVisible(store.getState())) {
104
+            store.dispatch(replaceAudioTrackById(action.deviceId));
105
+        } else {
106
+            APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
107
+        }
102
         break;
108
         break;
103
     case SET_VIDEO_INPUT_DEVICE:
109
     case SET_VIDEO_INPUT_DEVICE:
104
-        APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
110
+        if (isPrejoinPageVisible(store.getState())) {
111
+            store.dispatch(replaceVideoTrackById(action.deviceId));
112
+        } else {
113
+            APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
114
+        }
105
         break;
115
         break;
106
     case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
116
     case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
107
         _checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
117
         _checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
111
     return next(action);
121
     return next(action);
112
 });
122
 });
113
 
123
 
114
-
115
 /**
124
 /**
116
  * Does extra sync up on properties that may need to be updated after the
125
  * Does extra sync up on properties that may need to be updated after the
117
  * conference was joined.
126
  * conference was joined.

+ 3
- 0
react/features/base/icons/svg/arrow-left.svg 파일 보기

1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5223 18.2701C16.9328 18.6395 16.9661 19.2718 16.5966 19.6823C16.2272 20.0928 15.5949 20.1261 15.1844 19.7567L7.31769 12.6767C6.87631 12.2794 6.87631 11.5873 7.31769 11.1901L15.1844 4.11007C15.5949 3.74061 16.2272 3.77389 16.5966 4.1844C16.9661 4.59491 16.9328 5.2272 16.5223 5.59666L9.4815 11.9334L16.5223 18.2701Z" fill="#A4B8D1"/>
3
+</svg>

+ 1
- 1
react/features/base/icons/svg/arrow_down.svg 파일 보기

1
 <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
1
 <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
2
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z" fill="#5E6D7A"/>
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z"/>
3
 </svg>
3
 </svg>

+ 3
- 0
react/features/base/icons/svg/close-x.svg 파일 보기

1
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.41416 8L15.071 2.34315C15.4615 1.95262 15.4615 1.31946 15.071 0.928933C14.6805 0.538409 14.0473 0.538409 13.6568 0.928933L7.99995 6.58579L2.34309 0.928933C1.95257 0.538409 1.3194 0.538409 0.92888 0.928933C0.538355 1.31946 0.538355 1.95262 0.92888 2.34315L6.58573 8L0.92888 13.6569C0.538355 14.0474 0.538355 14.6805 0.92888 15.0711C1.3194 15.4616 1.95257 15.4616 2.34309 15.0711L7.99995 9.41421L13.6568 15.0711C14.0473 15.4616 14.6805 15.4616 15.071 15.0711C15.4615 14.6805 15.4615 14.0474 15.071 13.6569L9.41416 8Z" />
3
+</svg>

+ 2
- 0
react/features/base/icons/svg/index.js 파일 보기

4
 export { default as IconAddPeople } from './link.svg';
4
 export { default as IconAddPeople } from './link.svg';
5
 export { default as IconArrowBack } from './arrow_back.svg';
5
 export { default as IconArrowBack } from './arrow_back.svg';
6
 export { default as IconArrowDown } from './arrow_down.svg';
6
 export { default as IconArrowDown } from './arrow_down.svg';
7
+export { default as IconArrowLeft } from './arrow-left.svg';
7
 export { default as IconAudioOnly } from './visibility.svg';
8
 export { default as IconAudioOnly } from './visibility.svg';
8
 export { default as IconAudioOnlyOff } from './visibility-off.svg';
9
 export { default as IconAudioOnlyOff } from './visibility-off.svg';
9
 export { default as IconAudioRoute } from './volume.svg';
10
 export { default as IconAudioRoute } from './volume.svg';
16
 export { default as IconChatUnread } from './chat-unread.svg';
17
 export { default as IconChatUnread } from './chat-unread.svg';
17
 export { default as IconCheck } from './check.svg';
18
 export { default as IconCheck } from './check.svg';
18
 export { default as IconClose } from './close.svg';
19
 export { default as IconClose } from './close.svg';
20
+export { default as IconCloseX } from './close-x.svg';
19
 export { default as IconClosedCaption } from './closed_caption.svg';
21
 export { default as IconClosedCaption } from './closed_caption.svg';
20
 export { default as IconConnectionActive } from './gsm-bars.svg';
22
 export { default as IconConnectionActive } from './gsm-bars.svg';
21
 export { default as IconConnectionInactive } from './ninja.svg';
23
 export { default as IconConnectionInactive } from './ninja.svg';

+ 19
- 4
react/features/conference/components/web/Conference.js 파일 보기

13
 import { Filmstrip } from '../../../filmstrip';
13
 import { Filmstrip } from '../../../filmstrip';
14
 import { CalleeInfoContainer } from '../../../invite';
14
 import { CalleeInfoContainer } from '../../../invite';
15
 import { LargeVideo } from '../../../large-video';
15
 import { LargeVideo } from '../../../large-video';
16
+import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
16
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
17
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
17
 
18
 
18
 import {
19
 import {
84
      */
85
      */
85
     _roomName: string,
86
     _roomName: string,
86
 
87
 
88
+    /**
89
+     * If prejoin page is visible or not.
90
+     */
91
+    _showPrejoin: boolean,
92
+
87
     dispatch: Function,
93
     dispatch: Function,
88
     t: Function
94
     t: Function
89
 }
95
 }
178
             // interfaceConfig is obsolete but legacy support is required.
184
             // interfaceConfig is obsolete but legacy support is required.
179
             filmStripOnly: filmstripOnly
185
             filmStripOnly: filmstripOnly
180
         } = interfaceConfig;
186
         } = interfaceConfig;
187
+        const {
188
+            _iAmRecorder,
189
+            _layoutClassName,
190
+            _showPrejoin
191
+        } = this.props;
181
         const hideVideoQualityLabel
192
         const hideVideoQualityLabel
182
             = filmstripOnly
193
             = filmstripOnly
183
                 || VIDEO_QUALITY_LABEL_DISABLED
194
                 || VIDEO_QUALITY_LABEL_DISABLED
184
-                || this.props._iAmRecorder;
195
+                || _iAmRecorder;
185
 
196
 
186
         return (
197
         return (
187
             <div
198
             <div
188
-                className = { this.props._layoutClassName }
199
+                className = { _layoutClassName }
189
                 id = 'videoconference_page'
200
                 id = 'videoconference_page'
190
                 onMouseMove = { this._onShowToolbar }>
201
                 onMouseMove = { this._onShowToolbar }>
202
+
191
                 <Notice />
203
                 <Notice />
192
                 <Subject />
204
                 <Subject />
193
                 <div id = 'videospace'>
205
                 <div id = 'videospace'>
197
                     <Filmstrip filmstripOnly = { filmstripOnly } />
209
                     <Filmstrip filmstripOnly = { filmstripOnly } />
198
                 </div>
210
                 </div>
199
 
211
 
200
-                { filmstripOnly || <Toolbox /> }
212
+                { filmstripOnly || _showPrejoin || <Toolbox /> }
201
                 { filmstripOnly || <Chat /> }
213
                 { filmstripOnly || <Chat /> }
202
 
214
 
203
                 { this.renderNotificationsContainer() }
215
                 { this.renderNotificationsContainer() }
204
 
216
 
217
+                { !filmstripOnly && _showPrejoin && <Prejoin />}
218
+
205
                 <CalleeInfoContainer />
219
                 <CalleeInfoContainer />
206
             </div>
220
             </div>
207
         );
221
         );
268
         ...abstractMapStateToProps(state),
282
         ...abstractMapStateToProps(state),
269
         _iAmRecorder: state['features/base/config'].iAmRecorder,
283
         _iAmRecorder: state['features/base/config'].iAmRecorder,
270
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
284
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
271
-        _roomName: getConferenceNameForTitle(state)
285
+        _roomName: getConferenceNameForTitle(state),
286
+        _showPrejoin: isPrejoinPageVisible(state)
272
     };
287
     };
273
 }
288
 }
274
 
289
 

+ 11
- 2
react/features/invite/functions.js 파일 보기

298
  */
298
  */
299
 export function isAddPeopleEnabled(state: Object): boolean {
299
 export function isAddPeopleEnabled(state: Object): boolean {
300
     const { peopleSearchUrl } = state['features/base/config'];
300
     const { peopleSearchUrl } = state['features/base/config'];
301
-    const { isGuest } = state['features/base/jwt'];
302
 
301
 
303
-    return !isGuest && Boolean(peopleSearchUrl);
302
+    return !isGuest(state) && Boolean(peopleSearchUrl);
304
 }
303
 }
305
 
304
 
306
 /**
305
 /**
316
         && conference && conference.isSIPCallingSupported();
315
         && conference && conference.isSIPCallingSupported();
317
 }
316
 }
318
 
317
 
318
+/**
319
+ * Determines if the current user is guest or not.
320
+ *
321
+ * @param {Object} state - Current state.
322
+ * @returns {boolean}
323
+ */
324
+export function isGuest(state: Object): boolean {
325
+    return state['features/base/jwt'].isGuest;
326
+}
327
+
319
 /**
328
 /**
320
  * Checks whether a string looks like it could be for a phone number.
329
  * Checks whether a string looks like it could be for a phone number.
321
  *
330
  *

+ 65
- 0
react/features/prejoin/actionTypes.js 파일 보기

1
+/**
2
+ * Action type to add a video track to the store.
3
+ */
4
+export const ADD_PREJOIN_VIDEO_TRACK = 'ADD_PREJOIN_VIDEO_TRACK';
5
+
6
+/**
7
+ * Action type to add an audio track to the store.
8
+ */
9
+export const ADD_PREJOIN_AUDIO_TRACK = 'ADD_PREJOIN_AUDIO_TRACK';
10
+
11
+/**
12
+ * Action type to add a content sharing track to the store.
13
+ */
14
+export const ADD_PREJOIN_CONTENT_SHARING_TRACK
15
+    = 'ADD_PREJOIN_CONTENT_SHARING_TRACK';
16
+
17
+/**
18
+ * Action type to signal the start of the conference.
19
+ */
20
+export const PREJOIN_START_CONFERENCE = 'PREJOIN_START_CONFERENCE';
21
+
22
+/**
23
+ * Action type to set the status of the device.
24
+ */
25
+export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
26
+
27
+/**
28
+ * Action type to set the visiblity of the 'JoinByPhone' dialog.
29
+ */
30
+export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
31
+
32
+/**
33
+ * Action type to disable the audio while on prejoin page.
34
+ */
35
+export const SET_PREJOIN_AUDIO_DISABLED = 'SET_PREJOIN_AUDIO_DISABLED';
36
+
37
+/**
38
+ * Action type to mute/unmute the audio while on prejoin page.
39
+ */
40
+export const SET_PREJOIN_AUDIO_MUTED = 'SET_PREJOIN_AUDIO_MUTED';
41
+
42
+/**
43
+ * Action type to set the errors while creating the prejoin streams.
44
+ */
45
+export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS';
46
+
47
+/**
48
+ * Action type to set the name of the user.
49
+ */
50
+export const SET_PREJOIN_NAME = 'SET_PREJOIN_NAME';
51
+
52
+/**
53
+ * Action type to set the visibility of the prejoin page.
54
+ */
55
+export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY';
56
+
57
+/**
58
+ * Action type to mute/unmute the video while on prejoin page.
59
+ */
60
+export const SET_PREJOIN_VIDEO_DISABLED = 'SET_PREJOIN_VIDEO_DISABLED';
61
+
62
+/**
63
+ * Action type to mute/unmute the video while on prejoin page.
64
+ */
65
+export const SET_PREJOIN_VIDEO_MUTED = 'SET_PREJOIN_VIDEO_MUTED';

+ 338
- 0
react/features/prejoin/actions.js 파일 보기

1
+// @flow
2
+
3
+import {
4
+    ADD_PREJOIN_AUDIO_TRACK,
5
+    ADD_PREJOIN_CONTENT_SHARING_TRACK,
6
+    ADD_PREJOIN_VIDEO_TRACK,
7
+    PREJOIN_START_CONFERENCE,
8
+    SET_DEVICE_STATUS,
9
+    SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
10
+    SET_PREJOIN_AUDIO_DISABLED,
11
+    SET_PREJOIN_AUDIO_MUTED,
12
+    SET_PREJOIN_DEVICE_ERRORS,
13
+    SET_PREJOIN_NAME,
14
+    SET_PREJOIN_PAGE_VISIBILITY,
15
+    SET_PREJOIN_VIDEO_DISABLED,
16
+    SET_PREJOIN_VIDEO_MUTED
17
+} from './actionTypes';
18
+import { createLocalTrack } from '../base/lib-jitsi-meet';
19
+import { getAudioTrack, getVideoTrack } from './functions';
20
+import logger from './logger';
21
+
22
+/**
23
+ * Action used to add an audio track to the store.
24
+ *
25
+ * @param {Object} value - The track to be added.
26
+ * @returns {Object}
27
+ */
28
+export function addPrejoinAudioTrack(value: Object) {
29
+    return {
30
+        type: ADD_PREJOIN_AUDIO_TRACK,
31
+        value
32
+    };
33
+}
34
+
35
+/**
36
+ * Action used to add a video track to the store.
37
+ *
38
+ * @param {Object} value - The track to be added.
39
+ * @returns {Object}
40
+ */
41
+export function addPrejoinVideoTrack(value: Object) {
42
+    return {
43
+        type: ADD_PREJOIN_VIDEO_TRACK,
44
+        value
45
+    };
46
+}
47
+
48
+/**
49
+ * Action used to add a content sharing track to the store.
50
+ *
51
+ * @param {Object} value - The track to be added.
52
+ * @returns {Object}
53
+ */
54
+export function addPrejoinContentSharingTrack(value: Object) {
55
+    return {
56
+        type: ADD_PREJOIN_CONTENT_SHARING_TRACK,
57
+        value
58
+    };
59
+}
60
+
61
+/**
62
+ * Adds all the newly created tracks to store on init.
63
+ *
64
+ * @param {Object[]} tracks - The newly created tracks.
65
+ * @param {Object} errors - The errors from creating the tracks.
66
+ *
67
+ * @returns {Function}
68
+ */
69
+export function initPrejoin(tracks: Object[], errors: Object) {
70
+    return async function(dispatch: Function) {
71
+        const audioTrack = tracks.find(t => t.isAudioTrack());
72
+        const videoTrack = tracks.find(t => t.isVideoTrack());
73
+
74
+        dispatch(setPrejoinDeviceErrors(errors));
75
+
76
+        if (audioTrack) {
77
+            dispatch(addPrejoinAudioTrack(audioTrack));
78
+        } else {
79
+            dispatch(setAudioDisabled());
80
+        }
81
+
82
+        if (videoTrack) {
83
+            if (videoTrack.videoType === 'desktop') {
84
+                dispatch(addPrejoinContentSharingTrack(videoTrack));
85
+                dispatch(setPrejoinVideoDisabled(true));
86
+            } else {
87
+                dispatch(addPrejoinVideoTrack(videoTrack));
88
+            }
89
+        } else {
90
+            dispatch(setPrejoinVideoDisabled(true));
91
+        }
92
+    };
93
+}
94
+
95
+/**
96
+ * Joins the conference.
97
+ *
98
+ * @returns {Function}
99
+ */
100
+export function joinConference() {
101
+    return function(dispatch: Function) {
102
+        dispatch(setPrejoinPageVisibility(false));
103
+        dispatch(startConference());
104
+    };
105
+}
106
+
107
+/**
108
+ * Joins the conference without audio.
109
+ *
110
+ * @returns {Function}
111
+ */
112
+export function joinConferenceWithoutAudio() {
113
+    return async function(dispatch: Function, getState: Function) {
114
+        const audioTrack = getAudioTrack(getState());
115
+
116
+        if (audioTrack) {
117
+            await dispatch(replacePrejoinAudioTrack(null));
118
+        }
119
+        dispatch(setAudioDisabled());
120
+        dispatch(joinConference());
121
+    };
122
+}
123
+
124
+/**
125
+ * Replaces the existing audio track with a new one.
126
+ *
127
+ * @param {Object} track - The new track.
128
+ * @returns {Function}
129
+ */
130
+export function replacePrejoinAudioTrack(track: Object) {
131
+    return async (dispatch: Function, getState: Function) => {
132
+        const oldTrack = getAudioTrack(getState());
133
+
134
+        oldTrack && await oldTrack.dispose();
135
+        dispatch(addPrejoinAudioTrack(track));
136
+    };
137
+}
138
+
139
+/**
140
+ * Creates a new audio track based on a device id and replaces the current one.
141
+ *
142
+ * @param {string} deviceId - The deviceId of the microphone.
143
+ * @returns {Function}
144
+ */
145
+export function replaceAudioTrackById(deviceId: string) {
146
+    return async (dispatch: Function) => {
147
+        try {
148
+            const track = await createLocalTrack('audio', deviceId);
149
+
150
+            dispatch(replacePrejoinAudioTrack(track));
151
+        } catch (err) {
152
+            dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
153
+            logger.log('Error replacing audio track', err);
154
+        }
155
+    };
156
+}
157
+
158
+/**
159
+ * Replaces the existing video track with a new one.
160
+ *
161
+ * @param {Object} track - The new track.
162
+ * @returns {Function}
163
+ */
164
+export function replacePrejoinVideoTrack(track: Object) {
165
+    return async (dispatch: Function, getState: Function) => {
166
+        const oldTrack = getVideoTrack(getState());
167
+
168
+        oldTrack && await oldTrack.dispose();
169
+        dispatch(addPrejoinVideoTrack(track));
170
+    };
171
+}
172
+
173
+/**
174
+ * Creates a new video track based on a device id and replaces the current one.
175
+ *
176
+ * @param {string} deviceId - The deviceId of the camera.
177
+ * @returns {Function}
178
+ */
179
+export function replaceVideoTrackById(deviceId: Object) {
180
+    return async (dispatch: Function) => {
181
+        try {
182
+            const track = await createLocalTrack('video', deviceId);
183
+
184
+            dispatch(replacePrejoinVideoTrack(track));
185
+        } catch (err) {
186
+            dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
187
+            logger.log('Error replacing video track', err);
188
+        }
189
+    };
190
+}
191
+
192
+
193
+/**
194
+ * Action used to mark audio muted.
195
+ *
196
+ * @param {boolean} value - True for muted.
197
+ * @returns {Object}
198
+ */
199
+export function setPrejoinAudioMuted(value: boolean) {
200
+    return {
201
+        type: SET_PREJOIN_AUDIO_MUTED,
202
+        value
203
+    };
204
+}
205
+
206
+/**
207
+ * Action used to mark video disabled.
208
+ *
209
+ * @param {boolean} value - True for muted.
210
+ * @returns {Object}
211
+ */
212
+export function setPrejoinVideoDisabled(value: boolean) {
213
+    return {
214
+        type: SET_PREJOIN_VIDEO_DISABLED,
215
+        value
216
+    };
217
+}
218
+
219
+
220
+/**
221
+ * Action used to mark video muted.
222
+ *
223
+ * @param {boolean} value - True for muted.
224
+ * @returns {Object}
225
+ */
226
+export function setPrejoinVideoMuted(value: boolean) {
227
+    return {
228
+        type: SET_PREJOIN_VIDEO_MUTED,
229
+        value
230
+    };
231
+}
232
+
233
+/**
234
+ * Action used to mark audio as disabled.
235
+ *
236
+ * @returns {Object}
237
+ */
238
+export function setAudioDisabled() {
239
+    return {
240
+        type: SET_PREJOIN_AUDIO_DISABLED
241
+    };
242
+}
243
+
244
+/**
245
+ * Sets the device status as OK with the corresponding text.
246
+ *
247
+ * @param {string} deviceStatusText - The text to be set.
248
+ * @returns {Object}
249
+ */
250
+export function setDeviceStatusOk(deviceStatusText: string) {
251
+    return {
252
+        type: SET_DEVICE_STATUS,
253
+        value: {
254
+            deviceStatusText,
255
+            deviceStatusType: 'ok'
256
+        }
257
+    };
258
+}
259
+
260
+/**
261
+ * Sets the device status as 'warning' with the corresponding text.
262
+ *
263
+ * @param {string} deviceStatusText - The text to be set.
264
+ * @returns {Object}
265
+ */
266
+export function setDeviceStatusWarning(deviceStatusText: string) {
267
+    return {
268
+        type: SET_DEVICE_STATUS,
269
+        value: {
270
+            deviceStatusText,
271
+            deviceStatusType: 'warning'
272
+        }
273
+    };
274
+}
275
+
276
+
277
+/**
278
+ * Action used to set the visiblitiy of the 'JoinByPhoneDialog'.
279
+ *
280
+ * @param {boolean} value - The value.
281
+ * @returns {Object}
282
+ */
283
+export function setJoinByPhoneDialogVisiblity(value: boolean) {
284
+    return {
285
+        type: SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
286
+        value
287
+    };
288
+}
289
+
290
+/**
291
+ * Action used to set the initial errors after creating the tracks.
292
+ *
293
+ * @param {Object} value - The track errors.
294
+ * @returns {Object}
295
+ */
296
+export function setPrejoinDeviceErrors(value: Object) {
297
+    return {
298
+        type: SET_PREJOIN_DEVICE_ERRORS,
299
+        value
300
+    };
301
+}
302
+
303
+/**
304
+ * Action used to set the name of the guest user.
305
+ *
306
+ * @param {string} value - The name.
307
+ * @returns {Object}
308
+ */
309
+export function setPrejoinName(value: string) {
310
+    return {
311
+        type: SET_PREJOIN_NAME,
312
+        value
313
+    };
314
+}
315
+
316
+/**
317
+ * Action used to set the visiblity of the prejoin page.
318
+ *
319
+ * @param {boolean} value - The value.
320
+ * @returns {Object}
321
+ */
322
+export function setPrejoinPageVisibility(value: boolean) {
323
+    return {
324
+        type: SET_PREJOIN_PAGE_VISIBILITY,
325
+        value
326
+    };
327
+}
328
+
329
+/**
330
+ * Action used to mark the start of the conference.
331
+ *
332
+ * @returns {Object}
333
+ */
334
+function startConference() {
335
+    return {
336
+        type: PREJOIN_START_CONFERENCE
337
+    };
338
+}

+ 197
- 0
react/features/prejoin/components/Prejoin.js 파일 보기

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import {
5
+    joinConference as joinConferenceAction,
6
+    joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
7
+    setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction,
8
+    setPrejoinName
9
+} from '../actions';
10
+import { getRoomName } from '../../base/conference';
11
+import { translate } from '../../base/i18n';
12
+import { connect } from '../../base/redux';
13
+import ActionButton from './buttons/ActionButton';
14
+import {
15
+    areJoinByPhoneButtonsVisible,
16
+    getPrejoinName,
17
+    isDeviceStatusVisible,
18
+    isJoinByPhoneDialogVisible
19
+} from '../functions';
20
+import { isGuest } from '../../invite';
21
+import CopyMeetingUrl from './preview/CopyMeetingUrl';
22
+import DeviceStatus from './preview/DeviceStatus';
23
+import ParticipantName from './preview/ParticipantName';
24
+import Preview from './preview/Preview';
25
+import { VideoSettingsButton, AudioSettingsButton } from '../../toolbox';
26
+
27
+type Props = {
28
+
29
+    /**
30
+     * Flag signaling if the device status is visible or not.
31
+     */
32
+    deviceStatusVisible: boolean,
33
+
34
+    /**
35
+     * Flag signaling if a user is logged in or not.
36
+     */
37
+    isAnonymousUser: boolean,
38
+
39
+    /**
40
+     * Joins the current meeting.
41
+     */
42
+    joinConference: Function,
43
+
44
+    /**
45
+     * Joins the current meeting without audio.
46
+     */
47
+    joinConferenceWithoutAudio: Function,
48
+
49
+    /**
50
+     * The name of the user that is about to join.
51
+     */
52
+    name: string,
53
+
54
+    /**
55
+     * Sets the name for the joining user.
56
+     */
57
+    setName: Function,
58
+
59
+    /**
60
+     * The name of the meeting that is about to be joined.
61
+     */
62
+    roomName: string,
63
+
64
+    /**
65
+     * Sets visibilit of the 'JoinByPhoneDialog'.
66
+     */
67
+    setJoinByPhoneDialogVisiblity: Function,
68
+
69
+    /**
70
+     * If 'JoinByPhoneDialog' is visible or not.
71
+     */
72
+    showDialog: boolean,
73
+
74
+    /**
75
+     * If join by phone buttons should be visible.
76
+     */
77
+    showJoinByPhoneButtons: boolean,
78
+
79
+    /**
80
+     * Used for translation.
81
+     */
82
+    t: Function,
83
+};
84
+
85
+/**
86
+ * This component is displayed before joining a meeting.
87
+ */
88
+class Prejoin extends Component<Props> {
89
+    /**
90
+     * Initializes a new {@code Prejoin} instance.
91
+     *
92
+     * @inheritdoc
93
+     */
94
+    constructor(props) {
95
+        super(props);
96
+
97
+        this._showDialog = this._showDialog.bind(this);
98
+    }
99
+
100
+    _showDialog: () => void;
101
+
102
+    /**
103
+     * Displays the dialog for joining a meeting by phone.
104
+     *
105
+     * @returns {undefined}
106
+     */
107
+    _showDialog() {
108
+        this.props.setJoinByPhoneDialogVisiblity(true);
109
+    }
110
+
111
+    /**
112
+     * Implements React's {@link Component#render()}.
113
+     *
114
+     * @inheritdoc
115
+     * @returns {ReactElement}
116
+     */
117
+    render() {
118
+        const {
119
+            deviceStatusVisible,
120
+            isAnonymousUser,
121
+            joinConference,
122
+            joinConferenceWithoutAudio,
123
+            name,
124
+            setName,
125
+            showJoinByPhoneButtons,
126
+            t
127
+        } = this.props;
128
+        const { _showDialog } = this;
129
+
130
+        return (
131
+            <div className = 'prejoin-full-page'>
132
+                <Preview />
133
+                <div className = 'prejoin-input-area-container'>
134
+                    <div className = 'prejoin-input-area'>
135
+                        <div className = 'prejoin-title'>
136
+                            {t('prejoin.joinMeeting')}
137
+                        </div>
138
+                        <CopyMeetingUrl />
139
+                        <ParticipantName
140
+                            isEditable = { isAnonymousUser }
141
+                            setName = { setName }
142
+                            value = { name } />
143
+                        <ActionButton
144
+                            onClick = { joinConference }
145
+                            type = 'primary'>
146
+                            { t('calendarSync.join') }
147
+                        </ActionButton>
148
+                        {showJoinByPhoneButtons
149
+                            && <div className = 'prejoin-text-btns'>
150
+                                <ActionButton
151
+                                    onClick = { joinConferenceWithoutAudio }
152
+                                    type = 'text'>
153
+                                    { t('prejoin.joinWithoutAudio') }
154
+                                </ActionButton>
155
+                                <ActionButton
156
+                                    onClick = { _showDialog }
157
+                                    type = 'text'>
158
+                                    { t('prejoin.joinAudioByPhone') }
159
+                                </ActionButton>
160
+                            </div>}
161
+                    </div>
162
+                </div>
163
+                <div className = 'prejoin-preview-btn-container'>
164
+                    <AudioSettingsButton visible = { true } />
165
+                    <VideoSettingsButton visible = { true } />
166
+                </div>
167
+                { deviceStatusVisible && <DeviceStatus /> }
168
+            </div>
169
+        );
170
+    }
171
+}
172
+
173
+/**
174
+ * Maps (parts of) the redux state to the React {@code Component} props.
175
+ *
176
+ * @param {Object} state - The redux state.
177
+ * @returns {Object}
178
+ */
179
+function mapStateToProps(state): Object {
180
+    return {
181
+        isAnonymousUser: isGuest(state),
182
+        deviceStatusVisible: isDeviceStatusVisible(state),
183
+        name: getPrejoinName(state),
184
+        roomName: getRoomName(state),
185
+        showDialog: isJoinByPhoneDialogVisible(state),
186
+        showJoinByPhoneButtons: areJoinByPhoneButtonsVisible(state)
187
+    };
188
+}
189
+
190
+const mapDispatchToProps = {
191
+    joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
192
+    joinConference: joinConferenceAction,
193
+    setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
194
+    setName: setPrejoinName
195
+};
196
+
197
+export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin));

+ 51
- 0
react/features/prejoin/components/buttons/ActionButton.js 파일 보기

1
+// @flow
2
+
3
+import React from 'react';
4
+const classNameByType = {
5
+    primary: 'prejoin-btn--primary',
6
+    secondary: 'prejoin-btn--secondary',
7
+    text: 'prejoin-btn--text'
8
+};
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * Text of the button.
14
+     */
15
+    children: React$Node,
16
+
17
+    /**
18
+     * Text css class of the button.
19
+     */
20
+    className?: string,
21
+
22
+    /**
23
+     * The type of th button: primary, secondary, text.
24
+     */
25
+    type: string,
26
+
27
+    /**
28
+     * OnClick button handler.
29
+     */
30
+    onClick: Function,
31
+};
32
+
33
+/**
34
+ * Button used for prejoin actions: Join/Join without audio/Join by phone.
35
+ *
36
+ * @returns {ReactElement}
37
+ */
38
+function ActionButton({ children, className, type, onClick }: Props) {
39
+    const ownClassName = `prejoin-btn ${classNameByType[type]}`;
40
+    const cls = className ? `${className} ${ownClassName}` : ownClassName;
41
+
42
+    return (
43
+        <div
44
+            className = { cls }
45
+            onClick = { onClick }>
46
+            {children}
47
+        </div>
48
+    );
49
+}
50
+
51
+export default ActionButton;

+ 197
- 0
react/features/prejoin/components/preview/CopyMeetingUrl.js 파일 보기

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from '../../../base/redux';
5
+import { translate } from '../../../base/i18n';
6
+import { getCurrentConferenceUrl } from '../../../base/connection';
7
+import { Icon, IconCopy, IconCheck } from '../../../base/icons';
8
+import logger from '../../logger';
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * The meeting url.
14
+     */
15
+    url: string,
16
+
17
+    /**
18
+     * Used for translation.
19
+     */
20
+    t: Function
21
+};
22
+
23
+type State = {
24
+
25
+    /**
26
+     * If true it shows the 'copy link' message.
27
+     */
28
+    showCopyLink: boolean,
29
+
30
+    /**
31
+     * If true it shows the 'link copied' message.
32
+     */
33
+    showLinkCopied: boolean,
34
+};
35
+
36
+const COPY_TIMEOUT = 2000;
37
+
38
+/**
39
+ * Component used to copy meeting url on prejoin page.
40
+ */
41
+class CopyMeetingUrl extends Component<Props, State> {
42
+
43
+    textarea: Object;
44
+
45
+    /**
46
+     * Initializes a new {@code Prejoin} instance.
47
+     *
48
+     * @inheritdoc
49
+     */
50
+    constructor(props) {
51
+        super(props);
52
+
53
+        this.textarea = React.createRef();
54
+        this.state = {
55
+            showCopyLink: false,
56
+            showLinkCopied: false
57
+        };
58
+        this._copyUrl = this._copyUrl.bind(this);
59
+        this._hideCopyLink = this._hideCopyLink.bind(this);
60
+        this._hideLinkCopied = this._hideLinkCopied.bind(this);
61
+        this._showCopyLink = this._showCopyLink.bind(this);
62
+        this._showLinkCopied = this._showLinkCopied.bind(this);
63
+    }
64
+
65
+    _copyUrl: () => void;
66
+
67
+    /**
68
+     * Callback invoked to copy the url to clipboard.
69
+     *
70
+     * @returns {void}
71
+     */
72
+    _copyUrl() {
73
+        const textarea = this.textarea.current;
74
+
75
+        try {
76
+            textarea.select();
77
+            document.execCommand('copy');
78
+            textarea.blur();
79
+            this._showLinkCopied();
80
+            window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
81
+        } catch (err) {
82
+            logger.error('error when copying the meeting url');
83
+        }
84
+    }
85
+
86
+    _hideLinkCopied: () => void;
87
+
88
+    /**
89
+     * Hides the 'Link copied' message.
90
+     *
91
+     * @private
92
+     * @returns {void}
93
+     */
94
+    _hideLinkCopied() {
95
+        this.setState({
96
+            showLinkCopied: false
97
+        });
98
+    }
99
+
100
+    _hideCopyLink: () => void;
101
+
102
+    /**
103
+     * Hides the 'Copy link' text.
104
+     *
105
+     * @private
106
+     * @returns {void}
107
+     */
108
+    _hideCopyLink() {
109
+        this.setState({
110
+            showCopyLink: false
111
+        });
112
+    }
113
+
114
+    _showCopyLink: () => void;
115
+
116
+    /**
117
+     * Shows the dark 'Copy link' text on hover.
118
+     *
119
+     * @private
120
+     * @returns {void}
121
+     */
122
+    _showCopyLink() {
123
+        this.setState({
124
+            showCopyLink: true
125
+        });
126
+    }
127
+
128
+    _showLinkCopied: () => void;
129
+
130
+    /**
131
+     * Shows the green 'Link copied' message.
132
+     *
133
+     * @private
134
+     * @returns {void}
135
+     */
136
+    _showLinkCopied() {
137
+        this.setState({
138
+            showLinkCopied: true,
139
+            showCopyLink: false
140
+        });
141
+    }
142
+
143
+    /**
144
+     * Implements React's {@link Component#render()}.
145
+     *
146
+     * @inheritdoc
147
+     * @returns {ReactElement}
148
+     */
149
+    render() {
150
+        const { showCopyLink, showLinkCopied } = this.state;
151
+        const { url, t } = this.props;
152
+        const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
153
+        const src = showLinkCopied ? IconCheck : IconCopy;
154
+        const iconCls = showCopyLink || showCopyLink ? 'prejoin-copy-icon--white' : 'prejoin-copy-icon--light';
155
+
156
+        return (
157
+            <div
158
+                className = 'prejoin-copy-meeting'
159
+                onMouseEnter = { _showCopyLink }
160
+                onMouseLeave = { _hideCopyLink }>
161
+                <div className = 'prejoin-copy-url'>{url}</div>
162
+                {showCopyLink && <div
163
+                    className = 'prejoin-copy-badge prejoin-copy-badge--hover'
164
+                    onClick = { _copyUrl }>
165
+                    {t('prejoin.copyAndShare')}
166
+                </div>}
167
+                {showLinkCopied && <div
168
+                    className = 'prejoin-copy-badge prejoin-copy-badge--done'>
169
+                    {t('prejoin.linkCopied')}
170
+                </div>}
171
+                <Icon
172
+                    className = { `prejoin-copy-icon ${iconCls}` }
173
+                    size = { 24 }
174
+                    src = { src } />
175
+                <textarea
176
+                    className = 'prejoin-copy-textarea'
177
+                    readOnly = { true }
178
+                    ref = { this.textarea }
179
+                    tabIndex = '-1'
180
+                    value = { url } />
181
+            </div>);
182
+    }
183
+}
184
+
185
+/**
186
+ * Maps (parts of) the redux state to the React {@code Component} props.
187
+ *
188
+ * @param {Object} state - The redux state.
189
+ * @returns {Object}
190
+ */
191
+function mapStateToProps(state) {
192
+    return {
193
+        url: getCurrentConferenceUrl(state)
194
+    };
195
+}
196
+
197
+export default connect(mapStateToProps)(translate(CopyMeetingUrl));

+ 83
- 0
react/features/prejoin/components/preview/DeviceStatus.js 파일 보기

1
+// @flow
2
+
3
+import React from 'react';
4
+import { translate } from '../../../base/i18n';
5
+import { Icon, IconCheck, IconExclamation } from '../../../base/icons';
6
+import { connect } from '../../../base/redux';
7
+import {
8
+    getDeviceStatusType,
9
+    getDeviceStatusText,
10
+    getRawError
11
+} from '../../functions';
12
+
13
+export type Props = {
14
+
15
+    /**
16
+     * The text to be displayed in relation to the status of the audio/video devices.
17
+     */
18
+    deviceStatusText: string,
19
+
20
+    /**
21
+     * The type of status for current devices, controlling the background color of the text.
22
+     * Can be `ok` or `warning`.
23
+     */
24
+    deviceStatusType: string,
25
+
26
+    /**
27
+     * The error coming from device configuration.
28
+     */
29
+    rawError: string,
30
+
31
+    /**
32
+     * Used for translation.
33
+     */
34
+    t: Function
35
+};
36
+
37
+const iconMap = {
38
+    warning: {
39
+        src: IconExclamation,
40
+        className: 'prejoin-preview-status--warning'
41
+    },
42
+    ok: {
43
+        src: IconCheck,
44
+        className: 'prejoin-preview-status--ok'
45
+    }
46
+};
47
+
48
+/**
49
+ * Strip showing the current status of the devices.
50
+ * User is informed if there are missing or malfunctioning devices.
51
+ *
52
+ * @returns {ReactElement}
53
+ */
54
+function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props) {
55
+    const { src, className } = iconMap[deviceStatusType];
56
+
57
+    return (
58
+        <div className = { `prejoin-preview-status ${className}` }>
59
+            <Icon
60
+                className = 'prejoin-preview-icon'
61
+                size = { 16 }
62
+                src = { src } />
63
+            <span className = 'prejoin-preview-error-desc'>{t(deviceStatusText)}</span>
64
+            <span>{rawError}</span>
65
+        </div>
66
+    );
67
+}
68
+
69
+/**
70
+ * Maps (parts of) the redux state to the React {@code Component} props.
71
+ *
72
+ * @param {Object} state - The redux state.
73
+ * @returns {{ deviceStatusText: string, deviceStatusText: string }}
74
+ */
75
+function mapStateToProps(state) {
76
+    return {
77
+        deviceStatusText: getDeviceStatusText(state),
78
+        deviceStatusType: getDeviceStatusType(state),
79
+        rawError: getRawError(state)
80
+    };
81
+}
82
+
83
+export default translate(connect(mapStateToProps)(DeviceStatus));

+ 80
- 0
react/features/prejoin/components/preview/ParticipantName.js 파일 보기

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { translate } from '../../../base/i18n';
5
+
6
+type Props = {
7
+
8
+    /**
9
+     * Flag signaling if the name is ediable or not.
10
+     */
11
+    isEditable: boolean,
12
+
13
+    /**
14
+     * Sets the name for the joining user.
15
+     */
16
+    setName: Function,
17
+
18
+    /**
19
+     * Used to obtain translations.
20
+     */
21
+    t: Function,
22
+
23
+    /**
24
+     * The text to be displayed.
25
+     */
26
+    value: string,
27
+};
28
+
29
+/**
30
+ * Participant name - can be an editable input or just the text name.
31
+ *
32
+ * @returns {ReactElement}
33
+ */
34
+class ParticipantName extends Component<Props> {
35
+    /**
36
+     * Initializes a new {@code ParticipantName} instance.
37
+     *
38
+     * @param {Props} props - The props of the component.
39
+     * @inheritdoc
40
+     */
41
+    constructor(props) {
42
+        super(props);
43
+
44
+        this._onNameChange = this._onNameChange.bind(this);
45
+    }
46
+
47
+    _onNameChange: () => void;
48
+
49
+    /**
50
+     * Handler used for changing the guest user name.
51
+     *
52
+     * @returns {undefined}
53
+     */
54
+    _onNameChange({ target: { value } }) {
55
+        this.props.setName(value);
56
+    }
57
+
58
+    /**
59
+     * Implements React's {@link Component#render()}.
60
+     *
61
+     * @inheritdoc
62
+     * @returns {ReactElement}
63
+     */
64
+    render() {
65
+        const { value, isEditable, t } = this.props;
66
+
67
+        return isEditable ? (
68
+            <input
69
+                className = 'prejoin-preview-name prejoin-preview-name--editable'
70
+                onChange = { this._onNameChange }
71
+                placeholder = { t('dialog.enterDisplayName') }
72
+                value = { value } />
73
+        )
74
+            : <div className = 'prejoin-preview-name'>{value}</div>
75
+        ;
76
+    }
77
+}
78
+
79
+
80
+export default translate(ParticipantName);

+ 75
- 0
react/features/prejoin/components/preview/Preview.js 파일 보기

1
+// @flow
2
+
3
+import React from 'react';
4
+import { Avatar } from '../../../base/avatar';
5
+import { Video } from '../../../base/media';
6
+import { connect } from '../../../base/redux';
7
+import { getActiveVideoTrack, getPrejoinName, isPrejoinVideoMuted } from '../../functions';
8
+
9
+export type Props = {
10
+
11
+    /**
12
+     * The name of the user that is about to join.
13
+     */
14
+    name: string,
15
+
16
+    /**
17
+     * Flag signaling the visibility of camera preview.
18
+     */
19
+    showCameraPreview: boolean,
20
+
21
+    /**
22
+     * The JitsiLocalTrack to display.
23
+     */
24
+    videoTrack: ?Object,
25
+};
26
+
27
+/**
28
+ * Component showing the video preview and device status.
29
+ *
30
+ * @param {Props} props - The props of the component.
31
+ * @returns {ReactElement}
32
+ */
33
+function Preview(props: Props) {
34
+    const {
35
+        name,
36
+        showCameraPreview,
37
+        videoTrack
38
+    } = props;
39
+
40
+    if (showCameraPreview && videoTrack) {
41
+        return (
42
+            <div className = 'prejoin-preview'>
43
+                <div className = 'prejoin-preview-overlay' />
44
+                <Video
45
+                    className = 'flipVideoX prejoin-preview-video'
46
+                    videoTrack = {{ jitsiTrack: videoTrack }} />
47
+            </div>
48
+        );
49
+    }
50
+
51
+    return (
52
+        <div className = 'prejoin-preview prejoin-preview--no-video'>
53
+            <Avatar
54
+                className = 'prejoin-preview-avatar'
55
+                displayName = { name }
56
+                size = { 200 } />
57
+        </div>
58
+    );
59
+}
60
+
61
+/**
62
+ * Maps the redux state to the React {@code Component} props.
63
+ *
64
+ * @param {Object} state - The redux state.
65
+ * @returns {Object}
66
+ */
67
+function mapStateToProps(state) {
68
+    return {
69
+        name: getPrejoinName(state),
70
+        videoTrack: getActiveVideoTrack(state),
71
+        showCameraPreview: !isPrejoinVideoMuted(state)
72
+    };
73
+}
74
+
75
+export default connect(mapStateToProps)(Preview);

+ 228
- 0
react/features/prejoin/functions.js 파일 보기

1
+// @flow
2
+
3
+
4
+/**
5
+ * Mutes or unmutes a track.
6
+ *
7
+ * @param {Object} track - The track to be configured.
8
+ * @param {boolean} shouldMute - If it should mute or not.
9
+ * @returns {Promise<void>}
10
+ */
11
+function applyMuteOptionsToTrack(track, shouldMute) {
12
+    if (track.isMuted() === shouldMute) {
13
+        return;
14
+    }
15
+
16
+    if (shouldMute) {
17
+        return track.mute();
18
+    }
19
+
20
+    return track.unmute();
21
+}
22
+
23
+/**
24
+ * Selector for the visibility of the 'join by phone' buttons.
25
+ *
26
+ * @param {Object} state - The state of the app.
27
+ * @returns {boolean}
28
+ */
29
+export function areJoinByPhoneButtonsVisible(state: Object): boolean {
30
+    return state['features/prejoin'].buttonsVisible;
31
+}
32
+
33
+/**
34
+ * Selector for determining if the device status strip is visible or not.
35
+ *
36
+ * @param {Object} state - The state of the app.
37
+ * @returns {boolean}
38
+ */
39
+export function isDeviceStatusVisible(state: Object): boolean {
40
+    return !((isAudioDisabled(state) && isPrejoinVideoDisabled(state))
41
+        || (isPrejoinAudioMuted(state) && isPrejoinVideoMuted(state)));
42
+}
43
+
44
+/**
45
+ * Selector for getting the active video/content sharing track.
46
+ *
47
+ * @param {Object} state - The state of the app.
48
+ * @returns {boolean}
49
+ */
50
+export function getActiveVideoTrack(state: Object): Object {
51
+    const track = getVideoTrack(state) || getContentSharingTrack(state);
52
+
53
+    if (track && track.isActive()) {
54
+        return track;
55
+    }
56
+
57
+    return null;
58
+}
59
+
60
+/**
61
+ * Returns a list with all the prejoin tracks configured according to
62
+ * user's preferences.
63
+ *
64
+ * @param {Object} state - The state of the app.
65
+ * @returns {Promise<Object[]>}
66
+ */
67
+export async function getAllPrejoinConfiguredTracks(state: Object): Promise<Object[]> {
68
+    const tracks = [];
69
+    const audioTrack = getAudioTrack(state);
70
+    const videoTrack = getVideoTrack(state);
71
+    const csTrack = getContentSharingTrack(state);
72
+
73
+    if (csTrack) {
74
+        tracks.push(csTrack);
75
+    } else if (videoTrack) {
76
+        await applyMuteOptionsToTrack(videoTrack, isPrejoinVideoMuted(state));
77
+        tracks.push(videoTrack);
78
+    }
79
+
80
+    if (audioTrack) {
81
+        await applyMuteOptionsToTrack(audioTrack, isPrejoinAudioMuted(state));
82
+        isPrejoinAudioMuted(state) && audioTrack.mute();
83
+        tracks.push(audioTrack);
84
+    }
85
+
86
+    return tracks;
87
+}
88
+
89
+/**
90
+ * Selector for getting the prejoin audio track.
91
+ *
92
+ * @param {Object} state - The state of the app.
93
+ * @returns {Object}
94
+ */
95
+export function getAudioTrack(state: Object): Object {
96
+    return state['features/prejoin'].audioTrack;
97
+}
98
+
99
+/**
100
+ * Selector for getting the prejoin content sharing track.
101
+ *
102
+ * @param {Object} state - The state of the app.
103
+ * @returns {Object}
104
+ */
105
+export function getContentSharingTrack(state: Object): Object {
106
+    return state['features/prejoin'].contentSharingTrack;
107
+}
108
+
109
+/**
110
+ * Returns the text for the prejoin status bar.
111
+ *
112
+ * @param {Object} state - The state of the app.
113
+ * @returns {string}
114
+ */
115
+export function getDeviceStatusText(state: Object): string {
116
+    return state['features/prejoin'].deviceStatusText;
117
+}
118
+
119
+/**
120
+ * Returns the type of the prejoin status bar: 'ok'|'warning'.
121
+ *
122
+ * @param {Object} state - The state of the app.
123
+ * @returns {string}
124
+ */
125
+export function getDeviceStatusType(state: Object): string {
126
+    return state['features/prejoin'].deviceStatusType;
127
+}
128
+
129
+/**
130
+ * Selector for getting the prejoin video track.
131
+ *
132
+ * @param {Object} state - The state of the app.
133
+ * @returns {Object}
134
+ */
135
+export function getVideoTrack(state: Object): Object {
136
+    return state['features/prejoin'].videoTrack;
137
+}
138
+
139
+/**
140
+ * Selector for getting the mute status of the prejoin audio.
141
+ *
142
+ * @param {Object} state - The state of the app.
143
+ * @returns {boolean}
144
+ */
145
+export function isPrejoinAudioMuted(state: Object): boolean {
146
+    return state['features/prejoin'].audioMuted;
147
+}
148
+
149
+/**
150
+ * Selector for getting the name that the user filled while configuring.
151
+ *
152
+ * @param {Object} state - The state of the app.
153
+ * @returns {boolean}
154
+ */
155
+export function getPrejoinName(state: Object): string {
156
+    return state['features/prejoin'].name;
157
+}
158
+
159
+/**
160
+ * Selector for getting the mute status of the prejoin video.
161
+ *
162
+ * @param {Object} state - The state of the app.
163
+ * @returns {boolean}
164
+ */
165
+export function isPrejoinVideoMuted(state: Object): boolean {
166
+    return state['features/prejoin'].videoMuted;
167
+}
168
+
169
+/**
170
+ * Selector for getting the error if any while creating streams.
171
+ *
172
+ * @param {Object} state - The state of the app.
173
+ * @returns {string}
174
+ */
175
+export function getRawError(state: Object): string {
176
+    return state['features/prejoin'].rawError;
177
+}
178
+
179
+/**
180
+ * Selector for getting state of the prejoin audio.
181
+ *
182
+ * @param {Object} state - The state of the app.
183
+ * @returns {boolean}
184
+ */
185
+export function isAudioDisabled(state: Object): Object {
186
+    return state['features/prejoin'].audioDisabled;
187
+}
188
+
189
+/**
190
+ * Selector for getting state of the prejoin video.
191
+ *
192
+ * @param {Object} state - The state of the app.
193
+ * @returns {boolean}
194
+ */
195
+export function isPrejoinVideoDisabled(state: Object): Object {
196
+    return state['features/prejoin'].videoDisabled;
197
+}
198
+
199
+/**
200
+ * Selector for getting the visiblity state for the 'JoinByPhoneDialog'.
201
+ *
202
+ * @param {Object} state - The state of the app.
203
+ * @returns {boolean}
204
+ */
205
+export function isJoinByPhoneDialogVisible(state: Object): boolean {
206
+    return state['features/prejoin'].showJoinByPhoneDialog;
207
+}
208
+
209
+/**
210
+ * Returns true if the prejoin page is enabled and no flag
211
+ * to bypass showing the page is present.
212
+ *
213
+ * @param {Object} state - The state of the app.
214
+ * @returns {boolean}
215
+ */
216
+export function isPrejoinPageEnabled(state: Object): boolean {
217
+    return state['features/base/config'].prejoinPageEnabled;
218
+}
219
+
220
+/**
221
+ * Returns true if the prejoin page is visible & active.
222
+ *
223
+ * @param {Object} state - The state of the app.
224
+ * @returns {boolean}
225
+ */
226
+export function isPrejoinPageVisible(state: Object): boolean {
227
+    return isPrejoinPageEnabled(state) && state['features/prejoin'].showPrejoin;
228
+}

+ 7
- 0
react/features/prejoin/index.js 파일 보기

1
+export * from './actions';
2
+export * from './functions';
3
+
4
+export { default as Prejoin } from './components/Prejoin';
5
+
6
+import './middleware';
7
+import './reducer';

+ 5
- 0
react/features/prejoin/logger.js 파일 보기

1
+// @flow
2
+
3
+import { getLogger } from '../base/logging/functions';
4
+
5
+export default getLogger('features/prejoin');

+ 95
- 0
react/features/prejoin/middleware.js 파일 보기

1
+// @flow
2
+
3
+import {
4
+    ADD_PREJOIN_AUDIO_TRACK,
5
+    ADD_PREJOIN_VIDEO_TRACK,
6
+    PREJOIN_START_CONFERENCE
7
+} from './actionTypes';
8
+import { setPrejoinAudioMuted, setPrejoinVideoMuted } from './actions';
9
+import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
10
+import { participantUpdated, getLocalParticipant } from '../base/participants';
11
+import { MiddlewareRegistry } from '../base/redux';
12
+import { updateSettings } from '../base/settings';
13
+import { getAllPrejoinConfiguredTracks, getPrejoinName } from './functions';
14
+
15
+declare var APP: Object;
16
+
17
+/**
18
+ * The redux middleware for {@link PrejoinPage}.
19
+ *
20
+ * @param {Store} store - The redux store.
21
+ * @returns {Function}
22
+ */
23
+MiddlewareRegistry.register(store => next => async action => {
24
+    switch (action.type) {
25
+    case ADD_PREJOIN_AUDIO_TRACK: {
26
+        const { value: audioTrack } = action;
27
+
28
+        if (audioTrack) {
29
+            store.dispatch(
30
+                    updateSettings({
31
+                        micDeviceId: audioTrack.getDeviceId()
32
+                    }),
33
+            );
34
+        }
35
+
36
+        break;
37
+    }
38
+
39
+    case ADD_PREJOIN_VIDEO_TRACK: {
40
+        const { value: videoTrack } = action;
41
+
42
+        if (videoTrack) {
43
+            store.dispatch(
44
+                    updateSettings({
45
+                        cameraDeviceId: videoTrack.getDeviceId()
46
+                    }),
47
+            );
48
+        }
49
+
50
+        break;
51
+    }
52
+
53
+    case PREJOIN_START_CONFERENCE: {
54
+        const { dispatch, getState } = store;
55
+
56
+        _syncParticipantName(dispatch, getState);
57
+        const tracks = await getAllPrejoinConfiguredTracks(getState());
58
+
59
+        APP.conference.prejoinStart(tracks);
60
+
61
+        break;
62
+    }
63
+
64
+    case SET_AUDIO_MUTED: {
65
+        store.dispatch(setPrejoinAudioMuted(Boolean(action.muted)));
66
+        break;
67
+    }
68
+
69
+    case SET_VIDEO_MUTED: {
70
+        store.dispatch(setPrejoinVideoMuted(Boolean(action.muted)));
71
+        break;
72
+    }
73
+    }
74
+
75
+    return next(action);
76
+});
77
+
78
+/**
79
+ * Sets the local participant name if one is present.
80
+ *
81
+ * @param {Function} dispatch - The redux dispatch function.
82
+ * @param {Function} getState - Gets the current state.
83
+ * @returns {undefined}
84
+ */
85
+function _syncParticipantName(dispatch, getState) {
86
+    const state = getState();
87
+    const name = getPrejoinName(state);
88
+
89
+    name && dispatch(
90
+            participantUpdated({
91
+                ...getLocalParticipant(state),
92
+                name
93
+            }),
94
+    );
95
+}

+ 168
- 0
react/features/prejoin/reducer.js 파일 보기

1
+import { ReducerRegistry } from '../base/redux';
2
+
3
+import {
4
+    ADD_PREJOIN_AUDIO_TRACK,
5
+    ADD_PREJOIN_CONTENT_SHARING_TRACK,
6
+    ADD_PREJOIN_VIDEO_TRACK,
7
+    SET_DEVICE_STATUS,
8
+    SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
9
+    SET_PREJOIN_AUDIO_DISABLED,
10
+    SET_PREJOIN_AUDIO_MUTED,
11
+    SET_PREJOIN_DEVICE_ERRORS,
12
+    SET_PREJOIN_NAME,
13
+    SET_PREJOIN_PAGE_VISIBILITY,
14
+    SET_PREJOIN_VIDEO_DISABLED,
15
+    SET_PREJOIN_VIDEO_MUTED
16
+} from './actionTypes';
17
+
18
+const DEFAULT_STATE = {
19
+    audioDisabled: false,
20
+    audioMuted: false,
21
+    videoMuted: false,
22
+    videoDisabled: false,
23
+    deviceStatusText: 'prejoin.configuringDevices',
24
+    deviceStatusType: 'ok',
25
+    showPrejoin: true,
26
+    showJoinByPhoneDialog: false,
27
+    videoTrack: null,
28
+    audioTrack: null,
29
+    contentSharingTrack: null,
30
+    rawError: '',
31
+    name: ''
32
+};
33
+
34
+/**
35
+ * Listen for actions that mutate the prejoin state
36
+ */
37
+ReducerRegistry.register(
38
+    'features/prejoin', (state = DEFAULT_STATE, action) => {
39
+        switch (action.type) {
40
+        case ADD_PREJOIN_AUDIO_TRACK: {
41
+            return {
42
+                ...state,
43
+                audioTrack: action.value
44
+            };
45
+        }
46
+
47
+        case ADD_PREJOIN_CONTENT_SHARING_TRACK: {
48
+            return {
49
+                ...state,
50
+                contentSharingTrack: action.value
51
+            };
52
+        }
53
+
54
+        case ADD_PREJOIN_VIDEO_TRACK: {
55
+            return {
56
+                ...state,
57
+                videoTrack: action.value
58
+            };
59
+        }
60
+
61
+        case SET_PREJOIN_NAME: {
62
+            return {
63
+                ...state,
64
+                name: action.value
65
+            };
66
+        }
67
+
68
+        case SET_PREJOIN_PAGE_VISIBILITY:
69
+            return {
70
+                ...state,
71
+                showPrejoin: action.value
72
+            };
73
+
74
+        case SET_PREJOIN_VIDEO_DISABLED: {
75
+            return {
76
+                ...state,
77
+                videoDisabled: action.value
78
+            };
79
+        }
80
+
81
+        case SET_PREJOIN_VIDEO_MUTED:
82
+            return {
83
+                ...state,
84
+                videoMuted: action.value
85
+            };
86
+
87
+        case SET_PREJOIN_AUDIO_MUTED:
88
+            return {
89
+                ...state,
90
+                audioMuted: action.value
91
+            };
92
+
93
+        case SET_PREJOIN_DEVICE_ERRORS: {
94
+            const status = getStatusFromErrors(action.value);
95
+
96
+            return {
97
+                ...state,
98
+                ...status
99
+            };
100
+        }
101
+
102
+        case SET_DEVICE_STATUS: {
103
+            return {
104
+                ...state,
105
+                deviceStatusText: action.text,
106
+                deviceStatusType: action.type
107
+            };
108
+        }
109
+
110
+        case SET_PREJOIN_AUDIO_DISABLED: {
111
+            return {
112
+                ...state,
113
+                audioDisabled: true
114
+            };
115
+        }
116
+
117
+        case SET_JOIN_BY_PHONE_DIALOG_VISIBLITY: {
118
+            return {
119
+                ...state,
120
+                showJoinByPhoneDialog: action.value
121
+            };
122
+        }
123
+
124
+        default:
125
+            return state;
126
+        }
127
+    },
128
+);
129
+
130
+/**
131
+ * Returns a suitable error object based on the track errors.
132
+ *
133
+ * @param {Object} errors - The errors got while creating local tracks.
134
+ * @returns {Object}
135
+ */
136
+function getStatusFromErrors(errors) {
137
+    const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
138
+
139
+    if (audioAndVideoError) {
140
+        if (audioOnlyError) {
141
+            if (videoOnlyError) {
142
+                return {
143
+                    deviceStatusType: 'warning',
144
+                    deviceStatusText: 'prejoin.audioAndVideoError',
145
+                    rawError: audioAndVideoError.message
146
+                };
147
+            }
148
+
149
+            return {
150
+                deviceStatusType: 'warning',
151
+                deviceStatusText: 'prejoin.audioOnlyError',
152
+                rawError: audioOnlyError.message
153
+            };
154
+        }
155
+
156
+        return {
157
+            deviceStatusType: 'warning',
158
+            deviceStatusText: 'prejoin.videoOnlyError',
159
+            rawError: audioAndVideoError.message
160
+        };
161
+    }
162
+
163
+    return {
164
+        deviceStatusType: 'ok',
165
+        deviceStatusText: 'prejoin.lookGood',
166
+        rawError: ''
167
+    };
168
+}

+ 4
- 2
react/features/settings/components/web/video/VideoSettingsContent.js 파일 보기

211
         const { trackData } = this.state;
211
         const { trackData } = this.state;
212
 
212
 
213
         return (
213
         return (
214
-            <div className = 'video-preview'>
215
-                {trackData.map((data, i) => this._renderPreviewEntry(data, i))}
214
+            <div className = 'video-preview-container'>
215
+                <div className = 'video-preview'>
216
+                    {trackData.map((data, i) => this._renderPreviewEntry(data, i))}
217
+                </div>
216
             </div>
218
             </div>
217
         );
219
         );
218
     }
220
     }

+ 21
- 4
react/features/toolbox/components/AudioMuteButton.js 파일 보기

12
 import { AbstractAudioMuteButton } from '../../base/toolbox';
12
 import { AbstractAudioMuteButton } from '../../base/toolbox';
13
 import type { AbstractButtonProps } from '../../base/toolbox';
13
 import type { AbstractButtonProps } from '../../base/toolbox';
14
 import { isLocalTrackMuted } from '../../base/tracks';
14
 import { isLocalTrackMuted } from '../../base/tracks';
15
+import {
16
+    isPrejoinAudioMuted,
17
+    isAudioDisabled,
18
+    isPrejoinPageVisible
19
+} from '../../prejoin';
15
 import { muteLocal } from '../../remote-video-menu/actions';
20
 import { muteLocal } from '../../remote-video-menu/actions';
16
 
21
 
17
 declare var APP: Object;
22
 declare var APP: Object;
144
  * @param {Object} state - The Redux state.
149
  * @param {Object} state - The Redux state.
145
  * @private
150
  * @private
146
  * @returns {{
151
  * @returns {{
147
- *     _audioMuted: boolean
152
+ *     _audioMuted: boolean,
153
+ *     _disabled: boolean
148
  * }}
154
  * }}
149
  */
155
  */
150
 function _mapStateToProps(state): Object {
156
 function _mapStateToProps(state): Object {
151
-    const tracks = state['features/base/tracks'];
157
+    let _audioMuted;
158
+    let _disabled;
159
+
160
+    if (isPrejoinPageVisible(state)) {
161
+        _audioMuted = isPrejoinAudioMuted(state);
162
+        _disabled = state['features/base/config'].startSilent;
163
+    } else {
164
+        const tracks = state['features/base/tracks'];
165
+
166
+        _audioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
167
+        _disabled = state['features/base/config'].startSilent || isAudioDisabled(state);
168
+    }
152
 
169
 
153
     return {
170
     return {
154
-        _audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
155
-        _disabled: state['features/base/config'].startSilent
171
+        _audioMuted,
172
+        _disabled
156
     };
173
     };
157
 }
174
 }
158
 
175
 

+ 30
- 1
react/features/toolbox/components/VideoMuteButton.js 파일 보기

17
 import { AbstractVideoMuteButton } from '../../base/toolbox';
17
 import { AbstractVideoMuteButton } from '../../base/toolbox';
18
 import type { AbstractButtonProps } from '../../base/toolbox';
18
 import type { AbstractButtonProps } from '../../base/toolbox';
19
 import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
19
 import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
20
+import {
21
+    isPrejoinPageVisible,
22
+    isPrejoinVideoDisabled,
23
+    isPrejoinVideoMuted
24
+} from '../../prejoin';
20
 import UIEvents from '../../../../service/UI/UIEvents';
25
 import UIEvents from '../../../../service/UI/UIEvents';
21
 
26
 
22
 declare var APP: Object;
27
 declare var APP: Object;
41
      */
46
      */
42
     _videoMuted: boolean,
47
     _videoMuted: boolean,
43
 
48
 
49
+    /**
50
+     * Whether video button is disabled or not.
51
+     */
52
+    _videoDisabled: boolean,
53
+
44
     /**
54
     /**
45
      * The redux {@code dispatch} function.
55
      * The redux {@code dispatch} function.
46
      */
56
      */
96
             || APP.keyboardshortcut.unregisterShortcut('V');
106
             || APP.keyboardshortcut.unregisterShortcut('V');
97
     }
107
     }
98
 
108
 
109
+    /**
110
+     * Indicates if video is currently disabled or not.
111
+     *
112
+     * @override
113
+     * @protected
114
+     * @returns {boolean}
115
+     */
116
+    _isDisabled() {
117
+        return this.props._videoDisabled;
118
+    }
119
+
99
     /**
120
     /**
100
      * Indicates if video is currently muted ot nor.
121
      * Indicates if video is currently muted ot nor.
101
      *
122
      *
170
 function _mapStateToProps(state): Object {
191
 function _mapStateToProps(state): Object {
171
     const { enabled: audioOnly } = state['features/base/audio-only'];
192
     const { enabled: audioOnly } = state['features/base/audio-only'];
172
     const tracks = state['features/base/tracks'];
193
     const tracks = state['features/base/tracks'];
194
+    let _videoMuted = isLocalVideoTrackMuted(tracks);
195
+    let _videoDisabled = false;
196
+
197
+    if (isPrejoinPageVisible(state)) {
198
+        _videoMuted = isPrejoinVideoMuted(state);
199
+        _videoDisabled = isPrejoinVideoDisabled(state);
200
+    }
173
 
201
 
174
     return {
202
     return {
175
         _audioOnly: Boolean(audioOnly),
203
         _audioOnly: Boolean(audioOnly),
204
+        _videoDisabled,
176
         _videoMediaType: getLocalVideoType(tracks),
205
         _videoMediaType: getLocalVideoType(tracks),
177
-        _videoMuted: isLocalVideoTrackMuted(tracks)
206
+        _videoMuted
178
     };
207
     };
179
 }
208
 }
180
 
209
 

+ 19
- 9
react/features/toolbox/components/web/AudioSettingsButton.js 파일 보기

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import AudioMuteButton from '../AudioMuteButton';
5
 import AudioMuteButton from '../AudioMuteButton';
6
-import { hasAvailableDevices } from '../../../base/devices';
6
+import { isAudioSettingsButtonDisabled } from '../../functions';
7
 import { IconArrowDown } from '../../../base/icons';
7
 import { IconArrowDown } from '../../../base/icons';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
9
 import { ToolboxButtonWithIcon } from '../../../base/toolbox';
9
 import { ToolboxButtonWithIcon } from '../../../base/toolbox';
25
     permissionPromptVisibility: boolean,
25
     permissionPromptVisibility: boolean,
26
 
26
 
27
     /**
27
     /**
28
-     * If the user has audio input or audio output devices.
28
+     * If the button should be disabled.
29
      */
29
      */
30
-    hasDevices: boolean,
30
+    isDisabled: boolean,
31
 
31
 
32
     /**
32
     /**
33
      * Flag controlling the visibility of the button.
33
      * Flag controlling the visibility of the button.
49
  * @returns {ReactElement}
49
  * @returns {ReactElement}
50
  */
50
  */
51
 class AudioSettingsButton extends Component<Props, State> {
51
 class AudioSettingsButton extends Component<Props, State> {
52
+    _isMounted: boolean;
53
+
52
     /**
54
     /**
53
      * Initializes a new {@code AudioSettingsButton} instance.
55
      * Initializes a new {@code AudioSettingsButton} instance.
54
      *
56
      *
58
     constructor(props) {
60
     constructor(props) {
59
         super(props);
61
         super(props);
60
 
62
 
63
+        this._isMounted = true;
61
         this.state = {
64
         this.state = {
62
             hasPermissions: false
65
             hasPermissions: false
63
         };
66
         };
73
             'audio',
76
             'audio',
74
         );
77
         );
75
 
78
 
76
-        this.setState({
79
+        this._isMounted && this.setState({
77
             hasPermissions
80
             hasPermissions
78
         });
81
         });
79
     }
82
     }
98
         }
101
         }
99
     }
102
     }
100
 
103
 
104
+    /**
105
+     * Implements React's {@link Component#componentWillUnmount}.
106
+     *
107
+     * @inheritdoc
108
+     */
109
+    componentWillUnmount() {
110
+        this._isMounted = false;
111
+    }
112
+
101
     /**
113
     /**
102
      * Implements React's {@link Component#render}.
114
      * Implements React's {@link Component#render}.
103
      *
115
      *
104
      * @inheritdoc
116
      * @inheritdoc
105
      */
117
      */
106
     render() {
118
     render() {
107
-        const { hasDevices, onAudioOptionsClick, visible } = this.props;
108
-        const settingsDisabled = !this.state.hasPermissions || !hasDevices;
119
+        const { isDisabled, onAudioOptionsClick, visible } = this.props;
120
+        const settingsDisabled = !this.state.hasPermissions || isDisabled;
109
 
121
 
110
         return visible ? (
122
         return visible ? (
111
             <AudioSettingsPopup>
123
             <AudioSettingsPopup>
128
  */
140
  */
129
 function mapStateToProps(state) {
141
 function mapStateToProps(state) {
130
     return {
142
     return {
131
-        hasDevices:
132
-            hasAvailableDevices(state, 'audioInput')
133
-            || hasAvailableDevices(state, 'audioOutput'),
143
+        isDisabled: isAudioSettingsButtonDisabled(state),
134
         permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
144
         permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
135
     };
145
     };
136
 }
146
 }

+ 19
- 8
react/features/toolbox/components/web/VideoSettingsButton.js 파일 보기

1
 // @flow
1
 // @flow
2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
-
4
+import { isVideoSettingsButtonDisabled } from '../../functions';
5
 import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
5
 import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
6
 import VideoMuteButton from '../VideoMuteButton';
6
 import VideoMuteButton from '../VideoMuteButton';
7
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
7
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8
-import { hasAvailableDevices } from '../../../base/devices';
9
 import { IconArrowDown } from '../../../base/icons';
8
 import { IconArrowDown } from '../../../base/icons';
10
 import { connect } from '../../../base/redux';
9
 import { connect } from '../../../base/redux';
11
 import { ToolboxButtonWithIcon } from '../../../base/toolbox';
10
 import { ToolboxButtonWithIcon } from '../../../base/toolbox';
25
     permissionPromptVisibility: boolean,
24
     permissionPromptVisibility: boolean,
26
 
25
 
27
     /**
26
     /**
28
-     * If the user has any video devices.
27
+     * If the button should be disabled
29
      */
28
      */
30
-    hasDevices: boolean,
29
+    isDisabled: boolean,
31
 
30
 
32
     /**
31
     /**
33
      * Flag controlling the visibility of the button.
32
      * Flag controlling the visibility of the button.
49
  * @returns {ReactElement}
48
  * @returns {ReactElement}
50
  */
49
  */
51
 class VideoSettingsButton extends Component<Props, State> {
50
 class VideoSettingsButton extends Component<Props, State> {
51
+    _isMounted: boolean;
52
+
52
     /**
53
     /**
53
      * Initializes a new {@code VideoSettingsButton} instance.
54
      * Initializes a new {@code VideoSettingsButton} instance.
54
      *
55
      *
58
     constructor(props) {
59
     constructor(props) {
59
         super(props);
60
         super(props);
60
 
61
 
62
+        this._isMounted = true;
61
         this.state = {
63
         this.state = {
62
             hasPermissions: false
64
             hasPermissions: false
63
         };
65
         };
73
             'video',
75
             'video',
74
         );
76
         );
75
 
77
 
76
-        this.setState({
78
+        this._isMounted && this.setState({
77
             hasPermissions
79
             hasPermissions
78
         });
80
         });
79
     }
81
     }
98
         }
100
         }
99
     }
101
     }
100
 
102
 
103
+    /**
104
+     * Implements React's {@link Component#componentWillUnmount}.
105
+     *
106
+     * @inheritdoc
107
+     */
108
+    componentWillUnmount() {
109
+        this._isMounted = false;
110
+    }
111
+
101
     /**
112
     /**
102
      * Implements React's {@link Component#render}.
113
      * Implements React's {@link Component#render}.
103
      *
114
      *
104
      * @inheritdoc
115
      * @inheritdoc
105
      */
116
      */
106
     render() {
117
     render() {
107
-        const { hasDevices, onVideoOptionsClick, visible } = this.props;
108
-        const iconDisabled = !this.state.hasPermissions || !hasDevices;
118
+        const { isDisabled, onVideoOptionsClick, visible } = this.props;
119
+        const iconDisabled = !this.state.hasPermissions || isDisabled;
109
 
120
 
110
         return visible ? (
121
         return visible ? (
111
             <VideoSettingsPopup>
122
             <VideoSettingsPopup>
128
  */
139
  */
129
 function mapStateToProps(state) {
140
 function mapStateToProps(state) {
130
     return {
141
     return {
131
-        hasDevices: hasAvailableDevices(state, 'videoInput'),
142
+        isDisabled: isVideoSettingsButtonDisabled(state),
132
         permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
143
         permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
133
     };
144
     };
134
 }
145
 }

+ 2
- 0
react/features/toolbox/components/web/index.js 파일 보기

1
+export { default as AudioSettingsButton } from './AudioSettingsButton';
2
+export { default as VideoSettingsButton } from './VideoSettingsButton';
1
 export { default as ToolbarButton } from './ToolbarButton';
3
 export { default as ToolbarButton } from './ToolbarButton';
2
 export { default as Toolbox } from './Toolbox';
4
 export { default as Toolbox } from './Toolbox';

+ 36
- 0
react/features/toolbox/functions.web.js 파일 보기

1
 // @flow
1
 // @flow
2
 
2
 
3
+import {
4
+    isAudioDisabled,
5
+    isPrejoinPageVisible,
6
+    isPrejoinVideoDisabled
7
+} from '../prejoin';
8
+import { hasAvailableDevices } from '../base/devices';
9
+
3
 declare var interfaceConfig: Object;
10
 declare var interfaceConfig: Object;
4
 
11
 
5
 /**
12
 /**
45
     return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
52
     return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
46
                                       || audioSettingsVisible || videoSettingsVisible));
53
                                       || audioSettingsVisible || videoSettingsVisible));
47
 }
54
 }
55
+
56
+/**
57
+ * Indicates if the audio settings button is disabled or not.
58
+ *
59
+ * @param {string} state - The state from the Redux store.
60
+ * @returns {boolean}
61
+ */
62
+export function isAudioSettingsButtonDisabled(state: Object) {
63
+    const devicesMissing = !hasAvailableDevices(state, 'audioInput')
64
+          && !hasAvailableDevices(state, 'audioOutput');
65
+
66
+    return isPrejoinPageVisible(state)
67
+        ? devicesMissing || isAudioDisabled(state)
68
+        : devicesMissing;
69
+}
70
+
71
+/**
72
+ * Indicates if the video settings button is disabled or not.
73
+ *
74
+ * @param {string} state - The state from the Redux store.
75
+ * @returns {boolean}
76
+ */
77
+export function isVideoSettingsButtonDisabled(state: Object) {
78
+    const devicesMissing = !hasAvailableDevices(state, 'videoInput');
79
+
80
+    return isPrejoinPageVisible(state)
81
+        ? devicesMissing || isPrejoinVideoDisabled(state)
82
+        : devicesMissing;
83
+}

Loading…
취소
저장