Browse Source

feat(shared-video) refactor dialog to use React

Also unify the mobile and web features into one, even though internally they still have separate ways to enable the functionality.
j8
Calinteodor 4 years ago
parent
commit
430591bd1e
No account linked to committer's email address
42 changed files with 594 additions and 459 deletions
  1. 1
    1
      conference.js
  2. 2
    1
      lang/main.json
  3. 19
    0
      modules/UI/UI.js
  4. 26
    179
      modules/UI/shared_video/SharedVideo.js
  5. 1
    1
      react/features/app/middlewares.native.js
  6. 1
    1
      react/features/app/reducers.native.js
  7. 1
    1
      react/features/base/flags/functions.js
  8. 1
    1
      react/features/base/participants/components/ParticipantView.native.js
  9. 15
    2
      react/features/shared-video/actionTypes.js
  10. 0
    31
      react/features/shared-video/actions.js
  11. 12
    12
      react/features/shared-video/actions.native.js
  12. 62
    0
      react/features/shared-video/actions.web.js
  13. 16
    22
      react/features/shared-video/components/AbstractSharedVideoDialog.js
  14. 0
    0
      react/features/shared-video/components/_.native.js
  15. 1
    0
      react/features/shared-video/components/_.web.js
  16. 1
    0
      react/features/shared-video/components/index.js
  17. 14
    22
      react/features/shared-video/components/native/SharedVideoButton.js
  18. 5
    4
      react/features/shared-video/components/native/SharedVideoDialog.js
  19. 3
    3
      react/features/shared-video/components/native/YoutubeLargeVideo.js
  20. 6
    0
      react/features/shared-video/components/native/index.js
  21. 0
    0
      react/features/shared-video/components/native/styles.js
  22. 112
    0
      react/features/shared-video/components/web/SharedVideoButton.js
  23. 102
    0
      react/features/shared-video/components/web/SharedVideoDialog.js
  24. 5
    0
      react/features/shared-video/components/web/index.js
  25. 19
    0
      react/features/shared-video/constants.js
  26. 44
    0
      react/features/shared-video/functions.js
  27. 0
    2
      react/features/shared-video/index.js
  28. 0
    30
      react/features/shared-video/middleware.js
  29. 15
    15
      react/features/shared-video/middleware.native.js
  30. 63
    0
      react/features/shared-video/middleware.web.js
  31. 8
    2
      react/features/shared-video/reducer.native.js
  32. 29
    0
      react/features/shared-video/reducer.web.js
  33. 2
    2
      react/features/toolbox/components/native/OverflowMenu.js
  34. 6
    43
      react/features/toolbox/components/web/Toolbox.js
  35. 2
    2
      react/features/video-layout/functions.js
  36. 0
    22
      react/features/youtube-player/actionTypes.js
  37. 0
    6
      react/features/youtube-player/components/_.web.js
  38. 0
    5
      react/features/youtube-player/components/index.js
  39. 0
    4
      react/features/youtube-player/components/native/index.js
  40. 0
    6
      react/features/youtube-player/constants.js
  41. 0
    15
      react/features/youtube-player/functions.js
  42. 0
    24
      react/features/youtube-player/reducer.js

+ 1
- 1
conference.js View File

125
 } from './react/features/prejoin';
125
 } from './react/features/prejoin';
126
 import { disableReceiver, stopReceiver } from './react/features/remote-control';
126
 import { disableReceiver, stopReceiver } from './react/features/remote-control';
127
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
127
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
128
-import { setSharedVideoStatus } from './react/features/shared-video';
128
+import { setSharedVideoStatus } from './react/features/shared-video/actions';
129
 import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
129
 import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
130
 import { createPresenterEffect } from './react/features/stream-effects/presenter';
130
 import { createPresenterEffect } from './react/features/stream-effects/presenter';
131
 import { endpointMessageReceived } from './react/features/subtitles';
131
 import { endpointMessageReceived } from './react/features/subtitles';

+ 2
- 1
lang/main.json View File

313
         "unlockRoom": "Remove meeting $t(lockRoomPassword)",
313
         "unlockRoom": "Remove meeting $t(lockRoomPassword)",
314
         "user": "user",
314
         "user": "user",
315
         "userPassword": "user password",
315
         "userPassword": "user password",
316
+        "videoLink": "Video link",
316
         "WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
317
         "WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
317
         "WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
318
         "WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
318
         "WaitingForHost": "Waiting for the host ...",
319
         "WaitingForHost": "Waiting for the host ...",
737
             "remoteVideoMute": "Disable camera of participant",
738
             "remoteVideoMute": "Disable camera of participant",
738
             "security": "Security options",
739
             "security": "Security options",
739
             "Settings": "Toggle settings",
740
             "Settings": "Toggle settings",
740
-            "sharedvideo": "Toggle Youtube video sharing",
741
+            "sharedvideo": "Toggle YouTube video sharing",
741
             "shareRoom": "Invite someone",
742
             "shareRoom": "Invite someone",
742
             "shareYourScreen": "Toggle screenshare",
743
             "shareYourScreen": "Toggle screenshare",
743
             "shortcuts": "Toggle shortcuts",
744
             "shortcuts": "Toggle shortcuts",

+ 19
- 0
modules/UI/UI.js View File

491
     }
491
     }
492
 };
492
 };
493
 
493
 
494
+/**
495
+ * Show shared video.
496
+ * @param {string} url video url
497
+ */
498
+UI.startSharedVideoEmitter = function(url) {
499
+    if (sharedVideoManager) {
500
+        sharedVideoManager.startSharedVideoEmitter(url);
501
+    }
502
+};
503
+
504
+/**
505
+ * Stop shared video.
506
+ */
507
+UI.stopSharedVideoEmitter = function() {
508
+    if (sharedVideoManager) {
509
+        sharedVideoManager.stopSharedVideoEmitter();
510
+    }
511
+};
512
+
494
 // TODO: Export every function separately. For now there is no point of doing
513
 // TODO: Export every function separately. For now there is no point of doing
495
 // this because we are importing everything.
514
 // this because we are importing everything.
496
 export default UI;
515
 export default UI;

+ 26
- 179
modules/UI/shared_video/SharedVideo.js View File

12
     participantLeft,
12
     participantLeft,
13
     pinParticipant
13
     pinParticipant
14
 } from '../../../react/features/base/participants';
14
 } from '../../../react/features/base/participants';
15
+import { VIDEO_PLAYER_PARTICIPANT_NAME } from '../../../react/features/shared-video/constants';
15
 import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web';
16
 import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web';
16
 import { getToolboxHeight } from '../../../react/features/toolbox/functions.web';
17
 import { getToolboxHeight } from '../../../react/features/toolbox/functions.web';
17
-import { YOUTUBE_PARTICIPANT_NAME } from '../../../react/features/youtube-player/constants';
18
 import UIEvents from '../../../service/UI/UIEvents';
18
 import UIEvents from '../../../service/UI/UIEvents';
19
-import UIUtil from '../util/UIUtil';
20
 import Filmstrip from '../videolayout/Filmstrip';
19
 import Filmstrip from '../videolayout/Filmstrip';
21
 import LargeContainer from '../videolayout/LargeContainer';
20
 import LargeContainer from '../videolayout/LargeContainer';
22
 import VideoLayout from '../videolayout/VideoLayout';
21
 import VideoLayout from '../videolayout/VideoLayout';
29
  * Example shared video link.
28
  * Example shared video link.
30
  * @type {string}
29
  * @type {string}
31
  */
30
  */
32
-const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
33
 const updateInterval = 5000; // milliseconds
31
 const updateInterval = 5000; // milliseconds
34
 
32
 
35
-/**
36
- * The dialog for user input (video link).
37
- * @type {null}
38
- */
39
-let dialog = null;
40
 
33
 
41
 /**
34
 /**
42
  * Manager of shared video.
35
  * Manager of shared video.
76
     }
69
     }
77
 
70
 
78
     /**
71
     /**
79
-     * Starts shared video by asking user for url, or if its already working
80
-     * asks whether the user wants to stop sharing the video.
72
+     * Start shared video event emitter if a video is not shown.
73
+     *
74
+     * @param url of the video
81
      */
75
      */
82
-    toggleSharedVideo() {
83
-        if (dialog) {
84
-            return;
85
-        }
76
+    startSharedVideoEmitter(url) {
86
 
77
 
87
         if (!this.isSharedVideoShown) {
78
         if (!this.isSharedVideoShown) {
88
-            requestVideoLink().then(
89
-                    url => {
90
-                        this.emitter.emit(
91
-                            UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
92
-                        sendAnalytics(createEvent('started'));
93
-                    },
94
-                    err => {
95
-                        logger.log('SHARED VIDEO CANCELED', err);
96
-                        sendAnalytics(createEvent('canceled'));
97
-                    }
98
-            );
79
+            if (url) {
80
+                this.emitter.emit(
81
+                    UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
82
+                sendAnalytics(createEvent('started'));
83
+            }
99
 
84
 
100
-            return;
85
+            logger.log('SHARED VIDEO CANCELED');
86
+            sendAnalytics(createEvent('canceled'));
101
         }
87
         }
88
+    }
89
+
90
+    /**
91
+     * Stop shared video event emitter done by the one who shared the video.
92
+     */
93
+    stopSharedVideoEmitter() {
102
 
94
 
103
         if (APP.conference.isLocalId(this.from)) {
95
         if (APP.conference.isLocalId(this.from)) {
104
-            showStopVideoPropmpt().then(
105
-                () => {
106
-                    // make sure we stop updates for playing before we send stop
107
-                    // if we stop it after receiving self presence, we can end
108
-                    // up sending stop playing, and on the other end it will not
109
-                    // stop
110
-                    if (this.intervalId) {
111
-                        clearInterval(this.intervalId);
112
-                        this.intervalId = null;
113
-                    }
114
-                    this.emitter.emit(
115
-                        UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
116
-                    sendAnalytics(createEvent('stopped'));
117
-                },
118
-                () => {}); // eslint-disable-line no-empty-function
119
-        } else {
120
-            APP.UI.messageHandler.showWarning({
121
-                descriptionKey: 'dialog.alreadySharedVideoMsg',
122
-                titleKey: 'dialog.alreadySharedVideoTitle'
123
-            });
124
-            sendAnalytics(createEvent('already.shared'));
96
+            if (this.intervalId) {
97
+                clearInterval(this.intervalId);
98
+                this.intervalId = null;
99
+            }
100
+            this.emitter.emit(
101
+                UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
102
+            sendAnalytics(createEvent('stopped'));
125
         }
103
         }
126
     }
104
     }
127
 
105
 
303
                 conference: APP.conference._room,
281
                 conference: APP.conference._room,
304
                 id: self.url,
282
                 id: self.url,
305
                 isFakeParticipant: true,
283
                 isFakeParticipant: true,
306
-                name: YOUTUBE_PARTICIPANT_NAME
284
+                name: VIDEO_PLAYER_PARTICIPANT_NAME
307
             }));
285
             }));
308
 
286
 
309
             APP.store.dispatch(pinParticipant(self.url));
287
             APP.store.dispatch(pinParticipant(self.url));
675
         return false;
653
         return false;
676
     }
654
     }
677
 }
655
 }
678
-
679
-/**
680
- * Checks if given string is youtube url.
681
- * @param {string} url string to check.
682
- * @returns {boolean}
683
- */
684
-function getYoutubeLink(url) {
685
-    const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
686
-
687
-
688
-    return url.match(p) ? RegExp.$1 : false;
689
-}
690
-
691
-/**
692
- * Ask user if he want to close shared video.
693
- */
694
-function showStopVideoPropmpt() {
695
-    return new Promise((resolve, reject) => {
696
-        const submitFunction = function(e, v) {
697
-            if (v) {
698
-                resolve();
699
-            } else {
700
-                reject();
701
-            }
702
-        };
703
-
704
-        const closeFunction = function() {
705
-            dialog = null;
706
-        };
707
-
708
-        dialog = APP.UI.messageHandler.openTwoButtonDialog({
709
-            titleKey: 'dialog.removeSharedVideoTitle',
710
-            msgKey: 'dialog.removeSharedVideoMsg',
711
-            leftButtonKey: 'dialog.Remove',
712
-            submitFunction,
713
-            closeFunction
714
-        });
715
-    });
716
-}
717
-
718
-/**
719
- * Ask user for shared video url to share with others.
720
- * Dialog validates client input to allow only youtube urls.
721
- */
722
-function requestVideoLink() {
723
-    const i18n = APP.translation;
724
-    const cancelButton = i18n.generateTranslationHTML('dialog.Cancel');
725
-    const shareButton = i18n.generateTranslationHTML('dialog.Share');
726
-    const backButton = i18n.generateTranslationHTML('dialog.Back');
727
-    const linkError
728
-        = i18n.generateTranslationHTML('dialog.shareVideoLinkError');
729
-
730
-    return new Promise((resolve, reject) => {
731
-        dialog = APP.UI.messageHandler.openDialogWithStates({
732
-            state0: {
733
-                titleKey: 'dialog.shareVideoTitle',
734
-                html: `
735
-                    <input name='sharedVideoUrl' type='text'
736
-                           class='input-control'
737
-                           data-i18n='[placeholder]defaultLink'
738
-                           autofocus>`,
739
-                persistent: false,
740
-                buttons: [
741
-                    { title: cancelButton,
742
-                        value: false },
743
-                    { title: shareButton,
744
-                        value: true }
745
-                ],
746
-                focus: ':input:first',
747
-                defaultButton: 1,
748
-                submit(e, v, m, f) { // eslint-disable-line max-params
749
-                    e.preventDefault();
750
-                    if (!v) {
751
-                        reject('cancelled');
752
-                        dialog.close();
753
-
754
-                        return;
755
-                    }
756
-
757
-                    const sharedVideoUrl = f.sharedVideoUrl;
758
-
759
-                    if (!sharedVideoUrl) {
760
-                        return;
761
-                    }
762
-
763
-                    const urlValue
764
-                        = encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
765
-                    const yVideoId = getYoutubeLink(urlValue);
766
-
767
-                    if (!yVideoId) {
768
-                        dialog.goToState('state1');
769
-
770
-                        return false;
771
-                    }
772
-
773
-                    resolve(yVideoId);
774
-                    dialog.close();
775
-                }
776
-            },
777
-
778
-            state1: {
779
-                titleKey: 'dialog.shareVideoTitle',
780
-                html: linkError,
781
-                persistent: false,
782
-                buttons: [
783
-                    { title: cancelButton,
784
-                        value: false },
785
-                    { title: backButton,
786
-                        value: true }
787
-                ],
788
-                focus: ':input:first',
789
-                defaultButton: 1,
790
-                submit(e, v) {
791
-                    e.preventDefault();
792
-                    if (v === 0) {
793
-                        reject();
794
-                        dialog.close();
795
-                    } else {
796
-                        dialog.goToState('state0');
797
-                    }
798
-                }
799
-            }
800
-        }, {
801
-            close() {
802
-                dialog = null;
803
-            }
804
-        }, {
805
-            url: defaultSharedVideoLink
806
-        });
807
-    });
808
-}

+ 1
- 1
react/features/app/middlewares.native.js View File

13
 import '../mobile/wake-lock/middleware';
13
 import '../mobile/wake-lock/middleware';
14
 import '../mobile/watchos/middleware';
14
 import '../mobile/watchos/middleware';
15
 import '../share-room/middleware';
15
 import '../share-room/middleware';
16
-import '../youtube-player/middleware';
16
+import '../shared-video/middleware';
17
 
17
 
18
 import './middlewares.any';
18
 import './middlewares.any';

+ 1
- 1
react/features/app/reducers.native.js View File

8
 import '../mobile/full-screen/reducer';
8
 import '../mobile/full-screen/reducer';
9
 import '../mobile/incoming-call/reducer';
9
 import '../mobile/incoming-call/reducer';
10
 import '../mobile/watchos/reducer';
10
 import '../mobile/watchos/reducer';
11
-import '../youtube-player/reducer';
11
+import '../shared-video/reducer';
12
 
12
 
13
 import './reducers.any';
13
 import './reducers.any';

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

11
  * @param {string} flag - The name of the React {@code Component} prop of
11
  * @param {string} flag - The name of the React {@code Component} prop of
12
  * the currently mounted {@code App} to get.
12
  * the currently mounted {@code App} to get.
13
  * @param {*} defaultValue - A default value for the flag, in case it's not defined.
13
  * @param {*} defaultValue - A default value for the flag, in case it's not defined.
14
- * @returns {*} The value of the specified React {@code Compoennt} prop of the
14
+ * @returns {*} The value of the specified React {@code Component} prop of the
15
  * currently mounted {@code App}.
15
  * currently mounted {@code App}.
16
  */
16
  */
17
 export function getFeatureFlag(stateful: Function | Object, flag: string, defaultValue: any) {
17
 export function getFeatureFlag(stateful: Function | Object, flag: string, defaultValue: any) {

+ 1
- 1
react/features/base/participants/components/ParticipantView.native.js View File

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 import { Text, View } from 'react-native';
4
 import { Text, View } from 'react-native';
5
 
5
 
6
-import { YoutubeLargeVideo } from '../../../youtube-player/components';
6
+import { YoutubeLargeVideo } from '../../../shared-video/components';
7
 import { Avatar } from '../../avatar';
7
 import { Avatar } from '../../avatar';
8
 import { translate } from '../../i18n';
8
 import { translate } from '../../i18n';
9
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
9
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';

+ 15
- 2
react/features/shared-video/actionTypes.js View File

1
+// @flow
2
+
1
 /**
3
 /**
2
  * The type of the action which signals to update the current known state of the
4
  * The type of the action which signals to update the current known state of the
3
- * shared YouTube video.
5
+ * shared video.
4
  *
6
  *
5
  * {
7
  * {
6
  *     type: SET_SHARED_VIDEO_STATUS,
8
  *     type: SET_SHARED_VIDEO_STATUS,
11
 
13
 
12
 /**
14
 /**
13
  * The type of the action which signals to start the flow for starting or
15
  * The type of the action which signals to start the flow for starting or
14
- * stopping a shared YouTube video.
16
+ * stopping a shared video.
15
  *
17
  *
16
  * {
18
  * {
17
  *     type: TOGGLE_SHARED_VIDEO
19
  *     type: TOGGLE_SHARED_VIDEO
18
  * }
20
  * }
19
  */
21
  */
20
 export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';
22
 export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';
23
+
24
+
25
+/**
26
+ * The type of the action which signals to disable or enable the shared video
27
+ * button.
28
+ *
29
+ * {
30
+ *     type: SET_DISABLE_BUTTON
31
+ * }
32
+ */
33
+export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON';

+ 0
- 31
react/features/shared-video/actions.js View File

1
-import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
2
-
3
-/**
4
- * Updates the current known status of the shared YouTube video.
5
- *
6
- * @param {string} status - The current status of the YouTube video being
7
- * shared.
8
- * @returns {{
9
- *     type: SET_SHARED_VIDEO_STATUS,
10
- *     status: string
11
- * }}
12
- */
13
-export function setSharedVideoStatus(status) {
14
-    return {
15
-        type: SET_SHARED_VIDEO_STATUS,
16
-        status
17
-    };
18
-}
19
-
20
-/**
21
- * Starts the flow for starting or stopping a shared YouTube video.
22
- *
23
- * @returns {{
24
- *     type: TOGGLE_SHARED_VIDEO
25
- * }}
26
- */
27
-export function toggleSharedVideo() {
28
-    return {
29
-        type: TOGGLE_SHARED_VIDEO
30
-    };
31
-}

react/features/youtube-player/actions.js → react/features/shared-video/actions.native.js View File

2
 
2
 
3
 import { openDialog } from '../base/dialog';
3
 import { openDialog } from '../base/dialog';
4
 
4
 
5
-import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
6
-import { EnterVideoLinkPrompt } from './components';
5
+import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
6
+import { SharedVideoDialog } from './components/native';
7
 
7
 
8
 /**
8
 /**
9
- * Updates the current known status of the shared YouTube video.
9
+ * Updates the current known status of the shared video.
10
  *
10
  *
11
- * @param {string} videoId - The youtubeId of the video to be shared.
12
- * @param {string} status - The current status of the YouTube video being shared.
13
- * @param {number} time - The current position of the YouTube video being shared.
14
- * @param {string} ownerId - The participantId of the user sharing the YouTube video.
11
+ * @param {string} videoId - The id of the video to be shared.
12
+ * @param {string} status - The current status of the video being shared.
13
+ * @param {number} time - The current position of the video being shared.
14
+ * @param {string} ownerId - The participantId of the user sharing the video.
15
  * @returns {{
15
  * @returns {{
16
  *     type: SET_SHARED_VIDEO_STATUS,
16
  *     type: SET_SHARED_VIDEO_STATUS,
17
  *     ownerId: string,
17
  *     ownerId: string,
31
 }
31
 }
32
 
32
 
33
 /**
33
 /**
34
- * Starts the flow for starting or stopping a shared YouTube video.
34
+ * Starts the flow for starting or stopping a shared video.
35
  *
35
  *
36
  * @returns {{
36
  * @returns {{
37
  *     type: TOGGLE_SHARED_VIDEO
37
  *     type: TOGGLE_SHARED_VIDEO
39
  */
39
  */
40
 export function toggleSharedVideo() {
40
 export function toggleSharedVideo() {
41
     return {
41
     return {
42
-        type: 'TOGGLE_SHARED_VIDEO'
42
+        type: TOGGLE_SHARED_VIDEO
43
     };
43
     };
44
 }
44
 }
45
 
45
 
46
 /**
46
 /**
47
- * Displays the prompt for entering the youtube video link.
47
+ * Displays the prompt for entering the video link.
48
  *
48
  *
49
  * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
49
  * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
50
  * @returns {Function}
50
  * @returns {Function}
51
  */
51
  */
52
-export function showEnterVideoLinkPrompt(onPostSubmit: ?Function) {
53
-    return openDialog(EnterVideoLinkPrompt, { onPostSubmit });
52
+export function showSharedVideoDialog(onPostSubmit: ?Function) {
53
+    return openDialog(SharedVideoDialog, { onPostSubmit });
54
 }
54
 }

+ 62
- 0
react/features/shared-video/actions.web.js View File

1
+// @flow
2
+
3
+import { openDialog } from '../base/dialog/actions';
4
+import { SharedVideoDialog } from '../shared-video/components';
5
+
6
+import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO, SET_DISABLE_BUTTON } from './actionTypes';
7
+
8
+/**
9
+ * Updates the current known status of the shared video.
10
+ *
11
+ * @param {string} status - The current status of the video being shared.
12
+ * @returns {{
13
+ *     type: SET_SHARED_VIDEO_STATUS,
14
+ *     status: string
15
+ * }}
16
+ */
17
+export function setSharedVideoStatus(status: string) {
18
+    return {
19
+        type: SET_SHARED_VIDEO_STATUS,
20
+        status
21
+    };
22
+}
23
+
24
+
25
+/**
26
+ * Disabled share video button.
27
+ *
28
+ * @param {boolean} disabled - The current state of the share video button.
29
+ * @returns {{
30
+ *     type: SET_DISABLE_BUTTON,
31
+ *     disabled: boolean
32
+ * }}
33
+ */
34
+export function setDisableButton(disabled: boolean) {
35
+    return {
36
+        type: SET_DISABLE_BUTTON,
37
+        disabled
38
+    };
39
+}
40
+
41
+/**
42
+ * Starts the flow for starting or stopping a shared video.
43
+ *
44
+ * @returns {{
45
+ *     type: TOGGLE_SHARED_VIDEO
46
+ * }}
47
+ */
48
+export function toggleSharedVideo() {
49
+    return {
50
+        type: TOGGLE_SHARED_VIDEO
51
+    };
52
+}
53
+
54
+/**
55
+ * Displays the dialog for entering the video link.
56
+ *
57
+ * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
58
+ * @returns {Function}
59
+ */
60
+export function showSharedVideoDialog(onPostSubmit: ?Function) {
61
+    return openDialog(SharedVideoDialog, { onPostSubmit });
62
+}

react/features/youtube-player/components/AbstractEnterVideoLinkPrompt.js → react/features/shared-video/components/AbstractSharedVideoDialog.js View File

3
 import { Component } from 'react';
3
 import { Component } from 'react';
4
 import type { Dispatch } from 'redux';
4
 import type { Dispatch } from 'redux';
5
 
5
 
6
+import { getYoutubeLink } from '../functions';
7
+
8
+
6
 /**
9
 /**
7
  * The type of the React {@code Component} props of
10
  * The type of the React {@code Component} props of
8
- * {@link AbstractEnterVideoLinkPrompt}.
11
+ * {@link AbstractSharedVideoDialog}.
9
  */
12
  */
10
 export type Props = {
13
 export type Props = {
11
 
14
 
12
     /**
15
     /**
13
-     * Invoked to update the shared youtube video link.
16
+     * Invoked to update the shared video link.
14
      */
17
      */
15
     dispatch: Dispatch<any>,
18
     dispatch: Dispatch<any>,
16
 
19
 
17
     /**
20
     /**
18
-     * Function to be invoked after typing a valid youtube video .
21
+     * Function to be invoked after typing a valid video.
22
+     */
23
+    onPostSubmit: ?Function,
24
+
25
+    /**
26
+     * Invoked to obtain translated strings.
19
      */
27
      */
20
-    onPostSubmit: ?Function
28
+    t: Function
21
 };
29
 };
22
 
30
 
23
 /**
31
 /**
24
- * Implements an abstract class for {@code EnterVideoLinkPrompt}.
32
+ * Implements an abstract class for {@code SharedVideoDialog}.
25
  */
33
  */
26
-export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Props, S > {
34
+export default class AbstractSharedVideoDialog<S: *> extends Component < Props, S > {
27
     /**
35
     /**
28
      * Instantiates a new component.
36
      * Instantiates a new component.
29
      *
37
      *
30
-     *
31
      * @inheritdoc
38
      * @inheritdoc
32
      */
39
      */
33
     constructor(props: Props) {
40
     constructor(props: Props) {
39
     _onSetVideoLink: string => boolean;
46
     _onSetVideoLink: string => boolean;
40
 
47
 
41
     /**
48
     /**
42
-     * Validates the entered video link by extractibg the id and dispatches it.
49
+     * Validates the entered video link by extracting the id and dispatches it.
43
      *
50
      *
44
      * It returns a boolean to comply the Dialog behaviour:
51
      * It returns a boolean to comply the Dialog behaviour:
45
      *     {@code true} - the dialog should be closed.
52
      *     {@code true} - the dialog should be closed.
48
      * @param {string} link - The entered video link.
55
      * @param {string} link - The entered video link.
49
      * @returns {boolean}
56
      * @returns {boolean}
50
      */
57
      */
51
-    _onSetVideoLink(link) {
58
+    _onSetVideoLink(link: string) {
52
         if (!link || !link.trim()) {
59
         if (!link || !link.trim()) {
53
             return false;
60
             return false;
54
         }
61
         }
67
     }
74
     }
68
 }
75
 }
69
 
76
 
70
-/**
71
- * Validates the entered video url.
72
- *
73
- * It returns a boolean to reflect whether the url matches the youtube regex.
74
- *
75
- * @param {string} url - The entered video link.
76
- * @returns {boolean}
77
- */
78
-function getYoutubeLink(url) {
79
-    const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
80
-    const result = url.match(p);
81
 
77
 
82
-    return result ? result[1] : false;
83
-}

react/features/youtube-player/components/_.native.js → react/features/shared-video/components/_.native.js View File


+ 1
- 0
react/features/shared-video/components/_.web.js View File

1
+export * from './web';

+ 1
- 0
react/features/shared-video/components/index.js View File

1
+export * from './_';

react/features/youtube-player/components/VideoShareButton.js → react/features/shared-video/components/native/SharedVideoButton.js View File

2
 
2
 
3
 import type { Dispatch } from 'redux';
3
 import type { Dispatch } from 'redux';
4
 
4
 
5
-import { getFeatureFlag, VIDEO_SHARE_BUTTON_ENABLED } from '../../base/flags';
6
-import { translate } from '../../base/i18n';
7
-import { IconShareVideo } from '../../base/icons';
8
-import { getLocalParticipant } from '../../base/participants';
9
-import { connect } from '../../base/redux';
10
-import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
11
-import { toggleSharedVideo } from '../actions';
5
+import { getFeatureFlag, VIDEO_SHARE_BUTTON_ENABLED } from '../../../base/flags';
6
+import { translate } from '../../../base/i18n';
7
+import { IconShareVideo } from '../../../base/icons';
8
+import { getLocalParticipant } from '../../../base/participants';
9
+import { connect } from '../../../base/redux';
10
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
11
+import { toggleSharedVideo } from '../../actions.native';
12
+import { isSharingStatus } from '../../functions';
12
 
13
 
13
 /**
14
 /**
14
  * The type of the React {@code Component} props of {@link TileViewButton}.
15
  * The type of the React {@code Component} props of {@link TileViewButton}.
21
     _isDisabled: boolean,
22
     _isDisabled: boolean,
22
 
23
 
23
     /**
24
     /**
24
-     * Whether or not the local participant is sharing a YouTube video.
25
+     * Whether or not the local participant is sharing a video.
25
      */
26
      */
26
     _sharingVideo: boolean,
27
     _sharingVideo: boolean,
27
 
28
 
76
     }
77
     }
77
 
78
 
78
     /**
79
     /**
79
-     * Dispatches an action to toggle YouTube video sharing.
80
+     * Dispatches an action to toggle video sharing.
80
      *
81
      *
81
      * @private
82
      * @private
82
      * @returns {void}
83
      * @returns {void}
95
  * @returns {Props}
96
  * @returns {Props}
96
  */
97
  */
97
 function _mapStateToProps(state, ownProps): Object {
98
 function _mapStateToProps(state, ownProps): Object {
98
-    const { ownerId, status: sharedVideoStatus } = state['features/youtube-player'];
99
+    const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
99
     const localParticipantId = getLocalParticipant(state).id;
100
     const localParticipantId = getLocalParticipant(state).id;
100
     const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
101
     const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
101
     const { visible = enabled } = ownProps;
102
     const { visible = enabled } = ownProps;
104
         return {
105
         return {
105
             _isDisabled: isSharingStatus(sharedVideoStatus),
106
             _isDisabled: isSharingStatus(sharedVideoStatus),
106
             _sharingVideo: false,
107
             _sharingVideo: false,
107
-            visible };
108
+            visible
109
+        };
108
     }
110
     }
109
 
111
 
110
     return {
112
     return {
113
+        _isDisabled: false,
111
         _sharingVideo: isSharingStatus(sharedVideoStatus),
114
         _sharingVideo: isSharingStatus(sharedVideoStatus),
112
         visible
115
         visible
113
     };
116
     };
114
 }
117
 }
115
 
118
 
116
-/**
117
- * Checks if the status is one that is actually sharing the video - playing, pause or start.
118
- *
119
- * @param {string} status - The shared video status.
120
- * @private
121
- * @returns {boolean}
122
- */
123
-function isSharingStatus(status) {
124
-    return [ 'playing', 'pause', 'start' ].includes(status);
125
-}
126
-
127
 export default translate(connect(_mapStateToProps)(VideoShareButton));
119
 export default translate(connect(_mapStateToProps)(VideoShareButton));

react/features/youtube-player/components/native/EnterVideoLinkPrompt.js → react/features/shared-video/components/native/SharedVideoDialog.js View File

4
 
4
 
5
 import { InputDialog } from '../../../base/dialog';
5
 import { InputDialog } from '../../../base/dialog';
6
 import { connect } from '../../../base/redux';
6
 import { connect } from '../../../base/redux';
7
-import AbstractEnterVideoLinkPrompt from '../AbstractEnterVideoLinkPrompt';
7
+import { defaultSharedVideoLink } from '../../constants';
8
+import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
8
 
9
 
9
 /**
10
 /**
10
  * Implements a component to render a display name prompt.
11
  * Implements a component to render a display name prompt.
11
  */
12
  */
12
-class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> {
13
+class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
13
     /**
14
     /**
14
      * Implements React's {@link Component#render()}.
15
      * Implements React's {@link Component#render()}.
15
      *
16
      *
21
                 contentKey = 'dialog.shareVideoTitle'
22
                 contentKey = 'dialog.shareVideoTitle'
22
                 onSubmit = { this._onSetVideoLink }
23
                 onSubmit = { this._onSetVideoLink }
23
                 textInputProps = {{
24
                 textInputProps = {{
24
-                    placeholder: 'https://youtu.be/TB7LlM4erx8'
25
+                    placeholder: defaultSharedVideoLink
25
                 }} />
26
                 }} />
26
         );
27
         );
27
     }
28
     }
29
     _onSetVideoLink: string => boolean;
30
     _onSetVideoLink: string => boolean;
30
 }
31
 }
31
 
32
 
32
-export default connect()(EnterVideoLinkPrompt);
33
+export default connect()(SharedVideoDialog);

react/features/youtube-player/components/native/YoutubeLargeVideo.js → react/features/shared-video/components/native/YoutubeLargeVideo.js View File

6
 
6
 
7
 import { getLocalParticipant } from '../../../base/participants';
7
 import { getLocalParticipant } from '../../../base/participants';
8
 import { connect } from '../../../base/redux';
8
 import { connect } from '../../../base/redux';
9
-import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
9
+import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
10
 import { setToolboxVisible } from '../../../toolbox/actions';
10
 import { setToolboxVisible } from '../../../toolbox/actions';
11
-import { setSharedVideoStatus } from '../../actions';
11
+import { setSharedVideoStatus } from '../../actions.native';
12
 
12
 
13
 import styles from './styles';
13
 import styles from './styles';
14
 
14
 
383
  * @returns {Props}
383
  * @returns {Props}
384
  */
384
  */
385
 function _mapStateToProps(state) {
385
 function _mapStateToProps(state) {
386
-    const { ownerId, status, time } = state['features/youtube-player'];
386
+    const { ownerId, status, time } = state['features/shared-video'];
387
     const localParticipant = getLocalParticipant(state);
387
     const localParticipant = getLocalParticipant(state);
388
     const responsiveUi = state['features/base/responsive-ui'];
388
     const responsiveUi = state['features/base/responsive-ui'];
389
     const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;
389
     const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;

+ 6
- 0
react/features/shared-video/components/native/index.js View File

1
+// @flow
2
+
3
+export { default as SharedVideoButton } from './SharedVideoButton';
4
+export { default as SharedVideoDialog } from './SharedVideoDialog';
5
+export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';
6
+

react/features/youtube-player/components/native/styles.js → react/features/shared-video/components/native/styles.js View File


+ 112
- 0
react/features/shared-video/components/web/SharedVideoButton.js View File

1
+// @flow
2
+
3
+import type { Dispatch } from 'redux';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconShareVideo } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import {
9
+    AbstractButton,
10
+    type AbstractButtonProps
11
+} from '../../../base/toolbox/components';
12
+import { showSharedVideoDialog } from '../../actions.web';
13
+import { isSharingStatus } from '../../functions';
14
+
15
+declare var APP: Object;
16
+
17
+type Props = AbstractButtonProps & {
18
+
19
+    /**
20
+     * The redux {@code dispatch} function.
21
+     */
22
+    dispatch: Dispatch<any>,
23
+
24
+    /**
25
+     * Whether or not the button is disabled.
26
+     */
27
+    _isDisabled: boolean,
28
+
29
+    /**
30
+     * Whether or not the local participant is sharing a video.
31
+     */
32
+    _sharingVideo: boolean
33
+};
34
+
35
+/**
36
+ * Implements an {@link AbstractButton} to open the user documentation in a new window.
37
+ */
38
+class SharedVideoButton extends AbstractButton<Props, *> {
39
+    accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
40
+    icon = IconShareVideo;
41
+    label = 'toolbar.sharedvideo';
42
+    tooltip = 'toolbar.sharedvideo';
43
+    toggledLabel = 'toolbar.stopSharedVideo';
44
+
45
+    /**
46
+     * Handles clicking / pressing the button, and opens a new dialog.
47
+     *
48
+     * @private
49
+     * @returns {void}
50
+     */
51
+    _handleClick() {
52
+        this._doToggleSharedVideoDialog();
53
+    }
54
+
55
+    /**
56
+     * Indicates whether this button is in toggled state or not.
57
+     *
58
+     * @override
59
+     * @protected
60
+     * @returns {boolean}
61
+     */
62
+    _isToggled() {
63
+        return this.props._sharingVideo;
64
+    }
65
+
66
+    /**
67
+     * Indicates whether this button is disabled or not.
68
+     *
69
+     * @override
70
+     * @protected
71
+     * @returns {boolean}
72
+     */
73
+    _isDisabled() {
74
+        return this.props._isDisabled;
75
+    }
76
+
77
+    /**
78
+     * Dispatches an action to toggle video sharing.
79
+     *
80
+     * @private
81
+     * @returns {void}
82
+     */
83
+    _doToggleSharedVideoDialog() {
84
+        const { dispatch } = this.props;
85
+
86
+        return this._isToggled()
87
+            ? APP.UI.stopSharedVideoEmitter()
88
+            : dispatch(showSharedVideoDialog(id => APP.UI.startSharedVideoEmitter(id)));
89
+    }
90
+}
91
+
92
+/**
93
+ * Maps part of the Redux state to the props of this component.
94
+ *
95
+ * @param {Object} state - The Redux state.
96
+ * @private
97
+ * @returns {Props}
98
+ */
99
+function _mapStateToProps(state): Object {
100
+    const {
101
+        disabled: sharedVideoBtnDisabled,
102
+        status: sharedVideoStatus
103
+    } = state['features/shared-video'];
104
+
105
+    return {
106
+        _isDisabled: sharedVideoBtnDisabled,
107
+        _sharingVideo: isSharingStatus(sharedVideoStatus)
108
+    };
109
+}
110
+
111
+
112
+export default translate(connect(_mapStateToProps)(SharedVideoButton));

+ 102
- 0
react/features/shared-video/components/web/SharedVideoDialog.js View File

1
+// @flow
2
+
3
+import { FieldTextStateless } from '@atlaskit/field-text';
4
+import React from 'react';
5
+
6
+import { Dialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+import { getFieldValue } from '../../../base/react';
9
+import { connect } from '../../../base/redux';
10
+import { defaultSharedVideoLink } from '../../constants';
11
+import { getYoutubeLink } from '../../functions';
12
+import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
13
+
14
+/**
15
+ * Component that renders the video share dialog.
16
+ *
17
+ * @returns {React$Element<any>}
18
+ */
19
+class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
20
+
21
+    /**
22
+     * Instantiates a new component.
23
+     *
24
+     * @inheritdoc
25
+     */
26
+    constructor(props) {
27
+        super(props);
28
+
29
+        this.state = {
30
+            value: '',
31
+            okDisabled: true
32
+        };
33
+
34
+        this._onChange = this._onChange.bind(this);
35
+        this._onSubmitValue = this._onSubmitValue.bind(this);
36
+    }
37
+
38
+    _onChange: Object => void;
39
+
40
+    /**
41
+     * Callback for the onChange event of the field.
42
+     *
43
+     * @param {Object} evt - The static event.
44
+     * @returns {void}
45
+     */
46
+    _onChange(evt: Object) {
47
+        const linkValue = getFieldValue(evt);
48
+
49
+        this.setState({
50
+            value: linkValue,
51
+            okDisabled: !getYoutubeLink(linkValue)
52
+        });
53
+    }
54
+
55
+    _onSubmitValue: () => boolean;
56
+
57
+    /**
58
+     * Callback to be invoked when the value of the link input is submitted.
59
+     *
60
+     * @returns {boolean}
61
+     */
62
+    _onSubmitValue() {
63
+        return this._onSetVideoLink(this.state.value);
64
+    }
65
+
66
+    /**
67
+     * Implements React's {@link Component#render()}.
68
+     *
69
+     * @inheritdoc
70
+     */
71
+    render() {
72
+        const { t } = this.props;
73
+
74
+        return (
75
+            <Dialog
76
+                hideCancelButton = { false }
77
+                okDisabled = { this.state.okDisabled }
78
+                okKey = { t('dialog.Share') }
79
+                onSubmit = { this._onSubmitValue }
80
+                titleKey = { t('dialog.shareVideoTitle') }
81
+                width = { 'small' }>
82
+                <FieldTextStateless
83
+                    autoFocus = { true }
84
+                    className = 'input-control'
85
+                    compact = { false }
86
+                    label = { t('dialog.videoLink') }
87
+                    name = 'sharedVideoUrl'
88
+                    onChange = { this._onChange }
89
+                    placeholder = { defaultSharedVideoLink }
90
+                    shouldFitContainer = { true }
91
+                    type = 'text'
92
+                    value = { this.state.value } />
93
+            </Dialog>
94
+        );
95
+    }
96
+
97
+    _onSetVideoLink: string => boolean;
98
+
99
+    _onChange: Object => void;
100
+}
101
+
102
+export default translate(connect()(SharedVideoDialog));

+ 5
- 0
react/features/shared-video/components/web/index.js View File

1
+// @flow
2
+
3
+export { default as SharedVideoButton } from './SharedVideoButton';
4
+export { default as SharedVideoDialog } from './SharedVideoDialog';
5
+

+ 19
- 0
react/features/shared-video/constants.js View File

1
+// @flow
2
+
3
+/**
4
+ * Example shared video link.
5
+ * @type {string}
6
+ */
7
+export const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
8
+
9
+/**
10
+ * Fixed name of the video player fake participant.
11
+ * @type {string}
12
+ */
13
+export const VIDEO_PLAYER_PARTICIPANT_NAME = 'YouTube';
14
+
15
+/**
16
+ * Shared video command.
17
+ * @type {string}
18
+ */
19
+export const SHARED_VIDEO = 'shared-video';

+ 44
- 0
react/features/shared-video/functions.js View File

1
+// @flow
2
+
3
+import { getParticipants } from '../base/participants';
4
+
5
+import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
6
+
7
+/**
8
+ * Validates the entered video url.
9
+ *
10
+ * It returns a boolean to reflect whether the url matches the youtube regex.
11
+ *
12
+ * @param {string} url - The entered video link.
13
+ * @returns {boolean}
14
+ */
15
+export function getYoutubeLink(url: string) {
16
+    const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
17
+    const result = url.match(p);
18
+
19
+    return result ? result[1] : false;
20
+}
21
+
22
+
23
+/**
24
+ * Checks if the status is one that is actually sharing the video - playing, pause or start.
25
+ *
26
+ * @param {string} status - The shared video status.
27
+ * @returns {boolean}
28
+ */
29
+export function isSharingStatus(status: string) {
30
+    return [ 'playing', 'pause', 'start' ].includes(status);
31
+}
32
+
33
+
34
+/**
35
+ * Returns true if there is a video being shared in the meeting.
36
+ *
37
+ * @param {Object | Function} stateful - The Redux state or a function that gets resolved to the Redux state.
38
+ * @returns {boolean}
39
+ */
40
+export function isVideoPlaying(stateful: Object | Function): boolean {
41
+    return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
42
+        && p.name === VIDEO_PLAYER_PARTICIPANT_NAME)
43
+    );
44
+}

+ 0
- 2
react/features/shared-video/index.js View File

1
-export * from './actions';
2
-export * from './actionTypes';

+ 0
- 30
react/features/shared-video/middleware.js View File

1
-// @flow
2
-
3
-import UIEvents from '../../../service/UI/UIEvents';
4
-import { MiddlewareRegistry } from '../base/redux';
5
-
6
-import { TOGGLE_SHARED_VIDEO } from './actionTypes';
7
-
8
-declare var APP: Object;
9
-
10
-/**
11
- * Middleware that captures actions related to YouTube video sharing and updates
12
- * components not hooked into redux.
13
- *
14
- * @param {Store} store - The redux store.
15
- * @returns {Function}
16
- */
17
-// eslint-disable-next-line no-unused-vars
18
-MiddlewareRegistry.register(store => next => action => {
19
-    if (typeof APP === 'undefined') {
20
-        return next(action);
21
-    }
22
-
23
-    switch (action.type) {
24
-    case TOGGLE_SHARED_VIDEO:
25
-        APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
26
-        break;
27
-    }
28
-
29
-    return next(action);
30
-});

react/features/youtube-player/middleware.js → react/features/shared-video/middleware.native.js View File

11
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
11
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
12
 
12
 
13
 import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes';
13
 import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes';
14
-import { setSharedVideoStatus, showEnterVideoLinkPrompt } from './actions';
15
-import { YOUTUBE_PARTICIPANT_NAME } from './constants';
16
-
17
-const SHARED_VIDEO = 'shared-video';
14
+import { setSharedVideoStatus, showSharedVideoDialog } from './actions.native';
15
+import { SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
16
+import { isSharingStatus } from './functions';
18
 
17
 
19
 /**
18
 /**
20
- * Middleware that captures actions related to YouTube video sharing and updates
19
+ * Middleware that captures actions related to video sharing and updates
21
  * components not hooked into redux.
20
  * components not hooked into redux.
22
  *
21
  *
23
  * @param {Store} store - The redux store.
22
  * @param {Store} store - The redux store.
29
     const conference = getCurrentConference(state);
28
     const conference = getCurrentConference(state);
30
     const localParticipantId = getLocalParticipant(state)?.id;
29
     const localParticipantId = getLocalParticipant(state)?.id;
31
     const { videoId, status, ownerId, time } = action;
30
     const { videoId, status, ownerId, time } = action;
32
-    const { ownerId: stateOwnerId, videoId: stateVideoId } = state['features/youtube-player'];
31
+    const { ownerId: stateOwnerId, videoId: stateVideoId } = state['features/shared-video'];
33
 
32
 
34
     switch (action.type) {
33
     switch (action.type) {
35
     case TOGGLE_SHARED_VIDEO:
34
     case TOGGLE_SHARED_VIDEO:
71
                     const localParticipantId = getLocalParticipant(getState()).id;
70
                     const localParticipantId = getLocalParticipant(getState()).id;
72
                     const status = attributes.state;
71
                     const status = attributes.state;
73
 
72
 
74
-                    if ([ 'playing', 'pause', 'start' ].includes(status)) {
73
+                    if (isSharingStatus(status)) {
75
                         handleSharingVideoStatus(store, value, attributes, conference);
74
                         handleSharingVideoStatus(store, value, attributes, conference);
76
                     } else if (status === 'stop') {
75
                     } else if (status === 'stop') {
77
                         dispatch(participantLeft(value, conference));
76
                         dispatch(participantLeft(value, conference));
82
                 }
81
                 }
83
             );
82
             );
84
         }
83
         }
85
-    });
84
+    }
85
+);
86
 
86
 
87
 /**
87
 /**
88
  * Handles the playing, pause and start statuses for the shared video.
88
  * Handles the playing, pause and start statuses for the shared video.
90
  * Sets the SharedVideoStatus if the event was triggered by the local user.
90
  * Sets the SharedVideoStatus if the event was triggered by the local user.
91
  *
91
  *
92
  * @param {Store} store - The redux store.
92
  * @param {Store} store - The redux store.
93
- * @param {string} videoId - The YoutubeId of the video to the shared.
93
+ * @param {string} videoId - The id of the video to the shared.
94
  * @param {Object} attributes - The attributes received from the share video command.
94
  * @param {Object} attributes - The attributes received from the share video command.
95
  * @param {JitsiConference} conference - The current conference.
95
  * @param {JitsiConference} conference - The current conference.
96
  * @returns {void}
96
  * @returns {void}
98
 function handleSharingVideoStatus(store, videoId, { state, time, from }, conference) {
98
 function handleSharingVideoStatus(store, videoId, { state, time, from }, conference) {
99
     const { dispatch, getState } = store;
99
     const { dispatch, getState } = store;
100
     const localParticipantId = getLocalParticipant(getState()).id;
100
     const localParticipantId = getLocalParticipant(getState()).id;
101
-    const oldStatus = getState()['features/youtube-player']?.status;
101
+    const oldStatus = getState()['features/shared-video']?.status;
102
 
102
 
103
     if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
103
     if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
104
         dispatch(participantJoined({
104
         dispatch(participantJoined({
106
             id: videoId,
106
             id: videoId,
107
             isFakeParticipant: true,
107
             isFakeParticipant: true,
108
             avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`,
108
             avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`,
109
-            name: YOUTUBE_PARTICIPANT_NAME
109
+            name: VIDEO_PLAYER_PARTICIPANT_NAME
110
         }));
110
         }));
111
 
111
 
112
         dispatch(pinParticipant(videoId));
112
         dispatch(pinParticipant(videoId));
130
 function _toggleSharedVideo(store, next, action) {
130
 function _toggleSharedVideo(store, next, action) {
131
     const { dispatch, getState } = store;
131
     const { dispatch, getState } = store;
132
     const state = getState();
132
     const state = getState();
133
-    const { videoId, ownerId, status } = state['features/youtube-player'];
133
+    const { videoId, ownerId, status } = state['features/shared-video'];
134
     const localParticipant = getLocalParticipant(state);
134
     const localParticipant = getLocalParticipant(state);
135
 
135
 
136
     if (status === 'playing' || status === 'start' || status === 'pause') {
136
     if (status === 'playing' || status === 'start' || status === 'pause') {
138
             dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id));
138
             dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id));
139
         }
139
         }
140
     } else {
140
     } else {
141
-        dispatch(showEnterVideoLinkPrompt(id => _onVideoLinkEntered(store, id)));
141
+        dispatch(showSharedVideoDialog(id => _onVideoLinkEntered(store, id)));
142
     }
142
     }
143
 
143
 
144
     return next(action);
144
     return next(action);
148
  * Sends SHARED_VIDEO start command.
148
  * Sends SHARED_VIDEO start command.
149
  *
149
  *
150
  * @param {Store} store - The redux store.
150
  * @param {Store} store - The redux store.
151
- * @param {string} id - The youtube id of the video to be shared.
151
+ * @param {string} id - The id of the video to be shared.
152
  * @returns {void}
152
  * @returns {void}
153
  */
153
  */
154
 function _onVideoLinkEntered(store, id) {
154
 function _onVideoLinkEntered(store, id) {
167
 /**
167
 /**
168
  * Sends SHARED_VIDEO command.
168
  * Sends SHARED_VIDEO command.
169
  *
169
  *
170
- * @param {string} id - The youtube id of the video.
170
+ * @param {string} id - The id of the video.
171
  * @param {string} status - The status of the shared video.
171
  * @param {string} status - The status of the shared video.
172
  * @param {JitsiConference} conference - The current conference.
172
  * @param {JitsiConference} conference - The current conference.
173
  * @param {string} localParticipantId - The id of the local participant.
173
  * @param {string} localParticipantId - The id of the local participant.

+ 63
- 0
react/features/shared-video/middleware.web.js View File

1
+// @flow
2
+
3
+import UIEvents from '../../../service/UI/UIEvents';
4
+import { getCurrentConference } from '../base/conference';
5
+import { getLocalParticipant } from '../base/participants';
6
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
7
+
8
+import { TOGGLE_SHARED_VIDEO } from './actionTypes';
9
+import { setDisableButton } from './actions.web';
10
+import { SHARED_VIDEO } from './constants';
11
+
12
+declare var APP: Object;
13
+
14
+/**
15
+ * Middleware that captures actions related to video sharing and updates
16
+ * components not hooked into redux.
17
+ *
18
+ * @param {Store} store - The redux store.
19
+ * @returns {Function}
20
+ */
21
+// eslint-disable-next-line no-unused-vars
22
+MiddlewareRegistry.register(store => next => action => {
23
+    if (typeof APP === 'undefined') {
24
+        return next(action);
25
+    }
26
+
27
+    switch (action.type) {
28
+    case TOGGLE_SHARED_VIDEO:
29
+        APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
30
+        break;
31
+    }
32
+
33
+    return next(action);
34
+});
35
+
36
+/**
37
+ * Set up state change listener to disable or enable the share video button in
38
+ * the toolbar menu.
39
+ */
40
+StateListenerRegistry.register(
41
+    state => getCurrentConference(state),
42
+    (conference, store, previousConference) => {
43
+        if (conference && conference !== previousConference) {
44
+            conference.addCommandListener(SHARED_VIDEO,
45
+                ({ attributes }) => {
46
+
47
+                    const { dispatch, getState } = store;
48
+                    const { from } = attributes;
49
+                    const localParticipantId = getLocalParticipant(getState()).id;
50
+                    const status = attributes.state;
51
+
52
+                    if (status === 'playing') {
53
+                        if (localParticipantId !== from) {
54
+                            dispatch(setDisableButton(true));
55
+                        }
56
+                    } else if (status === 'stop') {
57
+                        dispatch(setDisableButton(false));
58
+                    }
59
+                }
60
+            );
61
+        }
62
+    }
63
+);

react/features/shared-video/reducer.js → react/features/shared-video/reducer.native.js View File

1
+// @flow
2
+
1
 import { ReducerRegistry } from '../base/redux';
3
 import { ReducerRegistry } from '../base/redux';
2
 
4
 
3
 import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
5
 import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
6
  * Reduces the Redux actions of the feature features/shared-video.
8
  * Reduces the Redux actions of the feature features/shared-video.
7
  */
9
  */
8
 ReducerRegistry.register('features/shared-video', (state = {}, action) => {
10
 ReducerRegistry.register('features/shared-video', (state = {}, action) => {
11
+    const { videoId, status, time, ownerId } = action;
12
+
9
     switch (action.type) {
13
     switch (action.type) {
10
     case SET_SHARED_VIDEO_STATUS:
14
     case SET_SHARED_VIDEO_STATUS:
11
         return {
15
         return {
12
             ...state,
16
             ...state,
13
-            status: action.status
17
+            videoId,
18
+            status,
19
+            time,
20
+            ownerId
14
         };
21
         };
15
-
16
     default:
22
     default:
17
         return state;
23
         return state;
18
     }
24
     }

+ 29
- 0
react/features/shared-video/reducer.web.js View File

1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+
5
+import { SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
6
+
7
+/**
8
+ * Reduces the Redux actions of the feature features/shared-video.
9
+ */
10
+ReducerRegistry.register('features/shared-video', (state = {}, action) => {
11
+    const { status, disabled } = action;
12
+
13
+    switch (action.type) {
14
+    case SET_SHARED_VIDEO_STATUS:
15
+        return {
16
+            ...state,
17
+            status
18
+        };
19
+
20
+    case SET_DISABLE_BUTTON:
21
+        return {
22
+            ...state,
23
+            disabled
24
+        };
25
+
26
+    default:
27
+        return state;
28
+    }
29
+});

+ 2
- 2
react/features/toolbox/components/native/OverflowMenu.js View File

15
 import { AudioRouteButton } from '../../../mobile/audio-mode';
15
 import { AudioRouteButton } from '../../../mobile/audio-mode';
16
 import { LiveStreamButton, RecordButton } from '../../../recording';
16
 import { LiveStreamButton, RecordButton } from '../../../recording';
17
 import { RoomLockButton } from '../../../room-lock';
17
 import { RoomLockButton } from '../../../room-lock';
18
+import { SharedVideoButton } from '../../../shared-video/components';
18
 import { ClosedCaptionButton } from '../../../subtitles';
19
 import { ClosedCaptionButton } from '../../../subtitles';
19
 import { TileViewButton } from '../../../video-layout';
20
 import { TileViewButton } from '../../../video-layout';
20
-import { VideoShareButton } from '../../../youtube-player/components';
21
 import HelpButton from '../HelpButton';
21
 import HelpButton from '../HelpButton';
22
 import MuteEveryoneButton from '../MuteEveryoneButton';
22
 import MuteEveryoneButton from '../MuteEveryoneButton';
23
 
23
 
140
                     <TileViewButton { ...buttonProps } />
140
                     <TileViewButton { ...buttonProps } />
141
                     <RecordButton { ...buttonProps } />
141
                     <RecordButton { ...buttonProps } />
142
                     <LiveStreamButton { ...buttonProps } />
142
                     <LiveStreamButton { ...buttonProps } />
143
-                    <VideoShareButton { ...buttonProps } />
143
+                    <SharedVideoButton { ...buttonProps } />
144
                     <RoomLockButton { ...buttonProps } />
144
                     <RoomLockButton { ...buttonProps } />
145
                     <ClosedCaptionButton { ...buttonProps } />
145
                     <ClosedCaptionButton { ...buttonProps } />
146
                     <SharedDocumentButton { ...buttonProps } />
146
                     <SharedDocumentButton { ...buttonProps } />

+ 6
- 43
react/features/toolbox/components/web/Toolbox.js View File

22
     IconPresentation,
22
     IconPresentation,
23
     IconRaisedHand,
23
     IconRaisedHand,
24
     IconRec,
24
     IconRec,
25
-    IconShareDesktop,
26
-    IconShareVideo
25
+    IconShareDesktop
27
 } from '../../../base/icons';
26
 } from '../../../base/icons';
28
 import JitsiMeetJS from '../../../base/lib-jitsi-meet';
27
 import JitsiMeetJS from '../../../base/lib-jitsi-meet';
29
 import {
28
 import {
57
     SettingsButton,
56
     SettingsButton,
58
     openSettingsDialog
57
     openSettingsDialog
59
 } from '../../../settings';
58
 } from '../../../settings';
60
-import { toggleSharedVideo } from '../../../shared-video';
59
+import { SharedVideoButton } from '../../../shared-video/components';
61
 import { SpeakerStats } from '../../../speaker-stats';
60
 import { SpeakerStats } from '../../../speaker-stats';
62
 import {
61
 import {
63
     ClosedCaptionButton
62
     ClosedCaptionButton
90
 import ToolbarButton from './ToolbarButton';
89
 import ToolbarButton from './ToolbarButton';
91
 import VideoSettingsButton from './VideoSettingsButton';
90
 import VideoSettingsButton from './VideoSettingsButton';
92
 
91
 
92
+
93
 /**
93
 /**
94
  * The type of the React {@code Component} props of {@link Toolbox}.
94
  * The type of the React {@code Component} props of {@link Toolbox}.
95
  */
95
  */
259
         this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this);
259
         this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this);
260
         this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
260
         this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
261
         this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
261
         this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
262
-        this._onToolbarToggleSharedVideo = this._onToolbarToggleSharedVideo.bind(this);
263
         this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
262
         this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
264
         this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
263
         this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
265
 
264
 
504
         }
503
         }
505
     }
504
     }
506
 
505
 
507
-    /**
508
-     * Dispatches an action to toggle YouTube video sharing.
509
-     *
510
-     * @private
511
-     * @returns {void}
512
-     */
513
-    _doToggleSharedVideo() {
514
-        this.props.dispatch(toggleSharedVideo());
515
-    }
516
-
517
     /**
506
     /**
518
      * Dispatches an action to toggle the video quality dialog.
507
      * Dispatches an action to toggle the video quality dialog.
519
      *
508
      *
897
         this._doToggleScreenshare();
886
         this._doToggleScreenshare();
898
     }
887
     }
899
 
888
 
900
-    _onToolbarToggleSharedVideo: () => void;
901
-
902
-    /**
903
-     * Creates an analytics toolbar event and dispatches an action for toggling
904
-     * the sharing of a YouTube video.
905
-     *
906
-     * @private
907
-     * @returns {void}
908
-     */
909
-    _onToolbarToggleSharedVideo() {
910
-        sendAnalytics(createToolbarEvent('shared.video.toggled',
911
-            {
912
-                enable: !this.props._sharingVideo
913
-            }));
914
-
915
-        this._doToggleSharedVideo();
916
-    }
917
-
918
     _onToolbarOpenLocalRecordingInfoDialog: () => void;
889
     _onToolbarOpenLocalRecordingInfoDialog: () => void;
919
 
890
 
920
     /**
891
     /**
930
     }
901
     }
931
 
902
 
932
     /**
903
     /**
933
-     * Returns true if the the desktop sharing button should be visible and
904
+     * Returns true if the desktop sharing button should be visible and
934
      * false otherwise.
905
      * false otherwise.
935
      *
906
      *
936
      * @returns {boolean}
907
      * @returns {boolean}
1028
             _feedbackConfigured,
999
             _feedbackConfigured,
1029
             _fullScreen,
1000
             _fullScreen,
1030
             _screensharing,
1001
             _screensharing,
1031
-            _sharingVideo,
1032
             t
1002
             t
1033
         } = this.props;
1003
         } = this.props;
1034
 
1004
 
1057
                     key = 'record'
1027
                     key = 'record'
1058
                     showLabel = { true } />,
1028
                     showLabel = { true } />,
1059
             this._shouldShowButton('sharedvideo')
1029
             this._shouldShowButton('sharedvideo')
1060
-                && <OverflowMenuItem
1061
-                    accessibilityLabel = { t('toolbar.accessibilityLabel.sharedvideo') }
1062
-                    icon = { IconShareVideo }
1030
+                && <SharedVideoButton
1063
                     key = 'sharedvideo'
1031
                     key = 'sharedvideo'
1064
-                    onClick = { this._onToolbarToggleSharedVideo }
1065
-                    text = { _sharingVideo ? t('toolbar.stopSharedVideo') : t('toolbar.sharedvideo') } />,
1032
+                    showLabel = { true } />,
1066
             this._shouldShowButton('etherpad')
1033
             this._shouldShowButton('etherpad')
1067
                 && <SharedDocumentButton
1034
                 && <SharedDocumentButton
1068
                     key = 'etherpad'
1035
                     key = 'etherpad'
1435
         callStatsID,
1402
         callStatsID,
1436
         enableFeaturesBasedOnToken
1403
         enableFeaturesBasedOnToken
1437
     } = state['features/base/config'];
1404
     } = state['features/base/config'];
1438
-    const sharedVideoStatus = state['features/shared-video'].status;
1439
     const {
1405
     const {
1440
         fullScreen,
1406
         fullScreen,
1441
         overflowMenuVisible
1407
         overflowMenuVisible
1476
         _overflowMenuVisible: overflowMenuVisible,
1442
         _overflowMenuVisible: overflowMenuVisible,
1477
         _raisedHand: localParticipant.raisedHand,
1443
         _raisedHand: localParticipant.raisedHand,
1478
         _screensharing: localVideo && localVideo.videoType === 'desktop',
1444
         _screensharing: localVideo && localVideo.videoType === 'desktop',
1479
-        _sharingVideo: sharedVideoStatus === 'playing'
1480
-            || sharedVideoStatus === 'start'
1481
-            || sharedVideoStatus === 'pause',
1482
         _visible: isToolboxVisible(state),
1445
         _visible: isToolboxVisible(state),
1483
         _visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
1446
         _visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
1484
     };
1447
     };

+ 2
- 2
react/features/video-layout/functions.js View File

10
     SINGLE_COLUMN_BREAKPOINT,
10
     SINGLE_COLUMN_BREAKPOINT,
11
     TWO_COLUMN_BREAKPOINT
11
     TWO_COLUMN_BREAKPOINT
12
 } from '../filmstrip/constants';
12
 } from '../filmstrip/constants';
13
-import { isYoutubeVideoPlaying } from '../youtube-player/functions';
13
+import { isVideoPlaying } from '../shared-video/functions';
14
 
14
 
15
 import { LAYOUTS } from './constants';
15
 import { LAYOUTS } from './constants';
16
 
16
 
154
         || participantCount < 3
154
         || participantCount < 3
155
 
155
 
156
         // There is a shared YouTube video in the meeting
156
         // There is a shared YouTube video in the meeting
157
-        || isYoutubeVideoPlaying(state)
157
+        || isVideoPlaying(state)
158
 
158
 
159
         // We want jibri to use stage view by default
159
         // We want jibri to use stage view by default
160
         || iAmRecorder
160
         || iAmRecorder

+ 0
- 22
react/features/youtube-player/actionTypes.js View File

1
-/**
2
- * The type of the action which signals to update the current known state of the
3
- * shared YouTube video.
4
- *
5
- * {
6
- *     type: SET_SHARED_VIDEO_STATUS,
7
- *     status: string,
8
- *     time: string,
9
- *     ownerId: string
10
- * }
11
- */
12
-export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
13
-
14
-/**
15
- * The type of the action which signals to start the flow for starting or
16
- * stopping a shared YouTube video.
17
- *
18
- * {
19
- *     type: TOGGLE_SHARED_VIDEO
20
- * }
21
- */
22
-export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';

+ 0
- 6
react/features/youtube-player/components/_.web.js View File

1
-// @flow
2
-
3
-import { Component } from 'react';
4
-
5
-export { Component as EnterVideoLinkPrompt };
6
-export { Component as YoutubeLargeVideo };

+ 0
- 5
react/features/youtube-player/components/index.js View File

1
-// @flow
2
-
3
-export { default as VideoShareButton } from './VideoShareButton';
4
-
5
-export * from './_';

+ 0
- 4
react/features/youtube-player/components/native/index.js View File

1
-// @flow
2
-
3
-export { default as EnterVideoLinkPrompt } from './EnterVideoLinkPrompt';
4
-export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';

+ 0
- 6
react/features/youtube-player/constants.js View File

1
-// @flow
2
-
3
-/**
4
- * Fixed name of the YouTube player fake participant.
5
- */
6
-export const YOUTUBE_PARTICIPANT_NAME = 'YouTube';

+ 0
- 15
react/features/youtube-player/functions.js View File

1
-// @flow
2
-
3
-import { getParticipants } from '../base/participants';
4
-
5
-import { YOUTUBE_PARTICIPANT_NAME } from './constants';
6
-
7
-/**
8
- * Returns true if there is a youtube video being shaerd in the meeting.
9
- *
10
- * @param {Object | Function} stateful - The Redux state or a function that gets resolved to the Redux state.
11
- * @returns {boolean}
12
- */
13
-export function isYoutubeVideoPlaying(stateful: Object | Function): boolean {
14
-    return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant && p.name === YOUTUBE_PARTICIPANT_NAME));
15
-}

+ 0
- 24
react/features/youtube-player/reducer.js View File

1
-// @flow
2
-import { ReducerRegistry } from '../base/redux';
3
-
4
-import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
5
-
6
-/**
7
- * Reduces the Redux actions of the feature features/youtube-player.
8
- */
9
-ReducerRegistry.register('features/youtube-player', (state = {}, action) => {
10
-    const { videoId, status, time, ownerId } = action;
11
-
12
-    switch (action.type) {
13
-    case SET_SHARED_VIDEO_STATUS:
14
-        return {
15
-            ...state,
16
-            videoId,
17
-            status,
18
-            time,
19
-            ownerId
20
-        };
21
-    default:
22
-        return state;
23
-    }
24
-});

Loading…
Cancel
Save