Kaynağa Gözat

feat(device_selection): Implement popup

master
hristoterezov 8 yıl önce
ebeveyn
işleme
96e83989a5
29 değiştirilmiş dosya ile 1549 ekleme ve 590 silme
  1. 2
    0
      Makefile
  2. 1
    1
      css/_filmstrip.scss
  3. 7
    1
      css/_toolbars.scss
  4. 1
    0
      css/_variables.scss
  5. 2
    2
      interface_config.js
  6. 3
    47
      react/features/base/dialog/components/AbstractDialog.js
  7. 26
    96
      react/features/base/dialog/components/Dialog.web.js
  8. 210
    0
      react/features/base/dialog/components/StatelessDialog.web.js
  9. 1
    0
      react/features/base/dialog/components/index.js
  10. 50
    0
      react/features/base/dialog/constants.js
  11. 300
    0
      react/features/device-selection/DeviceSelectionPopup.js
  12. 10
    0
      react/features/device-selection/actionTypes.js
  13. 175
    1
      react/features/device-selection/actions.js
  14. 1
    1
      react/features/device-selection/components/AudioInputPreview.js
  15. 43
    410
      react/features/device-selection/components/DeviceSelectionDialog.js
  16. 529
    0
      react/features/device-selection/components/DeviceSelectionDialogBase.js
  17. 1
    1
      react/features/device-selection/components/DeviceSelector.js
  18. 2
    0
      react/features/device-selection/components/index.js
  19. 4
    0
      react/features/device-selection/index.js
  20. 24
    0
      react/features/device-selection/middleware.js
  21. 13
    0
      react/features/device-selection/popup.js
  22. 28
    0
      react/features/device-selection/reducer.js
  23. 19
    3
      react/features/toolbox/components/ToolbarButton.native.js
  24. 12
    2
      react/features/toolbox/components/ToolbarButton.web.js
  25. 43
    9
      react/features/toolbox/defaultToolbarButtons.js
  26. 3
    4
      react/features/toolbox/functions.web.js
  27. 10
    12
      react/features/toolbox/reducer.js
  28. 19
    0
      static/deviceSelectionPopup.html
  29. 10
    0
      webpack.config.js

+ 2
- 0
Makefile Dosyayı Görüntüle

@@ -31,6 +31,8 @@ deploy-appbundle:
31 31
 		$(BUILD_DIR)/do_external_connect.min.map \
32 32
 		$(BUILD_DIR)/external_api.min.js \
33 33
 		$(BUILD_DIR)/external_api.min.map \
34
+		$(BUILD_DIR)/device_selection_popup_bundle.min.js \
35
+		$(BUILD_DIR)/device_selection_popup_bundle.min.map \
34 36
 		$(OUTPUT_DIR)/analytics.js \
35 37
 		$(DEPLOY_DIR)
36 38
 

+ 1
- 1
css/_filmstrip.scss Dosyayı Görüntüle

@@ -136,7 +136,7 @@
136 136
     &__videos-filmstripOnly {
137 137
         margin-top: auto;
138 138
         margin-bottom: auto;
139
-        padding-right: $defaultToolbarSize;
139
+        padding-right: $defaultFilmStripOnlyToolbarSize;
140 140
     }
141 141
 
142 142
     .remote-videos-container {

+ 7
- 1
css/_toolbars.scss Dosyayı Görüntüle

@@ -200,7 +200,13 @@
200 200
         height: auto;
201 201
         position: absolute;
202 202
         right: 0;
203
-        width: $defaultToolbarSize;
203
+        width: $defaultFilmStripOnlyToolbarSize;
204
+
205
+        .button {
206
+            height: 37px;
207
+            line-height: 37px !important;
208
+            width: 37px;
209
+        }
204 210
 
205 211
         .button:first-child {
206 212
             border-top-left-radius: 3px;

+ 1
- 0
css/_variables.scss Dosyayı Görüntüle

@@ -34,6 +34,7 @@ $tooltipBg: rgba(0,0,0, 0.7);
34 34
  * Toolbar
35 35
  */
36 36
 $defaultToolbarSize: 50px;
37
+$defaultFilmStripOnlyToolbarSize: 37px;
37 38
 $splitterToolbarButtonMargin: 18px;
38 39
 $toolbarBackground: rgba(0, 0, 0, 0.5);
39 40
 $toolbarBadgeBackground: #165ECC;

+ 2
- 2
interface_config.js Dosyayı Görüntüle

@@ -36,14 +36,14 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
36 36
      */
37 37
     TOOLBAR_BUTTONS: [
38 38
         //main toolbar
39
-        'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
39
+        'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection', // jshint ignore:line
40 40
         //extended toolbar
41 41
         'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
42 42
     /**
43 43
      * Main Toolbar Buttons
44 44
      * All of them should be in TOOLBAR_BUTTONS
45 45
      */
46
-    MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup'], // jshint ignore:line
46
+    MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection'], // jshint ignore:line
47 47
     SETTINGS_SECTIONS: ['language', 'devices', 'moderator'],
48 48
     // Determines how the video would fit the screen. 'both' would fit the whole
49 49
     // screen, 'height' would fit the original video height to the height of the

+ 3
- 47
react/features/base/dialog/components/AbstractDialog.js Dosyayı Görüntüle

@@ -1,6 +1,7 @@
1 1
 import React, { Component } from 'react';
2 2
 
3 3
 import { hideDialog } from '../actions';
4
+import { dialogPropTypes } from '../constants';
4 5
 
5 6
 /**
6 7
  * Abstract dialog to display dialogs.
@@ -13,57 +14,12 @@ export default class AbstractDialog extends Component {
13 14
      * @static
14 15
      */
15 16
     static propTypes = {
16
-        /**
17
-         * Whether cancel button is disabled. Enabled by default.
18
-         */
19
-        cancelDisabled: React.PropTypes.bool,
20
-
21
-        /**
22
-         * Optional i18n key to change the cancel button title.
23
-         */
24
-        cancelTitleKey: React.PropTypes.string,
17
+        ...dialogPropTypes,
25 18
 
26 19
         /**
27 20
          * Used to show/hide the dialog on cancel.
28 21
          */
29
-        dispatch: React.PropTypes.func,
30
-
31
-        /**
32
-         * Is ok button enabled/disabled. Enabled by default.
33
-         */
34
-        okDisabled: React.PropTypes.bool,
35
-
36
-        /**
37
-         * Optional i18n key to change the ok button title.
38
-         */
39
-        okTitleKey: React.PropTypes.string,
40
-
41
-        /**
42
-         * The handler for onCancel event.
43
-         */
44
-        onCancel: React.PropTypes.func,
45
-
46
-        /**
47
-         * The handler for the event when submitting the dialog.
48
-         */
49
-        onSubmit: React.PropTypes.func,
50
-
51
-        /**
52
-         * Used to obtain translations in children classes.
53
-         */
54
-        t: React.PropTypes.func,
55
-
56
-        /**
57
-         * Key to use for showing a title.
58
-         */
59
-        titleKey: React.PropTypes.string,
60
-
61
-        /**
62
-         * The string to use as a title instead of {@code titleKey}. If a truthy
63
-         * value is specified, it takes precedence over {@code titleKey} i.e.
64
-         * the latter is unused.
65
-         */
66
-        titleString: React.PropTypes.string
22
+        dispatch: React.PropTypes.func
67 23
     };
68 24
 
69 25
     /**

+ 26
- 96
react/features/base/dialog/components/Dialog.web.js Dosyayı Görüntüle

@@ -1,12 +1,8 @@
1
-import AKButton from '@atlaskit/button';
2
-import AKButtonGroup from '@atlaskit/button-group';
3
-import ModalDialog from '@atlaskit/modal-dialog';
4 1
 import React from 'react';
5 2
 import { connect } from 'react-redux';
6 3
 
7
-import { translate } from '../../i18n';
8
-
9 4
 import AbstractDialog from './AbstractDialog';
5
+import StatelessDialog from './StatelessDialog';
10 6
 
11 7
 /**
12 8
  * Web dialog that uses atlaskit modal-dialog to display dialogs.
@@ -19,6 +15,8 @@ class Dialog extends AbstractDialog {
19 15
      * @static
20 16
      */
21 17
     static propTypes = {
18
+        ...AbstractDialog.propTypes,
19
+
22 20
         /**
23 21
          * This is the body of the dialog, the component children.
24 22
          */
@@ -30,6 +28,11 @@ class Dialog extends AbstractDialog {
30 28
          */
31 29
         isModal: React.PropTypes.bool,
32 30
 
31
+        /**
32
+         * Disables rendering of the submit button.
33
+         */
34
+        submitDisabled: React.PropTypes.bool,
35
+
33 36
         /**
34 37
          * Width of the dialog, can be:
35 38
          * - 'small' (400px), 'medium' (600px), 'large' (800px),
@@ -41,107 +44,34 @@ class Dialog extends AbstractDialog {
41 44
     };
42 45
 
43 46
     /**
44
-     * Implements React's {@link Component#render()}.
47
+     * Initializes a new Dialog instance.
45 48
      *
46
-     * @inheritdoc
47
-     * @returns {ReactElement}
49
+     * @param {Object} props - The read-only properties with which the new
50
+     * instance is to be initialized.
48 51
      */
49
-    render() {
50
-        return (
51
-            <ModalDialog
52
-                footer = { this._renderFooter() }
53
-                header = { this._renderHeader() }
54
-                isOpen = { true }
55
-                onDialogDismissed = { this._onCancel }
56
-                width = { this.props.width || 'medium' }>
57
-                <div>
58
-                    <form
59
-                        className = 'modal-dialog-form'
60
-                        id = 'modal-dialog-form'
61
-                        onSubmit = { this._onSubmit }>
62
-                        { this.props.children }
63
-                    </form>
64
-                </div>
65
-            </ModalDialog>);
66
-    }
67
-
68
-    /**
69
-     * Render cancel button.
70
-     *
71
-     * @returns {*} The cancel button if enabled and dialog is not modal.
72
-     * @private
73
-     */
74
-    _renderCancelButton() {
75
-        if (this.props.cancelDisabled || this.props.isModal) {
76
-            return null;
77
-        }
52
+    constructor(props) {
53
+        super(props);
78 54
 
79
-        return (
80
-            <AKButton
81
-                appearance = 'subtle'
82
-                id = 'modal-dialog-cancel-button'
83
-                onClick = { this._onCancel }>
84
-                { this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
85
-            </AKButton>
86
-        );
55
+        this._onCancel = this._onCancel.bind(this);
56
+        this._onSubmit = this._onSubmit.bind(this);
87 57
     }
88 58
 
89 59
     /**
90
-     * Render component in dialog footer.
91
-     *
92
-     * @returns {ReactElement}
93
-     * @private
94
-     */
95
-    _renderFooter() {
96
-        return (
97
-            <footer className = 'modal-dialog-footer'>
98
-                <AKButtonGroup>
99
-                    { this._renderCancelButton() }
100
-                    { this._renderOKButton() }
101
-                </AKButtonGroup>
102
-            </footer>
103
-        );
104
-    }
105
-
106
-    /**
107
-     * Render component in dialog header.
60
+     * Implements React's {@link Component#render()}.
108 61
      *
62
+     * @inheritdoc
109 63
      * @returns {ReactElement}
110
-     * @private
111 64
      */
112
-    _renderHeader() {
113
-        const { t } = this.props;
114
-
115
-        return (
116
-            <header>
117
-                <h2>
118
-                    { this.props.titleString || t(this.props.titleKey) }
119
-                </h2>
120
-            </header>
121
-        );
122
-    }
65
+    render() {
66
+        const props = {
67
+            ...this.props,
68
+            onSubmit: this._onSubmit,
69
+            onCancel: this._onCancel
70
+        };
123 71
 
124
-    /**
125
-     * Render ok button.
126
-     *
127
-     * @returns {*} The ok button if enabled.
128
-     * @private
129
-     */
130
-    _renderOKButton() {
131
-        if (this.props.submitDisabled) {
132
-            return null;
133
-        }
72
+        delete props.dispatch;
134 73
 
135
-        return (
136
-            <AKButton
137
-                appearance = 'primary'
138
-                form = 'modal-dialog-form'
139
-                id = 'modal-dialog-ok-button'
140
-                isDisabled = { this.props.okDisabled }
141
-                onClick = { this._onSubmit }>
142
-                { this.props.t(this.props.okTitleKey || 'dialog.Ok') }
143
-            </AKButton>
144
-        );
74
+        return <StatelessDialog { ...props } />;
145 75
     }
146 76
 
147 77
     /**
@@ -158,4 +88,4 @@ class Dialog extends AbstractDialog {
158 88
     }
159 89
 }
160 90
 
161
-export default translate(connect()(Dialog));
91
+export default connect()(Dialog);

+ 210
- 0
react/features/base/dialog/components/StatelessDialog.web.js Dosyayı Görüntüle

@@ -0,0 +1,210 @@
1
+import AKButton from '@atlaskit/button';
2
+import AKButtonGroup from '@atlaskit/button-group';
3
+import ModalDialog from '@atlaskit/modal-dialog';
4
+import React, { Component } from 'react';
5
+
6
+import { translate } from '../../i18n';
7
+
8
+import { dialogPropTypes } from '../constants';
9
+
10
+/**
11
+ * Web dialog that uses atlaskit modal-dialog to display dialogs.
12
+ */
13
+class StatelessDialog extends Component {
14
+
15
+    /**
16
+     * Web dialog component's property types.
17
+     *
18
+     * @static
19
+     */
20
+    static propTypes = {
21
+        ...dialogPropTypes,
22
+
23
+        /**
24
+         * This is the body of the dialog, the component children.
25
+         */
26
+        children: React.PropTypes.node,
27
+
28
+        /**
29
+         * Disables dismissing the dialog when the blanket is clicked. Enabled
30
+         * by default.
31
+         */
32
+        disableBlanketClickDismiss: React.PropTypes.bool,
33
+
34
+        /**
35
+         * Whether the dialog is modal. This means clicking on the blanket will
36
+         * leave the dialog open. No cancel button.
37
+         */
38
+        isModal: React.PropTypes.bool,
39
+
40
+        /**
41
+         * Disables rendering of the submit button.
42
+         */
43
+        submitDisabled: React.PropTypes.bool,
44
+
45
+        /**
46
+         * Width of the dialog, can be:
47
+         * - 'small' (400px), 'medium' (600px), 'large' (800px),
48
+         * 'x-large' (968px)
49
+         * - integer value for pixel width
50
+         * - string value for percentage
51
+         */
52
+        width: React.PropTypes.string
53
+
54
+    };
55
+
56
+    /**
57
+     * Initializes a new Dialog instance.
58
+     *
59
+     * @param {Object} props - The read-only properties with which the new
60
+     * instance is to be initialized.
61
+     */
62
+    constructor(props) {
63
+        super(props);
64
+
65
+        this._onCancel = this._onCancel.bind(this);
66
+        this._onDialogDismissed = this._onDialogDismissed.bind(this);
67
+        this._onSubmit = this._onSubmit.bind(this);
68
+    }
69
+
70
+    /**
71
+     * Implements React's {@link Component#render()}.
72
+     *
73
+     * @inheritdoc
74
+     * @returns {ReactElement}
75
+     */
76
+    render() {
77
+        return (
78
+            <ModalDialog
79
+                footer = { this._renderFooter() }
80
+                header = { this._renderHeader() }
81
+                isOpen = { true }
82
+                onDialogDismissed = { this._onDialogDismissed }
83
+                width = { this.props.width || 'medium' }>
84
+                <div>
85
+                    <form
86
+                        className = 'modal-dialog-form'
87
+                        id = 'modal-dialog-form'
88
+                        onSubmit = { this._onSubmit }>
89
+                        { this.props.children }
90
+                    </form>
91
+                </div>
92
+            </ModalDialog>);
93
+    }
94
+
95
+    /**
96
+     * Handles click on the blanket area.
97
+     *
98
+     * @returns {void}
99
+     */
100
+    _onDialogDismissed() {
101
+        if (!this.props.disableBlanketClickDismiss) {
102
+            this._onCancel();
103
+        }
104
+    }
105
+
106
+    /**
107
+     * Render cancel button.
108
+     *
109
+     * @returns {*} The cancel button if enabled and dialog is not modal.
110
+     * @private
111
+     */
112
+    _renderCancelButton() {
113
+        if (this.props.cancelDisabled || this.props.isModal) {
114
+            return null;
115
+        }
116
+
117
+        return (
118
+            <AKButton
119
+                appearance = 'subtle'
120
+                id = 'modal-dialog-cancel-button'
121
+                onClick = { this._onCancel }>
122
+                { this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
123
+            </AKButton>
124
+        );
125
+    }
126
+
127
+    /**
128
+     * Render component in dialog footer.
129
+     *
130
+     * @returns {ReactElement}
131
+     * @private
132
+     */
133
+    _renderFooter() {
134
+        return (
135
+            <footer className = 'modal-dialog-footer'>
136
+                <AKButtonGroup>
137
+                    { this._renderCancelButton() }
138
+                    { this._renderOKButton() }
139
+                </AKButtonGroup>
140
+            </footer>
141
+        );
142
+    }
143
+
144
+    /**
145
+     * Render component in dialog header.
146
+     *
147
+     * @returns {ReactElement}
148
+     * @private
149
+     */
150
+    _renderHeader() {
151
+        const { t } = this.props;
152
+
153
+        return (
154
+            <header>
155
+                <h2>
156
+                    { this.props.titleString || t(this.props.titleKey) }
157
+                </h2>
158
+            </header>
159
+        );
160
+    }
161
+
162
+    /**
163
+     * Render ok button.
164
+     *
165
+     * @returns {*} The ok button if enabled.
166
+     * @private
167
+     */
168
+    _renderOKButton() {
169
+        if (this.props.submitDisabled) {
170
+            return null;
171
+        }
172
+
173
+        return (
174
+            <AKButton
175
+                appearance = 'primary'
176
+                form = 'modal-dialog-form'
177
+                id = 'modal-dialog-ok-button'
178
+                isDisabled = { this.props.okDisabled }
179
+                onClick = { this._onSubmit }>
180
+                { this.props.t(this.props.okTitleKey || 'dialog.Ok') }
181
+            </AKButton>
182
+        );
183
+    }
184
+
185
+    /**
186
+     * Dispatches action to hide the dialog.
187
+     *
188
+     * @returns {void}
189
+     */
190
+    _onCancel() {
191
+        if (this.props.isModal) {
192
+            return;
193
+        }
194
+
195
+        this.props.onCancel();
196
+    }
197
+
198
+    /**
199
+     * Dispatches the action when submitting the dialog.
200
+     *
201
+     * @private
202
+     * @param {string} value - The submitted value if any.
203
+     * @returns {void}
204
+     */
205
+    _onSubmit(value) {
206
+        this.props.onSubmit(value);
207
+    }
208
+}
209
+
210
+export default translate(StatelessDialog);

+ 1
- 0
react/features/base/dialog/components/index.js Dosyayı Görüntüle

@@ -1,2 +1,3 @@
1 1
 export { default as DialogContainer } from './DialogContainer';
2 2
 export { default as Dialog } from './Dialog';
3
+export { default as StatelessDialog } from './StatelessDialog';

+ 50
- 0
react/features/base/dialog/constants.js Dosyayı Görüntüle

@@ -0,0 +1,50 @@
1
+import React from 'react';
2
+
3
+export const dialogPropTypes = {
4
+    /**
5
+     * Whether cancel button is disabled. Enabled by default.
6
+     */
7
+    cancelDisabled: React.PropTypes.bool,
8
+
9
+    /**
10
+     * Optional i18n key to change the cancel button title.
11
+     */
12
+    cancelTitleKey: React.PropTypes.string,
13
+
14
+    /**
15
+     * Is ok button enabled/disabled. Enabled by default.
16
+     */
17
+    okDisabled: React.PropTypes.bool,
18
+
19
+    /**
20
+     * Optional i18n key to change the ok button title.
21
+     */
22
+    okTitleKey: React.PropTypes.string,
23
+
24
+    /**
25
+     * The handler for onCancel event.
26
+     */
27
+    onCancel: React.PropTypes.func,
28
+
29
+    /**
30
+     * The handler for the event when submitting the dialog.
31
+     */
32
+    onSubmit: React.PropTypes.func,
33
+
34
+    /**
35
+     * Used to obtain translations in children classes.
36
+     */
37
+    t: React.PropTypes.func,
38
+
39
+    /**
40
+     * Key to use for showing a title.
41
+     */
42
+    titleKey: React.PropTypes.string,
43
+
44
+    /**
45
+     * The string to use as a title instead of {@code titleKey}. If a truthy
46
+     * value is specified, it takes precedence over {@code titleKey} i.e.
47
+     * the latter is unused.
48
+     */
49
+    titleString: React.PropTypes.string
50
+};

+ 300
- 0
react/features/device-selection/DeviceSelectionPopup.js Dosyayı Görüntüle

@@ -0,0 +1,300 @@
1
+import Logger from 'jitsi-meet-logger';
2
+import React from 'react';
3
+import ReactDOM from 'react-dom';
4
+import { I18nextProvider } from 'react-i18next';
5
+
6
+import {
7
+    PostMessageTransportBackend,
8
+    Transport
9
+} from '../../../modules/transport';
10
+import { parseURLParams } from '../base/config';
11
+
12
+import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase';
13
+
14
+declare var JitsiMeetJS: Object;
15
+
16
+const logger = Logger.getLogger(__filename);
17
+
18
+/**
19
+ * Implements a class that renders the React components for the device selection
20
+ * popup page and handles the communication between the components and Jitsi
21
+ * Meet.
22
+ */
23
+export default class DeviceSelectionPopup {
24
+    /**
25
+     * Initializes a new DeviceSelectionPopup instance.
26
+     *
27
+     * @param {Object} i18next - The i18next instance used for translation.
28
+     */
29
+    constructor(i18next) {
30
+        this.close = this.close.bind(this);
31
+        this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
32
+        this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
33
+        this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
34
+        this._i18next = i18next;
35
+        const { scope } = parseURLParams(window.location);
36
+
37
+        this._transport = new Transport({
38
+            backend: new PostMessageTransportBackend({
39
+                postisOptions: {
40
+                    scope,
41
+                    window: window.opener
42
+                }
43
+            })
44
+        });
45
+
46
+        this._transport.on('event', event => {
47
+            if (event.name === 'deviceListChanged') {
48
+                this._updateAvailableDevices();
49
+
50
+                return true;
51
+            }
52
+
53
+            return false;
54
+        });
55
+
56
+        this._dialogProps = {
57
+            availableDevices: {},
58
+            currentAudioInputId: '',
59
+            currentAudioOutputId: '',
60
+            currentVideoInputId: '',
61
+            disableAudioInputChange: true,
62
+            disableDeviceChange: true,
63
+            hasAudioPermission: JitsiMeetJS.mediaDevices
64
+                .isDevicePermissionGranted('audio'),
65
+            hasVideoPermission: JitsiMeetJS.mediaDevices
66
+                .isDevicePermissionGranted('video'),
67
+            hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
68
+            hideAudioOutputSelect: true
69
+        };
70
+        this._initState();
71
+    }
72
+
73
+    /**
74
+     * Sends event to Jitsi Meet to close the popup dialog.
75
+     *
76
+     * @returns {void}
77
+     */
78
+    close() {
79
+        this._transport.sendEvent({
80
+            type: 'devices-dialog',
81
+            name: 'close'
82
+        });
83
+    }
84
+
85
+    /**
86
+     * Changes the properties of the react component and re-renders it.
87
+     *
88
+     * @param {Object} newProps - The new properties that will be assigned to
89
+     * the current ones.
90
+     * @returns {void}
91
+     */
92
+    _changeDialogProps(newProps) {
93
+        this._dialogProps = {
94
+            ...this._dialogProps,
95
+            ...newProps
96
+        };
97
+        this._render();
98
+    }
99
+
100
+    /**
101
+     * Returns Promise that resolves with result an list of available devices.
102
+     *
103
+     * @returns {Promise}
104
+     */
105
+    _getAvailableDevices() {
106
+        return this._transport.sendRequest({
107
+            type: 'devices',
108
+            name: 'getAvailableDevices'
109
+        }).catch(e => {
110
+            logger.error(e);
111
+
112
+            return {};
113
+        });
114
+    }
115
+
116
+    /**
117
+     * Returns Promise that resolves with current selected devices.
118
+     *
119
+     * @returns {Promise}
120
+     */
121
+    _getCurrentDevices() {
122
+        return this._transport.sendRequest({
123
+            type: 'devices',
124
+            name: 'getCurrentDevices'
125
+        }).catch(e => {
126
+            logger.error(e);
127
+
128
+            return {};
129
+        });
130
+    }
131
+
132
+    /**
133
+     * Initializes the state.
134
+     *
135
+     * @returns {void}
136
+     */
137
+    _initState() {
138
+        return Promise.all([
139
+            this._getAvailableDevices(),
140
+            this._isDeviceListAvailable(),
141
+            this._isDeviceChangeAvailable(),
142
+            this._getCurrentDevices(),
143
+            this._isMultipleAudioInputSupported()
144
+        ]).then(([
145
+            availableDevices,
146
+            listAvailable,
147
+            changeAvailable,
148
+            currentDevices,
149
+            multiAudioInputSupported
150
+        ]) => {
151
+            this._changeDialogProps({
152
+                availableDevices,
153
+                currentAudioInputId: currentDevices.audioInput,
154
+                currentAudioOutputId: currentDevices.audioOutput,
155
+                currentVideoInputId: currentDevices.videoInput,
156
+                disableAudioInputChange: !multiAudioInputSupported,
157
+                disableDeviceChange: !listAvailable || !changeAvailable,
158
+                hideAudioOutputSelect: !changeAvailable
159
+            });
160
+        });
161
+    }
162
+
163
+    /**
164
+     * Returns Promise that resolves with true if the device change is available
165
+     * and with false if not.
166
+     *
167
+     * @returns {Promise}
168
+     */
169
+    _isDeviceChangeAvailable() {
170
+        return this._transport.sendRequest({
171
+            type: 'devices',
172
+            name: 'isDeviceChangeAvailable'
173
+        }).catch(e => {
174
+            logger.error(e);
175
+
176
+            return false;
177
+        });
178
+    }
179
+
180
+    /**
181
+     * Returns Promise that resolves with true if the device list is available
182
+     * and with false if not.
183
+     *
184
+     * @returns {Promise}
185
+     */
186
+    _isDeviceListAvailable() {
187
+        return this._transport.sendRequest({
188
+            type: 'devices',
189
+            name: 'isDeviceListAvailable'
190
+        }).catch(e => {
191
+            logger.error(e);
192
+
193
+            return false;
194
+        });
195
+    }
196
+
197
+    /**
198
+     * Returns Promise that resolves with true if the device list is available
199
+     * and with false if not.
200
+     *
201
+     * @returns {Promise}
202
+     */
203
+    _isMultipleAudioInputSupported() {
204
+        return this._transport.sendRequest({
205
+            type: 'devices',
206
+            name: 'isMultipleAudioInputSupported'
207
+        }).catch(e => {
208
+            logger.error(e);
209
+
210
+            return false;
211
+        });
212
+    }
213
+
214
+    /**
215
+     * Renders the React components for the popup page.
216
+     *
217
+     * @returns {void}
218
+     */
219
+    _render() {
220
+        const props = {
221
+            ...this._dialogProps,
222
+            closeModal: this.close,
223
+            disableBlanketClickDismiss: true,
224
+            setAudioInputDevice: this._setAudioInputDevice,
225
+            setAudioOutputDevice: this._setAudioOutputDevice,
226
+            setVideoInputDevice: this._setVideoInputDevice
227
+        };
228
+
229
+        ReactDOM.render(
230
+            <I18nextProvider
231
+                i18n = { this._i18next }>
232
+                <DeviceSelectionDialogBase { ...props } />
233
+            </I18nextProvider>,
234
+            document.getElementById('react'));
235
+    }
236
+
237
+    /**
238
+     * Sets the audio input device to the one with the id that is passed.
239
+     *
240
+     * @param {string} id - The id of the new device.
241
+     * @returns {Promise}
242
+     */
243
+    _setAudioInputDevice(id) {
244
+        return this._setDevice({
245
+            id,
246
+            kind: 'audioinput'
247
+        });
248
+    }
249
+
250
+    /**
251
+     * Sets the audio output device to the one with the id that is passed.
252
+     *
253
+     * @param {string} id - The id of the new device.
254
+     * @returns {Promise}
255
+     */
256
+    _setAudioOutputDevice(id) {
257
+        return this._setDevice({
258
+            id,
259
+            kind: 'audiooutput'
260
+        });
261
+    }
262
+
263
+    /**
264
+     * Sets the currently used device to the one that is passed.
265
+     *
266
+     * @param {Object} device - The new device to be used.
267
+     * @returns {Promise}
268
+     */
269
+    _setDevice(device) {
270
+        return this._transport.sendRequest({
271
+            type: 'devices',
272
+            name: 'setDevice',
273
+            device
274
+        });
275
+    }
276
+
277
+    /**
278
+     * Sets the video input device to the one with the id that is passed.
279
+     *
280
+     * @param {string} id - The id of the new device.
281
+     * @returns {Promise}
282
+     */
283
+    _setVideoInputDevice(id) {
284
+        return this._setDevice({
285
+            id,
286
+            kind: 'videoinput'
287
+        });
288
+    }
289
+
290
+    /**
291
+     * Updates the available devices.
292
+     *
293
+     * @returns {void}
294
+     */
295
+    _updateAvailableDevices() {
296
+        this._getAvailableDevices().then(devices =>
297
+            this._changeDialogProps({ availableDevices: devices })
298
+        );
299
+    }
300
+}

+ 10
- 0
react/features/device-selection/actionTypes.js Dosyayı Görüntüle

@@ -0,0 +1,10 @@
1
+/**
2
+ * The type of Redux action which Sets information about device selection popup.
3
+ *
4
+ * {{
5
+ *     type: SET_DEVICE_SELECTION_POPUP_DATA,
6
+ *     popupDialogData: Object
7
+ * }}
8
+ */
9
+export const SET_DEVICE_SELECTION_POPUP_DATA
10
+    = Symbol('SET_DEVICE_SELECTION_POPUP_DATA');

+ 175
- 1
react/features/device-selection/actions.js Dosyayı Görüntüle

@@ -1,8 +1,20 @@
1
-/* globals APP */
1
+/* globals APP, interfaceConfig */
2 2
 
3 3
 import { openDialog } from '../base/dialog';
4 4
 import JitsiMeetJS from '../base/lib-jitsi-meet';
5
+import { API_ID } from '../../../modules/API/constants';
6
+import {
7
+    setAudioInputDevice,
8
+    setAudioOutputDevice,
9
+    setVideoInputDevice
10
+} from '../base/devices';
11
+import { i18next } from '../base/i18n';
12
+import {
13
+    PostMessageTransportBackend,
14
+    Transport
15
+} from '../../../modules/transport';
5 16
 
17
+import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
6 18
 import { DeviceSelectionDialog } from './components';
7 19
 
8 20
 /**
@@ -13,6 +25,21 @@ import { DeviceSelectionDialog } from './components';
13 25
  */
14 26
 export function openDeviceSelectionDialog() {
15 27
     return dispatch => {
28
+        if (interfaceConfig.filmStripOnly) {
29
+            dispatch(_openDeviceSelectionDialogInPopup());
30
+        } else {
31
+            dispatch(_openDeviceSelectionDialogHere());
32
+        }
33
+    };
34
+}
35
+
36
+/**
37
+ * Opens the DeviceSelectionDialog in the same window.
38
+ *
39
+ * @returns {Function}
40
+ */
41
+function _openDeviceSelectionDialogHere() {
42
+    return dispatch =>
16 43
         JitsiMeetJS.mediaDevices.isDeviceListAvailable()
17 44
             .then(isDeviceListAvailable => {
18 45
                 dispatch(openDialog(DeviceSelectionDialog, {
@@ -33,5 +60,152 @@ export function openDeviceSelectionDialog() {
33 60
                         .isDeviceChangeAvailable('output')
34 61
                 }));
35 62
             });
63
+}
64
+
65
+/**
66
+ * Opens a popup window with the device selection dialog in it.
67
+ *
68
+ * @returns {Function}
69
+ */
70
+function _openDeviceSelectionDialogInPopup() {
71
+    return (dispatch, getState) => {
72
+        const { popupDialogData } = getState()['features/device-selection'];
73
+
74
+        if (popupDialogData) {
75
+            popupDialogData.popup.focus();
76
+
77
+            return;
78
+        }
79
+
80
+        // API_ID will always be defined because the iframe api is enabled
81
+        const scope = `dialog_${API_ID}`;
82
+        const url = `static/deviceSelectionPopup.html#scope=${
83
+            encodeURIComponent(JSON.stringify(scope))}`;
84
+        const popup
85
+            = window.open(
86
+                url,
87
+                'device-selection-popup',
88
+                'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');
89
+
90
+        popup.addEventListener('DOMContentLoaded', () => {
91
+            popup.init(i18next);
92
+        });
93
+
94
+        const transport = new Transport({
95
+            backend: new PostMessageTransportBackend({
96
+                postisOptions: {
97
+                    scope,
98
+                    window: popup
99
+                }
100
+            })
101
+        });
102
+
103
+        transport.on('request',
104
+            _processRequest.bind(undefined, dispatch, getState));
105
+        transport.on('event', event => {
106
+            if (event.type === 'devices-dialog' && event.name === 'close') {
107
+                popup.close();
108
+                transport.dispose();
109
+                dispatch(_setDeviceSelectionPopupData());
110
+
111
+                return true;
112
+            }
113
+
114
+            return false;
115
+        });
116
+
117
+        dispatch(_setDeviceSelectionPopupData({
118
+            popup,
119
+            transport
120
+        }));
121
+    };
122
+}
123
+
124
+/**
125
+ * Processes device requests from external applications.
126
+ *
127
+ * @param {Dispatch} dispatch - The redux {@code dispatch} function.
128
+ * @param {Function} getState - The redux function that gets/retrieves the redux
129
+ * state.
130
+ * @param {Object} request - The request to be processed.
131
+ * @param {Function} responseCallback - The callback that will send the
132
+ * response.
133
+ * @returns {boolean}
134
+ */ // eslint-disable-next-line max-params
135
+function _processRequest(dispatch, getState, request, responseCallback) {
136
+    if (request.type === 'devices') {
137
+        switch (request.name) {
138
+        case 'isDeviceListAvailable':
139
+            JitsiMeetJS.mediaDevices.isDeviceListAvailable()
140
+                .then(isDeviceListAvailable =>
141
+                    responseCallback(isDeviceListAvailable))
142
+                .catch(e => responseCallback(null, e));
143
+            break;
144
+        case 'isDeviceChangeAvailable':
145
+            responseCallback(
146
+                JitsiMeetJS.mediaDevices.isDeviceChangeAvailable());
147
+            break;
148
+        case 'isMultipleAudioInputSupported':
149
+            responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
150
+            break;
151
+        case 'getCurrentDevices':
152
+            responseCallback({
153
+                audioInput: APP.settings.getMicDeviceId(),
154
+                audioOutput: APP.settings.getAudioOutputDeviceId(),
155
+                videoInput: APP.settings.getCameraDeviceId()
156
+            });
157
+            break;
158
+        case 'getAvailableDevices':
159
+            responseCallback(getState()['features/base/devices']);
160
+            break;
161
+        case 'setDevice': {
162
+            let action;
163
+            const { device } = request;
164
+
165
+            switch (device.kind) {
166
+            case 'audioinput':
167
+                action = setAudioInputDevice;
168
+                break;
169
+            case 'audiooutput':
170
+                action = setAudioOutputDevice;
171
+                break;
172
+            case 'videoinput':
173
+                action = setVideoInputDevice;
174
+                break;
175
+            default:
176
+
177
+            }
178
+            dispatch(action(device.id));
179
+            responseCallback(true);
180
+            break;
181
+        }
182
+        default:
183
+
184
+            return false;
185
+        }
186
+
187
+        return true;
188
+    }
189
+
190
+    return false;
191
+}
192
+
193
+/**
194
+ * Sets information about device selection popup in the store.
195
+ *
196
+ * @param {Object} popupDialogData - Information about the popup.
197
+ * @param {Object} popupDialog.popup - The popup object returned from
198
+ * window.open.
199
+ * @param {Object} popupDialogData.transport - The transport instance used for
200
+ * communication with the popup window.
201
+ * @returns {{
202
+ *     type: SET_DEVICE_SELECTION_POPUP_DATA,
203
+ *     popupDialogData: Object
204
+ * }}
205
+ */
206
+function _setDeviceSelectionPopupData(popupDialogData) {
207
+    return {
208
+        type: SET_DEVICE_SELECTION_POPUP_DATA,
209
+        popupDialogData
36 210
     };
37 211
 }

+ 1
- 1
react/features/device-selection/components/AudioInputPreview.js Dosyayı Görüntüle

@@ -53,7 +53,7 @@ class AudioInputPreview extends PureComponent {
53 53
      */
54 54
     componentWillReceiveProps(nextProps) {
55 55
         this._listenForAudioUpdates(nextProps.track);
56
-        this._updateAudioLevel(0);
56
+        this._updateAudioLevel(undefined, 0);
57 57
     }
58 58
 
59 59
     /**

+ 43
- 410
react/features/device-selection/components/DeviceSelectionDialog.js Dosyayı Görüntüle

@@ -6,17 +6,9 @@ import {
6 6
     setAudioOutputDevice,
7 7
     setVideoInputDevice
8 8
 } from '../../base/devices';
9
-import {
10
-    Dialog,
11
-    hideDialog
12
-} from '../../base/dialog';
13
-import { translate } from '../../base/i18n';
14
-import { createLocalTrack } from '../../base/lib-jitsi-meet';
9
+import { hideDialog } from '../../base/dialog';
15 10
 
16
-import AudioInputPreview from './AudioInputPreview';
17
-import AudioOutputPreview from './AudioOutputPreview';
18
-import DeviceSelector from './DeviceSelector';
19
-import VideoInputPreview from './VideoInputPreview';
11
+import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
20 12
 
21 13
 /**
22 14
  * React component for previewing and selecting new audio and video sources.
@@ -96,417 +88,58 @@ class DeviceSelectionDialog extends Component {
96 88
          * rendered. This is specifically used for hiding audio output on
97 89
          * temasys browsers which do not support such change.
98 90
          */
99
-        hideAudioOutputSelect: React.PropTypes.bool,
100
-
101
-        /**
102
-         * Invoked to obtain translated strings.
103
-         */
104
-        t: React.PropTypes.func
91
+        hideAudioOutputSelect: React.PropTypes.bool
105 92
     };
106 93
 
107
-    /**
108
-     * Initializes a new DeviceSelectionDialog instance.
109
-     *
110
-     * @param {Object} props - The read-only React Component props with which
111
-     * the new instance is to be initialized.
112
-     */
113
-    constructor(props) {
114
-        super(props);
115
-
116
-        const { _availableDevices } = this.props;
117
-
118
-        this.state = {
119
-            // JitsiLocalTrack to use for live previewing of audio input.
120
-            previewAudioTrack: null,
121
-
122
-            // JitsiLocalTrack to use for live previewing of video input.
123
-            previewVideoTrack: null,
124
-
125
-            // An message describing a problem with obtaining a video preview.
126
-            previewVideoTrackError: null,
127
-
128
-            // The audio input device id to show as selected by default.
129
-            selectedAudioInputId: this.props.currentAudioInputId || '',
130
-
131
-            // The audio output device id to show as selected by default.
132
-            selectedAudioOutputId: this.props.currentAudioOutputId || '',
133
-
134
-            // The video input device id to show as selected by default.
135
-            // FIXME: On temasys, without a device selected and put into local
136
-            // storage as the default device to use, the current video device id
137
-            // is a blank string. This is because the library gets a local video
138
-            // track and then maps the track's device id by matching the track's
139
-            // label to the MediaDeviceInfos returned from enumerateDevices. In
140
-            // WebRTC, the track label is expected to return the camera device
141
-            // label. However, temasys video track labels refer to track id, not
142
-            // device label, so the library cannot match the track to a device.
143
-            // The workaround of defaulting to the first videoInput available
144
-            // is re-used from the previous device settings implementation.
145
-            selectedVideoInputId: this.props.currentVideoInputId
146
-                || (_availableDevices.videoInput
147
-                    && _availableDevices.videoInput[0]
148
-                    && _availableDevices.videoInput[0].deviceId)
149
-                || ''
150
-        };
151
-
152
-        // Preventing closing while cleaning up previews is important for
153
-        // supporting temasys video cleanup. Temasys requires its video object
154
-        // to be in the dom and visible for proper detaching of tracks. Delaying
155
-        // closure until cleanup is complete ensures no errors in the process.
156
-        this._isClosing = false;
157
-
158
-        // Bind event handlers so they are only bound once for every instance.
159
-        this._closeModal = this._closeModal.bind(this);
160
-        this._onCancel = this._onCancel.bind(this);
161
-        this._onSubmit = this._onSubmit.bind(this);
162
-        this._updateAudioOutput = this._updateAudioOutput.bind(this);
163
-        this._updateAudioInput = this._updateAudioInput.bind(this);
164
-        this._updateVideoInput = this._updateVideoInput.bind(this);
165
-    }
166
-
167
-    /**
168
-     * Sets default device choices so a choice is pre-selected in the dropdowns
169
-     * and live previews are created.
170
-     *
171
-     * @inheritdoc
172
-     */
173
-    componentDidMount() {
174
-        this._updateAudioOutput(this.state.selectedAudioOutputId);
175
-        this._updateAudioInput(this.state.selectedAudioInputId);
176
-        this._updateVideoInput(this.state.selectedVideoInputId);
177
-    }
178
-
179
-    /**
180
-     * Disposes preview tracks that might not already be disposed.
181
-     *
182
-     * @inheritdoc
183
-     */
184
-    componentWillUnmount() {
185
-        // This handles the case where neither submit nor cancel were triggered,
186
-        // such as on modal switch. In that case, make a dying attempt to clean
187
-        // up previews.
188
-        if (!this._isClosing) {
189
-            this._attemptPreviewTrackCleanup();
190
-        }
191
-    }
192
-
193 94
     /**
194 95
      * Implements React's {@link Component#render()}.
195 96
      *
196 97
      * @inheritdoc
197 98
      */
198 99
     render() {
199
-        return (
200
-            <Dialog
201
-                cancelTitleKey = { 'dialog.Cancel' }
202
-                okTitleKey = { 'dialog.Save' }
203
-                onCancel = { this._onCancel }
204
-                onSubmit = { this._onSubmit }
205
-                titleKey = 'deviceSelection.deviceSettings' >
206
-                <div className = 'device-selection'>
207
-                    <div className = 'device-selection-column column-video'>
208
-                        <div className = 'device-selection-video-container'>
209
-                            <VideoInputPreview
210
-                                error = { this.state.previewVideoTrackError }
211
-                                track = { this.state.previewVideoTrack } />
212
-                        </div>
213
-                        { this._renderAudioInputPreview() }
214
-                    </div>
215
-                    <div className = 'device-selection-column column-selectors'>
216
-                        <div className = 'device-selectors'>
217
-                            { this._renderSelectors() }
218
-                        </div>
219
-                        { this._renderAudioOutputPreview() }
220
-                    </div>
221
-                </div>
222
-            </Dialog>
223
-        );
224
-    }
225
-
226
-    /**
227
-     * Cleans up preview tracks if they are not active tracks.
228
-     *
229
-     * @private
230
-     * @returns {Array<Promise>} Zero to two promises will be returned. One
231
-     * promise can be for video cleanup and another for audio cleanup.
232
-     */
233
-    _attemptPreviewTrackCleanup() {
234
-        return Promise.all([
235
-            this._disposeVideoPreview(),
236
-            this._disposeAudioPreview()
237
-        ]);
238
-    }
239
-
240
-    /**
241
-     * Signals to close DeviceSelectionDialog.
242
-     *
243
-     * @private
244
-     * @returns {void}
245
-     */
246
-    _closeModal() {
247
-        this.props.dispatch(hideDialog());
248
-    }
249
-
250
-    /**
251
-     * Utility function for disposing the current audio preview.
252
-     *
253
-     * @private
254
-     * @returns {Promise}
255
-     */
256
-    _disposeAudioPreview() {
257
-        return this.state.previewAudioTrack
258
-            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
259
-    }
260
-
261
-    /**
262
-     * Utility function for disposing the current video preview.
263
-     *
264
-     * @private
265
-     * @returns {Promise}
266
-     */
267
-    _disposeVideoPreview() {
268
-        return this.state.previewVideoTrack
269
-            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
270
-    }
271
-
272
-    /**
273
-     * Disposes preview tracks and signals to close DeviceSelectionDialog.
274
-     *
275
-     * @private
276
-     * @returns {boolean} Returns false to prevent closure until cleanup is
277
-     * complete.
278
-     */
279
-    _onCancel() {
280
-        if (this._isClosing) {
281
-            return false;
282
-        }
283
-
284
-        this._isClosing = true;
285
-
286
-        const cleanupPromises = this._attemptPreviewTrackCleanup();
287
-
288
-        Promise.all(cleanupPromises)
289
-            .then(this._closeModal)
290
-            .catch(this._closeModal);
291
-
292
-        return false;
293
-    }
294
-
295
-    /**
296
-     * Identifies changes to the preferred input/output devices and perform
297
-     * necessary cleanup and requests to use those devices. Closes the modal
298
-     * after cleanup and device change requests complete.
299
-     *
300
-     * @private
301
-     * @returns {boolean} Returns false to prevent closure until cleanup is
302
-     * complete.
303
-     */
304
-    _onSubmit() {
305
-        if (this._isClosing) {
306
-            return false;
307
-        }
308
-
309
-        this._isClosing = true;
310
-
311
-        const deviceChangePromises = this._attemptPreviewTrackCleanup()
312
-            .then(() => {
313
-                if (this.state.selectedVideoInputId
314
-                        !== this.props.currentVideoInputId) {
315
-                    this.props.dispatch(
316
-                        setVideoInputDevice(this.state.selectedVideoInputId));
317
-                }
318
-
319
-                if (this.state.selectedAudioInputId
320
-                        !== this.props.currentAudioInputId) {
321
-                    this.props.dispatch(
322
-                        setAudioInputDevice(this.state.selectedAudioInputId));
323
-                }
324
-
325
-                if (this.state.selectedAudioOutputId
326
-                        !== this.props.currentAudioOutputId) {
327
-                    this.props.dispatch(
328
-                        setAudioOutputDevice(this.state.selectedAudioOutputId));
329
-                }
330
-            });
331
-
332
-        Promise.all(deviceChangePromises)
333
-            .then(this._closeModal)
334
-            .catch(this._closeModal);
335
-
336
-        return false;
337
-    }
338
-
339
-    /**
340
-     * Creates an AudioInputPreview for previewing if audio is being received.
341
-     * Null will be returned if local stats for tracking audio input levels
342
-     * cannot be obtained.
343
-     *
344
-     * @private
345
-     * @returns {ReactComponent|null}
346
-     */
347
-    _renderAudioInputPreview() {
348
-        if (this.props.hideAudioInputPreview) {
349
-            return null;
350
-        }
351
-
352
-        return (
353
-            <AudioInputPreview
354
-                track = { this.state.previewAudioTrack } />
355
-        );
356
-    }
357
-
358
-    /**
359
-     * Creates an AudioOutputPreview instance for playing a test sound with the
360
-     * passed in device id. Null will be returned if hideAudioOutput is truthy.
361
-     *
362
-     * @private
363
-     * @returns {ReactComponent|null}
364
-     */
365
-    _renderAudioOutputPreview() {
366
-        if (this.props.hideAudioOutputSelect) {
367
-            return null;
368
-        }
369
-
370
-        return (
371
-            <AudioOutputPreview
372
-                deviceId = { this.state.selectedAudioOutputId } />
373
-        );
374
-    }
375
-
376
-    /**
377
-     * Creates a DeviceSelector instance based on the passed in configuration.
378
-     *
379
-     * @private
380
-     * @param {Object} props - The props for the DeviceSelector.
381
-     * @returns {ReactElement}
382
-     */
383
-    _renderSelector(props) {
384
-        return (
385
-            <DeviceSelector { ...props } />
386
-        );
387
-    }
388
-
389
-    /**
390
-     * Creates DeviceSelector instances for video output, audio input, and audio
391
-     * output.
392
-     *
393
-     * @private
394
-     * @returns {Array<ReactElement>} DeviceSelector instances.
395
-     */
396
-    _renderSelectors() {
397
-        const { _availableDevices } = this.props;
398
-        const configurations = [
399
-            {
400
-                devices: _availableDevices.videoInput,
401
-                hasPermission: this.props.hasVideoPermission,
402
-                icon: 'icon-camera',
403
-                isDisabled: this.props.disableDeviceChange,
404
-                key: 'videoInput',
405
-                label: 'settings.selectCamera',
406
-                onSelect: this._updateVideoInput,
407
-                selectedDeviceId: this.state.selectedVideoInputId
100
+        const {
101
+            currentAudioInputId,
102
+            currentAudioOutputId,
103
+            currentVideoInputId,
104
+            disableAudioInputChange,
105
+            disableDeviceChange,
106
+            dispatch,
107
+            hasAudioPermission,
108
+            hasVideoPermission,
109
+            hideAudioInputPreview,
110
+            hideAudioOutputSelect
111
+        } = this.props;
112
+
113
+        const props = {
114
+            availableDevices: this.props._availableDevices,
115
+            closeModal: () => dispatch(hideDialog()),
116
+            currentAudioInputId,
117
+            currentAudioOutputId,
118
+            currentVideoInputId,
119
+            disableAudioInputChange,
120
+            disableDeviceChange,
121
+            hasAudioPermission,
122
+            hasVideoPermission,
123
+            hideAudioInputPreview,
124
+            hideAudioOutputSelect,
125
+            setAudioInputDevice: id => {
126
+                dispatch(setAudioInputDevice(id));
127
+
128
+                return Promise.resolve();
408 129
             },
409
-            {
410
-                devices: _availableDevices.audioInput,
411
-                hasPermission: this.props.hasAudioPermission,
412
-                icon: 'icon-microphone',
413
-                isDisabled: this.props.disableAudioInputChange
414
-                    || this.props.disableDeviceChange,
415
-                key: 'audioInput',
416
-                label: 'settings.selectMic',
417
-                onSelect: this._updateAudioInput,
418
-                selectedDeviceId: this.state.selectedAudioInputId
419
-            }
420
-        ];
421
-
422
-        if (!this.props.hideAudioOutputSelect) {
423
-            configurations.push({
424
-                devices: _availableDevices.audioOutput,
425
-                hasPermission: this.props.hasAudioPermission
426
-                    || this.props.hasVideoPermission,
427
-                icon: 'icon-volume',
428
-                isDisabled: this.props.disableDeviceChange,
429
-                key: 'audioOutput',
430
-                label: 'settings.selectAudioOutput',
431
-                onSelect: this._updateAudioOutput,
432
-                selectedDeviceId: this.state.selectedAudioOutputId
433
-            });
434
-        }
130
+            setAudioOutputDevice: id => {
131
+                dispatch(setAudioOutputDevice(id));
435 132
 
436
-        return configurations.map(this._renderSelector);
437
-    }
438
-
439
-    /**
440
-     * Callback invoked when a new audio input device has been selected. Updates
441
-     * the internal state of the user's selection as well as the audio track
442
-     * that should display in the preview.
443
-     *
444
-     * @param {string} deviceId - The id of the chosen audio input device.
445
-     * @private
446
-     * @returns {void}
447
-     */
448
-    _updateAudioInput(deviceId) {
449
-        this.setState({
450
-            selectedAudioInputId: deviceId
451
-        }, () => {
452
-            this._disposeAudioPreview()
453
-                .then(() => createLocalTrack('audio', deviceId))
454
-                .then(jitsiLocalTrack => {
455
-                    this.setState({
456
-                        previewAudioTrack: jitsiLocalTrack
457
-                    });
458
-                })
459
-                .catch(() => {
460
-                    this.setState({
461
-                        previewAudioTrack: null
462
-                    });
463
-                });
464
-        });
465
-    }
133
+                return Promise.resolve();
134
+            },
135
+            setVideoInputDevice: id => {
136
+                dispatch(setVideoInputDevice(id));
466 137
 
467
-    /**
468
-     * Callback invoked when a new audio output device has been selected.
469
-     * Updates the internal state of the user's selection.
470
-     *
471
-     * @param {string} deviceId - The id of the chosen audio output device.
472
-     * @private
473
-     * @returns {void}
474
-     */
475
-    _updateAudioOutput(deviceId) {
476
-        this.setState({
477
-            selectedAudioOutputId: deviceId
478
-        });
479
-    }
138
+                return Promise.resolve();
139
+            }
140
+        };
480 141
 
481
-    /**
482
-     * Callback invoked when a new video input device has been selected. Updates
483
-     * the internal state of the user's selection as well as the video track
484
-     * that should display in the preview.
485
-     *
486
-     * @param {string} deviceId - The id of the chosen video input device.
487
-     * @private
488
-     * @returns {void}
489
-     */
490
-    _updateVideoInput(deviceId) {
491
-        this.setState({
492
-            selectedVideoInputId: deviceId
493
-        }, () => {
494
-            this._disposeVideoPreview()
495
-                .then(() => createLocalTrack('video', deviceId))
496
-                .then(jitsiLocalTrack => {
497
-                    this.setState({
498
-                        previewVideoTrack: jitsiLocalTrack,
499
-                        previewVideoTrackError: null
500
-                    });
501
-                })
502
-                .catch(() => {
503
-                    this.setState({
504
-                        previewVideoTrack: null,
505
-                        previewVideoTrackError:
506
-                            this.props.t('deviceSelection.previewUnavailable')
507
-                    });
508
-                });
509
-        });
142
+        return <DeviceSelectionDialogBase { ...props } />;
510 143
     }
511 144
 }
512 145
 
@@ -526,4 +159,4 @@ function _mapStateToProps(state) {
526 159
     };
527 160
 }
528 161
 
529
-export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
162
+export default connect(_mapStateToProps)(DeviceSelectionDialog);

+ 529
- 0
react/features/device-selection/components/DeviceSelectionDialogBase.js Dosyayı Görüntüle

@@ -0,0 +1,529 @@
1
+import React, { Component } from 'react';
2
+
3
+import { StatelessDialog } from '../../base/dialog';
4
+import { translate } from '../../base/i18n';
5
+import { createLocalTrack } from '../../base/lib-jitsi-meet';
6
+
7
+import AudioInputPreview from './AudioInputPreview';
8
+import AudioOutputPreview from './AudioOutputPreview';
9
+import DeviceSelector from './DeviceSelector';
10
+import VideoInputPreview from './VideoInputPreview';
11
+
12
+/**
13
+ * React component for previewing and selecting new audio and video sources.
14
+ *
15
+ * @extends Component
16
+ */
17
+class DeviceSelectionDialogBase extends Component {
18
+    /**
19
+     * DeviceSelectionDialogBase component's property types.
20
+     *
21
+     * @static
22
+     */
23
+    static propTypes = {
24
+       /**
25
+         * All known audio and video devices split by type. This prop comes from
26
+         * the app state.
27
+         */
28
+        availableDevices: React.PropTypes.object,
29
+
30
+        /**
31
+         * Closes the dialog.
32
+         */
33
+        closeModal: React.PropTypes.func,
34
+
35
+        /**
36
+         * Device id for the current audio input device. This device will be set
37
+         * as the default audio input device to preview.
38
+         */
39
+        currentAudioInputId: React.PropTypes.string,
40
+
41
+        /**
42
+         * Device id for the current audio output device. This device will be
43
+         * set as the default audio output device to preview.
44
+         */
45
+        currentAudioOutputId: React.PropTypes.string,
46
+
47
+        /**
48
+         * Device id for the current video input device. This device will be set
49
+         * as the default video input device to preview.
50
+         */
51
+        currentVideoInputId: React.PropTypes.string,
52
+
53
+        /**
54
+         * Whether or not the audio selector can be interacted with. If true,
55
+         * the audio input selector will be rendered as disabled. This is
56
+         * specifically used to prevent audio device changing in Firefox, which
57
+         * currently does not work due to a browser-side regression.
58
+         */
59
+        disableAudioInputChange: React.PropTypes.bool,
60
+
61
+        /**
62
+         * Disables dismissing the dialog when the blanket is clicked. Enabled
63
+         * by default.
64
+         */
65
+        disableBlanketClickDismiss: React.PropTypes.bool,
66
+
67
+        /**
68
+         * True if device changing is configured to be disallowed. Selectors
69
+         * will display as disabled.
70
+         */
71
+        disableDeviceChange: React.PropTypes.bool,
72
+
73
+        /**
74
+         * Whether or not a new audio input source can be selected.
75
+         */
76
+        hasAudioPermission: React.PropTypes.bool,
77
+
78
+        /**
79
+         * Whether or not a new video input sources can be selected.
80
+         */
81
+        hasVideoPermission: React.PropTypes.bool,
82
+
83
+        /**
84
+         * If true, the audio meter will not display. Necessary for browsers or
85
+         * configurations that do not support local stats to prevent a
86
+         * non-responsive mic preview from displaying.
87
+         */
88
+        hideAudioInputPreview: React.PropTypes.bool,
89
+
90
+        /**
91
+         * Whether or not the audio output source selector should display. If
92
+         * true, the audio output selector and test audio link will not be
93
+         * rendered. This is specifically used for hiding audio output on
94
+         * temasys browsers which do not support such change.
95
+         */
96
+        hideAudioOutputSelect: React.PropTypes.bool,
97
+
98
+        /**
99
+         * Function that sets the audio input device.
100
+         */
101
+        setAudioInputDevice: React.PropTypes.func,
102
+
103
+        /**
104
+         * Function that sets the audio output device.
105
+         */
106
+        setAudioOutputDevice: React.PropTypes.func,
107
+
108
+        /**
109
+         * Function that sets the video input device.
110
+         */
111
+        setVideoInputDevice: React.PropTypes.func,
112
+
113
+        /**
114
+         * Invoked to obtain translated strings.
115
+         */
116
+        t: React.PropTypes.func
117
+    };
118
+
119
+    /**
120
+     * Initializes a new DeviceSelectionDialogBase instance.
121
+     *
122
+     * @param {Object} props - The read-only React Component props with which
123
+     * the new instance is to be initialized.
124
+     */
125
+    constructor(props) {
126
+        super(props);
127
+
128
+        const { availableDevices } = this.props;
129
+
130
+        this.state = {
131
+            // JitsiLocalTrack to use for live previewing of audio input.
132
+            previewAudioTrack: null,
133
+
134
+            // JitsiLocalTrack to use for live previewing of video input.
135
+            previewVideoTrack: null,
136
+
137
+            // An message describing a problem with obtaining a video preview.
138
+            previewVideoTrackError: null,
139
+
140
+            // The audio input device id to show as selected by default.
141
+            selectedAudioInputId: this.props.currentAudioInputId || '',
142
+
143
+            // The audio output device id to show as selected by default.
144
+            selectedAudioOutputId: this.props.currentAudioOutputId || '',
145
+
146
+            // The video input device id to show as selected by default.
147
+            // FIXME: On temasys, without a device selected and put into local
148
+            // storage as the default device to use, the current video device id
149
+            // is a blank string. This is because the library gets a local video
150
+            // track and then maps the track's device id by matching the track's
151
+            // label to the MediaDeviceInfos returned from enumerateDevices. In
152
+            // WebRTC, the track label is expected to return the camera device
153
+            // label. However, temasys video track labels refer to track id, not
154
+            // device label, so the library cannot match the track to a device.
155
+            // The workaround of defaulting to the first videoInput available
156
+            // is re-used from the previous device settings implementation.
157
+            selectedVideoInputId: this.props.currentVideoInputId
158
+                || (availableDevices.videoInput
159
+                    && availableDevices.videoInput[0]
160
+                    && availableDevices.videoInput[0].deviceId)
161
+                || ''
162
+        };
163
+
164
+        // Preventing closing while cleaning up previews is important for
165
+        // supporting temasys video cleanup. Temasys requires its video object
166
+        // to be in the dom and visible for proper detaching of tracks. Delaying
167
+        // closure until cleanup is complete ensures no errors in the process.
168
+        this._isClosing = false;
169
+
170
+        this._setDevicesAndClose = this._setDevicesAndClose.bind(this);
171
+        this._onCancel = this._onCancel.bind(this);
172
+        this._onSubmit = this._onSubmit.bind(this);
173
+        this._updateAudioOutput = this._updateAudioOutput.bind(this);
174
+        this._updateAudioInput = this._updateAudioInput.bind(this);
175
+        this._updateVideoInput = this._updateVideoInput.bind(this);
176
+    }
177
+
178
+    /**
179
+     * Sets default device choices so a choice is pre-selected in the dropdowns
180
+     * and live previews are created.
181
+     *
182
+     * @inheritdoc
183
+     */
184
+    componentDidMount() {
185
+        this._updateAudioOutput(this.state.selectedAudioOutputId);
186
+        this._updateAudioInput(this.state.selectedAudioInputId);
187
+        this._updateVideoInput(this.state.selectedVideoInputId);
188
+    }
189
+
190
+    /**
191
+     * Disposes preview tracks that might not already be disposed.
192
+     *
193
+     * @inheritdoc
194
+     */
195
+    componentWillUnmount() {
196
+        // This handles the case where neither submit nor cancel were triggered,
197
+        // such as on modal switch. In that case, make a dying attempt to clean
198
+        // up previews.
199
+        if (!this._isClosing) {
200
+            this._attemptPreviewTrackCleanup();
201
+        }
202
+    }
203
+
204
+    /**
205
+     * Implements React's {@link Component#render()}.
206
+     *
207
+     * @inheritdoc
208
+     */
209
+    render() {
210
+        return (
211
+            <StatelessDialog
212
+                cancelTitleKey = { 'dialog.Cancel' }
213
+                disableBlanketClickDismiss
214
+                    = { this.props.disableBlanketClickDismiss }
215
+                okTitleKey = { 'dialog.Save' }
216
+                onCancel = { this._onCancel }
217
+                onSubmit = { this._onSubmit }
218
+                titleKey = 'deviceSelection.deviceSettings'>
219
+                <div className = 'device-selection'>
220
+                    <div className = 'device-selection-column column-video'>
221
+                        <div className = 'device-selection-video-container'>
222
+                            <VideoInputPreview
223
+                                error = { this.state.previewVideoTrackError }
224
+                                track = { this.state.previewVideoTrack } />
225
+                        </div>
226
+                        { this._renderAudioInputPreview() }
227
+                    </div>
228
+                    <div className = 'device-selection-column column-selectors'>
229
+                        <div className = 'device-selectors'>
230
+                            { this._renderSelectors() }
231
+                        </div>
232
+                        { this._renderAudioOutputPreview() }
233
+                    </div>
234
+                </div>
235
+            </StatelessDialog>
236
+        );
237
+    }
238
+
239
+    /**
240
+     * Cleans up preview tracks if they are not active tracks.
241
+     *
242
+     * @private
243
+     * @returns {Array<Promise>} Zero to two promises will be returned. One
244
+     * promise can be for video cleanup and another for audio cleanup.
245
+     */
246
+    _attemptPreviewTrackCleanup() {
247
+        return Promise.all([
248
+            this._disposeVideoPreview(),
249
+            this._disposeAudioPreview()
250
+        ]);
251
+    }
252
+
253
+    /**
254
+     * Utility function for disposing the current audio preview.
255
+     *
256
+     * @private
257
+     * @returns {Promise}
258
+     */
259
+    _disposeAudioPreview() {
260
+        return this.state.previewAudioTrack
261
+            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
262
+    }
263
+
264
+    /**
265
+     * Utility function for disposing the current video preview.
266
+     *
267
+     * @private
268
+     * @returns {Promise}
269
+     */
270
+    _disposeVideoPreview() {
271
+        return this.state.previewVideoTrack
272
+            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
273
+    }
274
+
275
+    /**
276
+     * Disposes preview tracks and signals to
277
+     * close DeviceSelectionDialogBase.
278
+     *
279
+     * @private
280
+     * @returns {boolean} Returns false to prevent closure until cleanup is
281
+     * complete.
282
+     */
283
+    _onCancel() {
284
+        if (this._isClosing) {
285
+            return false;
286
+        }
287
+
288
+        this._isClosing = true;
289
+
290
+        const cleanupPromises = this._attemptPreviewTrackCleanup();
291
+
292
+        Promise.all(cleanupPromises)
293
+            .then(this.props.closeModal)
294
+            .catch(this.props.closeModal);
295
+
296
+        return false;
297
+    }
298
+
299
+    /**
300
+     * Identifies changes to the preferred input/output devices and perform
301
+     * necessary cleanup and requests to use those devices. Closes the modal
302
+     * after cleanup and device change requests complete.
303
+     *
304
+     * @private
305
+     * @returns {boolean} Returns false to prevent closure until cleanup is
306
+     * complete.
307
+     */
308
+    _onSubmit() {
309
+        if (this._isClosing) {
310
+            return false;
311
+        }
312
+
313
+        this._isClosing = true;
314
+
315
+        this._attemptPreviewTrackCleanup()
316
+            .then(this._setDevicesAndClose, this._setDevicesAndClose);
317
+
318
+        return false;
319
+    }
320
+
321
+    /**
322
+     * Creates an AudioInputPreview for previewing if audio is being received.
323
+     * Null will be returned if local stats for tracking audio input levels
324
+     * cannot be obtained.
325
+     *
326
+     * @private
327
+     * @returns {ReactComponent|null}
328
+     */
329
+    _renderAudioInputPreview() {
330
+        if (this.props.hideAudioInputPreview) {
331
+            return null;
332
+        }
333
+
334
+        return (
335
+            <AudioInputPreview
336
+                track = { this.state.previewAudioTrack } />
337
+        );
338
+    }
339
+
340
+    /**
341
+     * Creates an AudioOutputPreview instance for playing a test sound with the
342
+     * passed in device id. Null will be returned if hideAudioOutput is truthy.
343
+     *
344
+     * @private
345
+     * @returns {ReactComponent|null}
346
+     */
347
+    _renderAudioOutputPreview() {
348
+        if (this.props.hideAudioOutputSelect) {
349
+            return null;
350
+        }
351
+
352
+        return (
353
+            <AudioOutputPreview
354
+                deviceId = { this.state.selectedAudioOutputId } />
355
+        );
356
+    }
357
+
358
+    /**
359
+     * Creates a DeviceSelector instance based on the passed in configuration.
360
+     *
361
+     * @private
362
+     * @param {Object} props - The props for the DeviceSelector.
363
+     * @returns {ReactElement}
364
+     */
365
+    _renderSelector(props) {
366
+        return (
367
+            <DeviceSelector { ...props } />
368
+        );
369
+    }
370
+
371
+    /**
372
+     * Creates DeviceSelector instances for video output, audio input, and audio
373
+     * output.
374
+     *
375
+     * @private
376
+     * @returns {Array<ReactElement>} DeviceSelector instances.
377
+     */
378
+    _renderSelectors() {
379
+        const { availableDevices } = this.props;
380
+
381
+        const configurations = [
382
+            {
383
+                devices: availableDevices.videoInput,
384
+                hasPermission: this.props.hasVideoPermission,
385
+                icon: 'icon-camera',
386
+                isDisabled: this.props.disableDeviceChange,
387
+                key: 'videoInput',
388
+                label: 'settings.selectCamera',
389
+                onSelect: this._updateVideoInput,
390
+                selectedDeviceId: this.state.selectedVideoInputId
391
+            },
392
+            {
393
+                devices: availableDevices.audioInput,
394
+                hasPermission: this.props.hasAudioPermission,
395
+                icon: 'icon-microphone',
396
+                isDisabled: this.props.disableAudioInputChange
397
+                    || this.props.disableDeviceChange,
398
+                key: 'audioInput',
399
+                label: 'settings.selectMic',
400
+                onSelect: this._updateAudioInput,
401
+                selectedDeviceId: this.state.selectedAudioInputId
402
+            }
403
+        ];
404
+
405
+        if (!this.props.hideAudioOutputSelect) {
406
+            configurations.push({
407
+                devices: availableDevices.audioOutput,
408
+                hasPermission: this.props.hasAudioPermission
409
+                    || this.props.hasVideoPermission,
410
+                icon: 'icon-volume',
411
+                isDisabled: this.props.disableDeviceChange,
412
+                key: 'audioOutput',
413
+                label: 'settings.selectAudioOutput',
414
+                onSelect: this._updateAudioOutput,
415
+                selectedDeviceId: this.state.selectedAudioOutputId
416
+            });
417
+        }
418
+
419
+        return configurations.map(this._renderSelector);
420
+    }
421
+
422
+    /**
423
+     * Sets the selected devices and closes the dialog.
424
+     *
425
+     * @returns {void}
426
+     */
427
+    _setDevicesAndClose() {
428
+        const {
429
+            setVideoInputDevice,
430
+            setAudioInputDevice,
431
+            setAudioOutputDevice,
432
+            closeModal
433
+        } = this.props;
434
+
435
+        const promises = [];
436
+
437
+        if (this.state.selectedVideoInputId
438
+                !== this.props.currentVideoInputId) {
439
+            promises.push(setVideoInputDevice(this.state.selectedVideoInputId));
440
+        }
441
+
442
+        if (this.state.selectedAudioInputId
443
+                !== this.props.currentAudioInputId) {
444
+            promises.push(setAudioInputDevice(this.state.selectedAudioInputId));
445
+        }
446
+
447
+        if (this.state.selectedAudioOutputId
448
+                !== this.props.currentAudioOutputId) {
449
+            promises.push(
450
+                setAudioOutputDevice(this.state.selectedAudioOutputId));
451
+        }
452
+        Promise.all(promises).then(closeModal, closeModal);
453
+    }
454
+
455
+    /**
456
+     * Callback invoked when a new audio input device has been selected. Updates
457
+     * the internal state of the user's selection as well as the audio track
458
+     * that should display in the preview.
459
+     *
460
+     * @param {string} deviceId - The id of the chosen audio input device.
461
+     * @private
462
+     * @returns {void}
463
+     */
464
+    _updateAudioInput(deviceId) {
465
+        this.setState({
466
+            selectedAudioInputId: deviceId
467
+        }, () => {
468
+            this._disposeAudioPreview()
469
+                .then(() => createLocalTrack('audio', deviceId))
470
+                .then(jitsiLocalTrack => {
471
+                    this.setState({
472
+                        previewAudioTrack: jitsiLocalTrack
473
+                    });
474
+                })
475
+                .catch(() => {
476
+                    this.setState({
477
+                        previewAudioTrack: null
478
+                    });
479
+                });
480
+        });
481
+    }
482
+
483
+    /**
484
+     * Callback invoked when a new audio output device has been selected.
485
+     * Updates the internal state of the user's selection.
486
+     *
487
+     * @param {string} deviceId - The id of the chosen audio output device.
488
+     * @private
489
+     * @returns {void}
490
+     */
491
+    _updateAudioOutput(deviceId) {
492
+        this.setState({
493
+            selectedAudioOutputId: deviceId
494
+        });
495
+    }
496
+
497
+    /**
498
+     * Callback invoked when a new video input device has been selected. Updates
499
+     * the internal state of the user's selection as well as the video track
500
+     * that should display in the preview.
501
+     *
502
+     * @param {string} deviceId - The id of the chosen video input device.
503
+     * @private
504
+     * @returns {void}
505
+     */
506
+    _updateVideoInput(deviceId) {
507
+        this.setState({
508
+            selectedVideoInputId: deviceId
509
+        }, () => {
510
+            this._disposeVideoPreview()
511
+                .then(() => createLocalTrack('video', deviceId))
512
+                .then(jitsiLocalTrack => {
513
+                    this.setState({
514
+                        previewVideoTrack: jitsiLocalTrack,
515
+                        previewVideoTrackError: null
516
+                    });
517
+                })
518
+                .catch(() => {
519
+                    this.setState({
520
+                        previewVideoTrack: null,
521
+                        previewVideoTrackError:
522
+                            this.props.t('deviceSelection.previewUnavailable')
523
+                    });
524
+                });
525
+        });
526
+    }
527
+}
528
+
529
+export default translate(DeviceSelectionDialogBase);

+ 1
- 1
react/features/device-selection/components/DeviceSelector.js Dosyayı Görüntüle

@@ -83,7 +83,7 @@ class DeviceSelector extends Component {
83 83
             return this._renderNoPermission();
84 84
         }
85 85
 
86
-        if (!this.props.devices.length) {
86
+        if (!this.props.devices || !this.props.devices.length) {
87 87
             return this._renderNoDevices();
88 88
         }
89 89
 

+ 2
- 0
react/features/device-selection/components/index.js Dosyayı Görüntüle

@@ -1 +1,3 @@
1 1
 export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
2
+export { default as DeviceSelectionDialogBase }
3
+    from './DeviceSelectionDialogBase';

+ 4
- 0
react/features/device-selection/index.js Dosyayı Görüntüle

@@ -1,2 +1,6 @@
1 1
 export * from './actions';
2
+export * from './actionTypes';
2 3
 export * from './components';
4
+
5
+import './middleware';
6
+import './reducer';

+ 24
- 0
react/features/device-selection/middleware.js Dosyayı Görüntüle

@@ -0,0 +1,24 @@
1
+import { UPDATE_DEVICE_LIST } from '../base/devices';
2
+import { MiddlewareRegistry } from '../base/redux';
3
+
4
+/**
5
+ * Implements the middleware of the feature device-selection.
6
+ *
7
+ * @param {Store} store - Redux store.
8
+ * @returns {Function}
9
+ */
10
+// eslint-disable-next-line no-unused-vars
11
+MiddlewareRegistry.register(store => next => action => {
12
+    const result = next(action);
13
+
14
+    if (action.type === UPDATE_DEVICE_LIST) {
15
+        const { popupDialogData }
16
+            = store.getState()['features/device-selection'];
17
+
18
+        if (popupDialogData) {
19
+            popupDialogData.transport.sendEvent({ name: 'deviceListChanged' });
20
+        }
21
+    }
22
+
23
+    return result;
24
+});

+ 13
- 0
react/features/device-selection/popup.js Dosyayı Görüntüle

@@ -0,0 +1,13 @@
1
+import 'aui-css';
2
+import 'aui-experimental-css';
3
+
4
+import DeviceSelectionPopup from './DeviceSelectionPopup';
5
+
6
+let deviceSelectionPopup;
7
+
8
+window.init = function(i18next) {
9
+    deviceSelectionPopup = new DeviceSelectionPopup(i18next);
10
+};
11
+
12
+window.addEventListener('beforeunload', () =>
13
+    deviceSelectionPopup.close());

+ 28
- 0
react/features/device-selection/reducer.js Dosyayı Görüntüle

@@ -0,0 +1,28 @@
1
+import { ReducerRegistry } from '../base/redux';
2
+
3
+import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
4
+
5
+/**
6
+ * Listen for actions which changes the state of the popup window for the device
7
+ * selection.
8
+ *
9
+ * @param {Object} state - The Redux state of the feature
10
+ * features/device-selection.
11
+ * @param {Object} action - Action object.
12
+ * @param {string} action.type - Type of action.
13
+ * @param {Object} action.popupDialogData - Object that stores the current
14
+ * Window object of the popup and the Transport instance. If no popup is shown
15
+ * the value will be undefined.
16
+ * @returns {Object}
17
+ */
18
+ReducerRegistry.register('features/device-selection',
19
+    (state = {}, action) => {
20
+        if (action.type === SET_DEVICE_SELECTION_POPUP_DATA) {
21
+            return {
22
+                ...state,
23
+                popupDialogData: action.popupDialogData
24
+            };
25
+        }
26
+
27
+        return state;
28
+    });

+ 19
- 3
react/features/toolbox/components/ToolbarButton.native.js Dosyayı Görüntüle

@@ -1,5 +1,6 @@
1 1
 import React from 'react';
2 2
 import { TouchableHighlight } from 'react-native';
3
+import { connect } from 'react-redux';
3 4
 
4 5
 import { Icon } from '../../base/font-icons';
5 6
 
@@ -10,13 +11,20 @@ import AbstractToolbarButton from './AbstractToolbarButton';
10 11
  *
11 12
  * @extends AbstractToolbarButton
12 13
  */
13
-export default class ToolbarButton extends AbstractToolbarButton {
14
+class ToolbarButton extends AbstractToolbarButton {
14 15
     /**
15 16
      * ToolbarButton component's property types.
16 17
      *
17 18
      * @static
18 19
      */
19
-    static propTypes = AbstractToolbarButton.propTypes
20
+    static propTypes = {
21
+        ...AbstractToolbarButton.propTypes,
22
+
23
+        /**
24
+         * Used to dispatch an action when the button is clicked.
25
+         */
26
+        dispatch: React.PropTypes.func
27
+    };
20 28
 
21 29
     /**
22 30
      * Renders the button of this Toolbar button.
@@ -29,7 +37,13 @@ export default class ToolbarButton extends AbstractToolbarButton {
29 37
     _renderButton(children) {
30 38
         const props = {};
31 39
 
32
-        'onClick' in this.props && (props.onPress = this.props.onClick);
40
+        'onClick' in this.props && (props.onPress = () => {
41
+            const action = this.props.onClick(event);
42
+
43
+            if (action) {
44
+                this.props.dispatch(action);
45
+            }
46
+        });
33 47
         'style' in this.props && (props.style = this.props.style);
34 48
         'underlayColor' in this.props
35 49
             && (props.underlayColor = this.props.underlayColor);
@@ -45,3 +59,5 @@ export default class ToolbarButton extends AbstractToolbarButton {
45 59
         return super._renderIcon(Icon);
46 60
     }
47 61
 }
62
+
63
+export default connect()(ToolbarButton);

+ 12
- 2
react/features/toolbox/components/ToolbarButton.web.js Dosyayı Görüntüle

@@ -1,6 +1,7 @@
1 1
 /* @flow */
2 2
 
3 3
 import React from 'react';
4
+import { connect } from 'react-redux';
4 5
 
5 6
 import { translate } from '../../base/i18n';
6 7
 
@@ -36,6 +37,11 @@ class ToolbarButton extends AbstractToolbarButton {
36 37
          */
37 38
         button: React.PropTypes.object.isRequired,
38 39
 
40
+        /**
41
+         * Used to dispatch an action when the button is clicked.
42
+         */
43
+        dispatch: React.PropTypes.func,
44
+
39 45
         /**
40 46
          * Handler for component mount.
41 47
          */
@@ -151,7 +157,11 @@ class ToolbarButton extends AbstractToolbarButton {
151 157
         } = button;
152 158
 
153 159
         if (enabled && !unclickable && onClick) {
154
-            onClick(event);
160
+            const action = onClick(event);
161
+
162
+            if (action) {
163
+                this.props.dispatch(action);
164
+            }
155 165
         }
156 166
     }
157 167
 
@@ -228,4 +238,4 @@ class ToolbarButton extends AbstractToolbarButton {
228 238
     }
229 239
 }
230 240
 
231
-export default translate(ToolbarButton);
241
+export default translate(connect()(ToolbarButton));

+ 43
- 9
react/features/toolbox/defaultToolbarButtons.js Dosyayı Görüntüle

@@ -2,25 +2,26 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
-import UIEvents from '../../../service/UI/UIEvents';
6
-
7
-import { openInviteDialog } from '../invite';
5
+import { openDeviceSelectionDialog } from '../device-selection';
8 6
 import { openDialOutDialog } from '../dial-out';
7
+import { openInviteDialog } from '../invite';
8
+import UIEvents from '../../../service/UI/UIEvents';
9 9
 
10 10
 declare var APP: Object;
11
+declare var interfaceConfig: Object;
11 12
 declare var JitsiMeetJS: Object;
12 13
 
13 14
 /**
14 15
  * All toolbar buttons' descriptors.
15 16
  */
16
-export default {
17
+const buttons: Object = {
17 18
     /**
18 19
      * The descriptor of the camera toolbar button.
19 20
      */
20 21
     camera: {
21 22
         classNames: [ 'button', 'icon-camera' ],
22 23
         enabled: true,
23
-        filmstripOnlyEnabled: true,
24
+        isDisplayed: () => true,
24 25
         id: 'toolbar_button_camera',
25 26
         onClick() {
26 27
             if (APP.conference.videoMuted) {
@@ -153,11 +154,32 @@ export default {
153 154
         id: 'toolbar_button_dial_out',
154 155
         onClick() {
155 156
             JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
156
-            APP.store.dispatch(openDialOutDialog());
157
+
158
+            return openDialOutDialog();
157 159
         },
158 160
         tooltipKey: 'dialOut.dialOut'
159 161
     },
160 162
 
163
+    /**
164
+     * The descriptor of the device selection toolbar button.
165
+     */
166
+    fodeviceselection: {
167
+        classNames: [ 'button', 'icon-settings' ],
168
+        enabled: true,
169
+        isDisplayed() {
170
+            return interfaceConfig.filmStripOnly;
171
+        },
172
+        id: 'toolbar_button_fodeviceselection',
173
+        onClick() {
174
+            JitsiMeetJS.analytics.sendEvent(
175
+                'toolbar.fodeviceselection.toggled');
176
+
177
+            return openDeviceSelectionDialog();
178
+        },
179
+        sideContainerId: 'settings_container',
180
+        tooltipKey: 'toolbar.Settings'
181
+    },
182
+
161 183
     /**
162 184
      * The descriptor of the dialpad toolbar button.
163 185
      */
@@ -217,7 +239,7 @@ export default {
217 239
     hangup: {
218 240
         classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
219 241
         enabled: true,
220
-        filmstripOnlyEnabled: true,
242
+        isDisplayed: () => true,
221 243
         id: 'toolbar_button_hangup',
222 244
         onClick() {
223 245
             JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
@@ -235,7 +257,8 @@ export default {
235 257
         id: 'toolbar_button_link',
236 258
         onClick() {
237 259
             JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
238
-            APP.store.dispatch(openInviteDialog());
260
+
261
+            return openInviteDialog();
239 262
         },
240 263
         tooltipKey: 'toolbar.invite'
241 264
     },
@@ -246,7 +269,7 @@ export default {
246 269
     microphone: {
247 270
         classNames: [ 'button', 'icon-microphone' ],
248 271
         enabled: true,
249
-        filmstripOnlyEnabled: true,
272
+        isDisplayed: () => true,
250 273
         id: 'toolbar_button_mute',
251 274
         onClick() {
252 275
             const sharedVideoManager = APP.UI.getSharedVideoManager();
@@ -386,3 +409,14 @@ export default {
386 409
         tooltipKey: 'toolbar.sharedvideo'
387 410
     }
388 411
 };
412
+
413
+
414
+Object.keys(buttons).forEach(name => {
415
+    const button = buttons[name];
416
+
417
+    if (!button.isDisplayed) {
418
+        button.isDisplayed = () => !interfaceConfig.filmStripOnly;
419
+    }
420
+});
421
+
422
+export default buttons;

+ 3
- 4
react/features/toolbox/functions.web.js Dosyayı Görüntüle

@@ -64,7 +64,6 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
64 64
 
65 65
     if (typeof interfaceConfig !== 'undefined'
66 66
             && interfaceConfig.TOOLBAR_BUTTONS) {
67
-        const { filmStripOnly } = interfaceConfig;
68 67
 
69 68
         toolbarButtons
70 69
             = interfaceConfig.TOOLBAR_BUTTONS.reduce(
@@ -84,9 +83,9 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
84 83
                             };
85 84
                         }
86 85
 
87
-                        // In filmstrip-only mode we only add a button if it's
88
-                        // filmstrip-only enabled.
89
-                        if (!filmStripOnly || button.filmstripOnlyEnabled) {
86
+                        // If isDisplayed method is not defined, display the
87
+                        // button only for non-filmstripOnly mode
88
+                        if (button.isDisplayed()) {
90 89
                             acc[place].set(buttonName, button);
91 90
                         }
92 91
                     }

+ 10
- 12
react/features/toolbox/reducer.js Dosyayı Görüntüle

@@ -15,6 +15,7 @@ import {
15 15
     SET_TOOLBOX_TIMEOUT_MS,
16 16
     SET_TOOLBOX_VISIBLE
17 17
 } from './actionTypes';
18
+import defaultToolbarButtons from './defaultToolbarButtons';
18 19
 
19 20
 declare var interfaceConfig: Object;
20 21
 
@@ -208,6 +209,15 @@ ReducerRegistry.register(
208 209
  * @returns {Object}
209 210
  */
210 211
 function _setButton(state, { button, buttonName }): Object {
212
+    const buttonDefinition = defaultToolbarButtons[buttonName];
213
+
214
+    // We don't need to update if the button shouldn't be displayed
215
+    if (!buttonDefinition || !buttonDefinition.isDisplayed()) {
216
+        return {
217
+            ...state
218
+        };
219
+    }
220
+
211 221
     const { primaryToolbarButtons, secondaryToolbarButtons } = state;
212 222
     let selectedButton = primaryToolbarButtons.get(buttonName);
213 223
     let place = 'primaryToolbarButtons';
@@ -222,18 +232,6 @@ function _setButton(state, { button, buttonName }): Object {
222 232
         ...button
223 233
     };
224 234
 
225
-    // In filmstrip-only mode we only show buttons if they're filmstrip-only
226
-    // enabled, so we don't need to update if this isn't the case.
227
-    // FIXME A reducer should be a pure function of the current state and the
228
-    // specified action so it should not use the global variable
229
-    // interfaceConfig. Anyway, we'll move interfaceConfig into the (redux)
230
-    // store so we'll surely revisit the source code bellow.
231
-    if (interfaceConfig.filmStripOnly && !selectedButton.filmstripOnlyEnabled) {
232
-        return {
233
-            ...state
234
-        };
235
-    }
236
-
237 235
     const updatedToolbar = state[place].set(buttonName, selectedButton);
238 236
 
239 237
     return {

+ 19
- 0
static/deviceSelectionPopup.html Dosyayı Görüntüle

@@ -0,0 +1,19 @@
1
+<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
2
+  <head>
3
+    <meta charset="utf-8">
4
+    <meta http-equiv="content-type" content="text/html;charset=utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <base href="../" />
7
+    <!--#include virtual="/title.html" -->
8
+    <script><!--#include virtual="/interface_config.js" --></script>
9
+    <script>
10
+        window.config = {};
11
+        window.JitsiMeetJS = window.opener.window.JitsiMeetJS;
12
+    </script>
13
+    <script src="libs/device_selection_popup_bundle.min.js"></script>
14
+    <link rel="stylesheet" href="css/all.css">
15
+  </head>
16
+  <body>
17
+    <div id="react"></div>
18
+  </body>
19
+</html>

+ 10
- 0
webpack.config.js Dosyayı Görüntüle

@@ -193,6 +193,16 @@ const configs = [
193 193
         output: Object.assign({}, config.output, {
194 194
             library: 'JitsiMeetExternalAPI'
195 195
         })
196
+    }),
197
+
198
+    // The Webpack configuration to bundle popup_bundle.js (js file for the
199
+    // device selection popup dialog).
200
+    Object.assign({}, config, {
201
+        entry: {
202
+            'device_selection_popup_bundle':
203
+                './react/features/device-selection/popup.js'
204
+        },
205
+        output: config.output
196 206
     })
197 207
 ];
198 208
 

Loading…
İptal
Kaydet