瀏覽代碼

Refactor settings modal (#3121)

* feat(settings): setting dialog

- Move device selection, profile edit, language select, moderator
  options, and server auth into one modal with tabs.
- Remove side panel profile and settings and logic used to update
  them.
- Pipe server auth status into redux to display in the settings
  dialog.
- Change filmstrip only device selection popup to use the new
  stateless settings dialog component.

* squash: do not show profile tab if not guest

* squash: profile button not clickable if no profile to show

* squash: nits

* ref: Settings dialog.
master
Hristo Terezov 6 年之前
父節點
當前提交
1f8fa3b6d4
共有 49 個檔案被更改,包括 1703 行新增1841 行删除
  1. 2
    3
      conference.js
  2. 1
    50
      css/_side_toolbar_container.scss
  3. 1
    0
      css/main.scss
  4. 43
    0
      css/modals/settings/_settings.scss
  5. 1
    1
      interface_config.js
  6. 6
    2
      lang/main.json
  7. 1
    61
      modules/UI/UI.js
  8. 0
    12
      modules/UI/side_pannels/SidePanels.js
  9. 4
    0
      modules/UI/side_pannels/chat/Chat.js
  10. 0
    194
      modules/UI/side_pannels/profile/Profile.js
  11. 0
    52
      modules/UI/side_pannels/settings/SettingsMenu.js
  12. 88
    0
      package-lock.json
  13. 1
    0
      package.json
  14. 12
    0
      react/features/base/conference/actionTypes.js
  15. 21
    0
      react/features/base/conference/actions.js
  16. 21
    0
      react/features/base/conference/reducer.js
  17. 67
    0
      react/features/base/dialog/components/AbstractDialogTab.js
  18. 0
    0
      react/features/base/dialog/components/DialogWithTabs.native.js
  19. 199
    0
      react/features/base/dialog/components/DialogWithTabs.web.js
  20. 5
    0
      react/features/base/dialog/components/index.js
  21. 35
    52
      react/features/device-selection/actions.js
  22. 354
    0
      react/features/device-selection/components/DeviceSelection.js
  23. 0
    165
      react/features/device-selection/components/DeviceSelectionDialog.js
  24. 0
    560
      react/features/device-selection/components/DeviceSelectionDialogBase.js
  25. 4
    3
      react/features/device-selection/components/index.js
  26. 35
    0
      react/features/device-selection/functions.js
  27. 1
    0
      react/features/device-selection/index.js
  28. 57
    21
      react/features/settings/DeviceSelectionPopup.js
  29. 66
    0
      react/features/settings/actions.js
  30. 0
    87
      react/features/settings/components/web/DeviceSelectionButton.js
  31. 0
    179
      react/features/settings/components/web/LanguageSelectDropdown.js
  32. 0
    199
      react/features/settings/components/web/ModeratorCheckboxes.js
  33. 239
    0
      react/features/settings/components/web/MoreTab.js
  34. 188
    0
      react/features/settings/components/web/ProfileTab.js
  35. 9
    14
      react/features/settings/components/web/SettingsButton.js
  36. 167
    0
      react/features/settings/components/web/SettingsDialog.js
  37. 0
    111
      react/features/settings/components/web/SettingsMenu.js
  38. 1
    1
      react/features/settings/components/web/index.js
  39. 5
    0
      react/features/settings/constants.js
  40. 57
    1
      react/features/settings/functions.js
  41. 1
    0
      react/features/settings/index.js
  42. 0
    0
      react/features/settings/popup.js
  43. 0
    20
      react/features/side-panel/actionTypes.js
  44. 1
    29
      react/features/side-panel/actions.js
  45. 1
    14
      react/features/side-panel/middleware.js
  46. 1
    0
      react/features/toolbox/components/web/OverflowMenuProfileItem.js
  47. 7
    3
      react/features/toolbox/components/web/Toolbox.js
  48. 0
    6
      service/UI/UIEvents.js
  49. 1
    1
      webpack.config.js

+ 2
- 3
conference.js 查看文件

@@ -33,6 +33,7 @@ import EventEmitter from 'events';
33 33
 import {
34 34
     AVATAR_ID_COMMAND,
35 35
     AVATAR_URL_COMMAND,
36
+    authStatusChanged,
36 37
     conferenceFailed,
37 38
     conferenceJoined,
38 39
     conferenceLeft,
@@ -1650,7 +1651,7 @@ export default {
1650 1651
         room.on(
1651 1652
             JitsiConferenceEvents.AUTH_STATUS_CHANGED,
1652 1653
             (authEnabled, authLogin) =>
1653
-                APP.UI.updateAuthInfo(authEnabled, authLogin));
1654
+                APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
1654 1655
 
1655 1656
         room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
1656 1657
             user => APP.UI.onUserFeaturesChanged(user));
@@ -1997,7 +1998,6 @@ export default {
1997 1998
                 id: from,
1998 1999
                 email: data.value
1999 2000
             }));
2000
-            APP.UI.setUserEmail(from, data.value);
2001 2001
         });
2002 2002
 
2003 2003
         room.addCommandListener(
@@ -2575,7 +2575,6 @@ export default {
2575 2575
             email: formattedEmail
2576 2576
         }));
2577 2577
 
2578
-        APP.UI.setUserEmail(localId, formattedEmail);
2579 2578
         sendData(commands.EMAIL, formattedEmail);
2580 2579
     },
2581 2580
 

+ 1
- 50
css/_side_toolbar_container.scss 查看文件

@@ -92,7 +92,7 @@
92 92
         /**
93 93
          * Titles and subtitles of inner containers.
94 94
          */
95
-        div.title, div.subTitle {
95
+        div.title {
96 96
             margin: 24px 0 11px;
97 97
         }
98 98
 
@@ -112,53 +112,4 @@
112 112
             margin-top: 0 !important;
113 113
         }
114 114
     }
115
-
116
-    .settings-menu {
117
-        display: flex;
118
-        flex-direction: column;
119
-        padding-left: 10%;
120
-        padding-right: 10%;
121
-
122
-        .moderator-checkbox {
123
-            display: inline-block;
124
-            margin: 0 5px 0;
125
-            width: auto;
126
-        }
127
-
128
-        .moderator-option {
129
-            margin-top: 15px;
130
-        }
131
-
132
-        .subTitle {
133
-            color: $defaultSideBarFontColor;
134
-            font-size: 11px;
135
-            font-weight: 500;
136
-        }
137
-    }
138
-}
139
-
140
-/**
141
- * Profile
142
- */
143
-.auth_container {
144
-    ul {
145
-        padding: 0;
146
-
147
-        li {
148
-            list-style-type: none;
149
-
150
-            a.authButton {
151
-                width: 160px;
152
-                margin: 10px 20px;
153
-                padding: 3px 29px;
154
-                box-sizing: border-box;
155
-                background-color: #06a5df;
156
-                border-radius: 4px;
157
-                cursor: pointer;
158
-                color: $defaultColor;
159
-                text-decoration: none;
160
-                text-align: center;
161
-            }
162
-        }
163
-    }
164 115
 }

+ 1
- 0
css/main.scss 查看文件

@@ -41,6 +41,7 @@
41 41
 @import 'modals/dialog';
42 42
 @import 'modals/feedback/feedback';
43 43
 @import 'modals/invite/info';
44
+@import 'modals/settings/settings';
44 45
 @import 'modals/speaker_stats/speaker_stats';
45 46
 @import 'modals/video-quality/video-quality';
46 47
 @import 'videolayout_default';

+ 43
- 0
css/modals/settings/_settings.scss 查看文件

@@ -0,0 +1,43 @@
1
+.settings-pane {
2
+    display: flex;
3
+    width: 100%;
4
+
5
+    &.profile-pane {
6
+        flex-direction: column;
7
+    }
8
+
9
+    .auth-name {
10
+        margin-bottom: 4px;
11
+    }
12
+
13
+    .device-selection {
14
+        margin-top: 20px;
15
+    }
16
+
17
+    .mock-atlaskit-label {
18
+        color: #56637A;
19
+        font-size: 12px;
20
+        font-weight: 600;
21
+        line-height: 1.33;
22
+        padding: 20px 0px 4px 0px;
23
+    }
24
+
25
+    .more-tab,
26
+    .profile-edit {
27
+        display: flex;
28
+        width: 100%;
29
+    }
30
+
31
+    .profile-edit-field,
32
+    .settings-sub-pane {
33
+        flex: 1;
34
+    }
35
+
36
+    .profile-edit-field {
37
+        margin-right: 20px;
38
+    }
39
+
40
+    .language-settings {
41
+        max-width: 50%;
42
+    }
43
+}

+ 1
- 1
interface_config.js 查看文件

@@ -51,7 +51,7 @@ var interfaceConfig = {
51 51
         'invite', 'feedback', 'stats', 'shortcuts'
52 52
     ],
53 53
 
54
-    SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
54
+    SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
55 55
 
56 56
     // Determines how the video would fit the screen. 'both' would fit the whole
57 57
     // screen, 'height' would fit the original video height to the height of the

+ 6
- 2
lang/main.json 查看文件

@@ -163,11 +163,15 @@
163 163
         "selectMic": "Microphone",
164 164
         "selectAudioOutput": "Audio output",
165 165
         "followMe": "Everyone follows me",
166
+        "language": "Language",
167
+        "loggedIn": "Logged in as __name__",
166 168
         "noDevice": "None",
167 169
         "cameraAndMic": "Camera and microphone",
168
-        "moderator": "MODERATOR",
170
+        "moderator": "Moderator",
171
+        "more": "More",
169 172
         "password": "SET PASSWORD",
170
-        "audioVideo": "AUDIO AND VIDEO"
173
+        "audioVideo": "AUDIO AND VIDEO",
174
+        "devices": "Devices"
171 175
     },
172 176
     "profile": {
173 177
         "title": "Profile",

+ 1
- 61
modules/UI/UI.js 查看文件

@@ -15,11 +15,7 @@ import SharedVideoManager from './shared_video/SharedVideo';
15 15
 
16 16
 import VideoLayout from './videolayout/VideoLayout';
17 17
 import Filmstrip from './videolayout/Filmstrip';
18
-import Profile from './side_pannels/profile/Profile';
19 18
 
20
-import {
21
-    openDeviceSelectionDialog
22
-} from '../../react/features/device-selection';
23 19
 import { updateDeviceList } from '../../react/features/base/devices';
24 20
 import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
25 21
 import {
@@ -33,7 +29,6 @@ import {
33 29
     setNotificationsEnabled,
34 30
     showWarningNotification
35 31
 } from '../../react/features/notifications';
36
-import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
37 32
 import {
38 33
     dockToolbox,
39 34
     setToolboxEnabled,
@@ -97,22 +92,6 @@ const UIListeners = new Map([
97 92
     ], [
98 93
         UIEvents.TOGGLE_CHAT,
99 94
         () => UI.toggleChat()
100
-    ], [
101
-        UIEvents.TOGGLE_SETTINGS,
102
-        () => {
103
-            // Opening of device selection is special-cased as it is a dialog
104
-            // opened through a button in settings and not directly displayed in
105
-            // settings itself. As it is not useful to only have a settings menu
106
-            // with a button to open a dialog, open the dialog directly instead.
107
-            if (shouldShowOnlyDeviceSelection()) {
108
-                APP.store.dispatch(openDeviceSelectionDialog());
109
-            } else {
110
-                UI.toggleSidePanel('settings_container');
111
-            }
112
-        }
113
-    ], [
114
-        UIEvents.TOGGLE_PROFILE,
115
-        () => UI.toggleSidePanel('profile_container')
116 95
     ], [
117 96
         UIEvents.TOGGLE_FILMSTRIP,
118 97
         () => UI.handleToggleFilmstrip()
@@ -216,7 +195,6 @@ UI.changeDisplayName = function(id, displayName) {
216 195
     VideoLayout.onDisplayNameChanged(id, displayName);
217 196
 
218 197
     if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
219
-        Profile.changeDisplayName(displayName);
220 198
         Chat.setChatConversationMode(Boolean(displayName));
221 199
     }
222 200
 };
@@ -268,7 +246,7 @@ UI.setLocalRaisedHandStatus
268 246
  */
269 247
 UI.initConference = function() {
270 248
     const { getState } = APP.store;
271
-    const { email, id, name } = getLocalParticipant(getState);
249
+    const { id, name } = getLocalParticipant(getState);
272 250
 
273 251
     // Update default button states before showing the toolbar
274 252
     // if local role changes buttons state will be again updated.
@@ -282,11 +260,6 @@ UI.initConference = function() {
282 260
         UI.changeDisplayName('localVideoContainer', displayName);
283 261
     }
284 262
 
285
-    // Make sure we configure our avatar id, before creating avatar for us
286
-    if (email) {
287
-        UI.setUserEmail(id, email);
288
-    }
289
-
290 263
     // FollowMe attempts to copy certain aspects of the moderator's UI into the
291 264
     // other participants' UI. Consequently, it needs (1) read and write access
292 265
     // to the UI (depending on the moderator role of the local participant) and
@@ -492,9 +465,6 @@ UI.addUser = function(user) {
492 465
         APP.store.dispatch(showParticipantJoinedNotification(displayName));
493 466
     }
494 467
 
495
-    // Configure avatar
496
-    UI.setUserEmail(id);
497
-
498 468
     // set initial display name
499 469
     if (displayName) {
500 470
         UI.changeDisplayName(id, displayName);
@@ -739,17 +709,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
739 709
 // Used by torture.
740 710
 UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
741 711
 
742
-/**
743
- * Update user email.
744
- * @param {string} id user id
745
- * @param {string} email user email
746
- */
747
-UI.setUserEmail = function(id, email) {
748
-    if (APP.conference.isLocalId(id)) {
749
-        Profile.changeEmail(email);
750
-    }
751
-};
752
-
753 712
 /**
754 713
  * Updates the displayed avatar for participant.
755 714
  *
@@ -880,25 +839,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
880 839
     );
881 840
 };
882 841
 
883
-/**
884
- * Updates auth info on the UI.
885
- * @param {boolean} isAuthEnabled if authentication is enabled
886
- * @param {string} [login] current login
887
- */
888
-UI.updateAuthInfo = function(isAuthEnabled, login) {
889
-    const showAuth = isAuthEnabled && UIUtil.isAuthenticationEnabled();
890
-    const loggedIn = Boolean(login);
891
-
892
-    Profile.showAuthenticationButtons(showAuth);
893
-
894
-    if (showAuth) {
895
-        Profile.setAuthenticatedIdentity(login);
896
-
897
-        Profile.showLoginButton(!loggedIn);
898
-        Profile.showLogoutButton(loggedIn);
899
-    }
900
-};
901
-
902 842
 /**
903 843
  * Notifies interested listeners that the raise hand property has changed.
904 844
  *

+ 0
- 12
modules/UI/side_pannels/SidePanels.js 查看文件

@@ -1,6 +1,4 @@
1 1
 import Chat from './chat/Chat';
2
-import SettingsMenu from './settings/SettingsMenu';
3
-import Profile from './profile/Profile';
4 2
 import { isButtonEnabled } from '../../../react/features/toolbox';
5 3
 
6 4
 const SidePanels = {
@@ -9,16 +7,6 @@ const SidePanels = {
9 7
         if (isButtonEnabled('chat')) {
10 8
             Chat.init(eventEmitter);
11 9
         }
12
-
13
-        // Initialize settings
14
-        if (isButtonEnabled('settings')) {
15
-            SettingsMenu.init(eventEmitter);
16
-        }
17
-
18
-        // Initialize profile
19
-        if (isButtonEnabled('profile')) {
20
-            Profile.init(eventEmitter);
21
-        }
22 10
     }
23 11
 };
24 12
 

+ 4
- 0
modules/UI/side_pannels/chat/Chat.js 查看文件

@@ -43,6 +43,10 @@ const htmlStr = `
43 43
 function initHTML() {
44 44
     $(`#${sidePanelsContainerId}`)
45 45
         .append(htmlStr);
46
+
47
+    // make sure we translate the panel, as adding it can be after i18n
48
+    // library had initialized and translated already present html
49
+    APP.translation.translateElement($(`#${sidePanelsContainerId}`));
46 50
 }
47 51
 
48 52
 /**

+ 0
- 194
modules/UI/side_pannels/profile/Profile.js 查看文件

@@ -1,194 +0,0 @@
1
-/* global $, APP */
2
-import UIUtil from '../../util/UIUtil';
3
-import UIEvents from '../../../../service/UI/UIEvents';
4
-
5
-import {
6
-    createProfilePanelButtonEvent,
7
-    sendAnalytics
8
-} from '../../../../react/features/analytics';
9
-
10
-const sidePanelsContainerId = 'sideToolbarContainer';
11
-const htmlStr = `
12
-    <div id='profile_container' class='sideToolbarContainer__inner'>
13
-        <div class='title' data-i18n='profile.title'></div>
14
-        <div class='sideToolbarBlock first'>
15
-            <label class='first' data-i18n='profile.setDisplayNameLabel'>
16
-            </label>
17
-            <input class='input-control' type='text' id='setDisplayName'
18
-                data-i18n='[placeholder]settings.name'>
19
-        </div>
20
-        <div class='sideToolbarBlock'>
21
-            <label data-i18n='profile.setEmailLabel'></label>
22
-            <input id='setEmail' type='text' class='input-control'
23
-                data-i18n='[placeholder]profile.setEmailInput'>
24
-        </div>
25
-        <div id='profile_auth_container'
26
-             class='sideToolbarBlock auth_container'>
27
-            <p data-i18n='toolbar.authenticate'></p>
28
-            <ul>
29
-                <li id='profile_auth_identity'></li>
30
-                <li id='profile_button_login'>
31
-                    <a class='authButton' data-i18n='toolbar.login'></a>
32
-                </li>
33
-                <li id='profile_button_logout'>
34
-                    <a class='authButton' data-i18n='toolbar.logout'></a>
35
-                </li>
36
-            </ul>
37
-        </div>
38
-    </div>`;
39
-
40
-/**
41
- *
42
- */
43
-function initHTML() {
44
-    $(`#${sidePanelsContainerId}`)
45
-        .append(htmlStr);
46
-
47
-    // make sure we translate the panel, as adding it can be after i18n
48
-    // library had initialized and translated already present html
49
-    APP.translation.translateElement($(`#${sidePanelsContainerId}`));
50
-}
51
-
52
-export default {
53
-    init(emitter) {
54
-        initHTML();
55
-
56
-        const settings = APP.store.getState()['features/base/settings'];
57
-
58
-        /**
59
-         * Updates display name.
60
-         *
61
-         * @returns {void}
62
-         */
63
-        function updateDisplayName() {
64
-            emitter.emit(UIEvents.NICKNAME_CHANGED, $('#setDisplayName').val());
65
-        }
66
-
67
-        $('#setDisplayName')
68
-            .val(settings.displayName)
69
-            .keyup(event => {
70
-                if (event.keyCode === 13) { // enter
71
-                    updateDisplayName();
72
-                }
73
-            })
74
-            .focusout(updateDisplayName);
75
-
76
-        /**
77
-         * Updates the email.
78
-         *
79
-         * @returns {void}
80
-         */
81
-        function updateEmail() {
82
-            emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val());
83
-        }
84
-
85
-        $('#setEmail')
86
-            .val(settings.email)
87
-            .keyup(event => {
88
-                if (event.keyCode === 13) { // enter
89
-                    updateEmail();
90
-                }
91
-            })
92
-            .focusout(updateEmail);
93
-
94
-        /**
95
-         *
96
-         */
97
-        function loginClicked() {
98
-            sendAnalytics(createProfilePanelButtonEvent('login.button'));
99
-            emitter.emit(UIEvents.AUTH_CLICKED);
100
-        }
101
-
102
-        $('#profile_button_login').click(loginClicked);
103
-
104
-        /**
105
-         *
106
-         */
107
-        function logoutClicked() {
108
-            const titleKey = 'dialog.logoutTitle';
109
-            const msgKey = 'dialog.logoutQuestion';
110
-
111
-            sendAnalytics(createProfilePanelButtonEvent('logout.button'));
112
-
113
-            // Ask for confirmation
114
-            APP.UI.messageHandler.openTwoButtonDialog({
115
-                titleKey,
116
-                msgKey,
117
-                leftButtonKey: 'dialog.Yes',
118
-                submitFunction(evt, yes) {
119
-                    if (yes) {
120
-                        emitter.emit(UIEvents.LOGOUT);
121
-                    }
122
-                }
123
-            });
124
-        }
125
-
126
-        $('#profile_button_logout').click(logoutClicked);
127
-    },
128
-
129
-    /**
130
-     * Check if settings menu is visible or not.
131
-     * @returns {boolean}
132
-     */
133
-    isVisible() {
134
-        return UIUtil.isVisible(document.getElementById('profile_container'));
135
-    },
136
-
137
-    /**
138
-     * Change user display name in the settings menu.
139
-     * @param {string} newDisplayName
140
-     */
141
-    changeDisplayName(newDisplayName) {
142
-        $('#setDisplayName').val(newDisplayName);
143
-    },
144
-
145
-    /**
146
-     * Change the value of the field for the user email.
147
-     * @param {string} email the new value that will be displayed in the field.
148
-     */
149
-    changeEmail(email) {
150
-        $('#setEmail').val(email);
151
-    },
152
-
153
-    /**
154
-     * Shows or hides authentication related buttons
155
-     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
156
-     */
157
-    showAuthenticationButtons(show) {
158
-        const id = 'profile_auth_container';
159
-
160
-        UIUtil.setVisible(id, show);
161
-    },
162
-
163
-    /**
164
-     * Shows/hides login button.
165
-     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
166
-     */
167
-    showLoginButton(show) {
168
-        const id = 'profile_button_login';
169
-
170
-        UIUtil.setVisible(id, show);
171
-    },
172
-
173
-    /**
174
-     * Shows/hides logout button.
175
-     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
176
-     */
177
-    showLogoutButton(show) {
178
-        const id = 'profile_button_logout';
179
-
180
-        UIUtil.setVisible(id, show);
181
-    },
182
-
183
-    /**
184
-     * Displays user's authenticated identity name (login).
185
-     * @param {string} authIdentity identity name to be displayed.
186
-     */
187
-    setAuthenticatedIdentity(authIdentity) {
188
-        const id = 'profile_auth_identity';
189
-
190
-        UIUtil.setVisible(id, Boolean(authIdentity));
191
-
192
-        $(`#${id}`).text(authIdentity ? authIdentity : '');
193
-    }
194
-};

+ 0
- 52
modules/UI/side_pannels/settings/SettingsMenu.js 查看文件

@@ -1,52 +0,0 @@
1
-/* global $, APP, interfaceConfig */
2
-
3
-/* eslint-disable no-unused-vars */
4
-
5
-import React from 'react';
6
-import ReactDOM from 'react-dom';
7
-import { I18nextProvider } from 'react-i18next';
8
-import { Provider } from 'react-redux';
9
-
10
-import { i18next } from '../../../../react/features/base/i18n';
11
-import {
12
-    SettingsMenu,
13
-    isSettingEnabled
14
-} from '../../../../react/features/settings';
15
-import UIUtil from '../../util/UIUtil';
16
-
17
-/* eslint-enable no-unused-vars */
18
-
19
-export default {
20
-    init() {
21
-        const settingsMenuContainer = document.createElement('div');
22
-
23
-        settingsMenuContainer.id = 'settings_container';
24
-        settingsMenuContainer.className = 'sideToolbarContainer__inner';
25
-
26
-        $('#sideToolbarContainer').append(settingsMenuContainer);
27
-
28
-        const props = {
29
-            showDeviceSettings: isSettingEnabled('devices'),
30
-            showLanguageSettings: isSettingEnabled('language'),
31
-            showModeratorSettings: isSettingEnabled('moderator'),
32
-            showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
33
-        };
34
-
35
-        ReactDOM.render(
36
-            <Provider store = { APP.store }>
37
-                <I18nextProvider i18n = { i18next }>
38
-                    <SettingsMenu { ...props } />
39
-                </I18nextProvider>
40
-            </Provider>,
41
-            settingsMenuContainer
42
-        );
43
-    },
44
-
45
-    /**
46
-     * Check if settings menu is visible or not.
47
-     * @returns {boolean}
48
-     */
49
-    isVisible() {
50
-        return UIUtil.isVisible(document.getElementById('settings_container'));
51
-    }
52
-};

+ 88
- 0
package-lock.json 查看文件

@@ -37,6 +37,94 @@
37 37
         "styled-components": "^1.3.0"
38 38
       }
39 39
     },
40
+    "@atlaskit/checkbox": {
41
+      "version": "2.0.2",
42
+      "resolved": "https://registry.npmjs.org/@atlaskit/checkbox/-/checkbox-2.0.2.tgz",
43
+      "integrity": "sha512-sX5vixywY61A7Q/YR04g3Z+PhaZNEQ1orc/t2JX80iF+FLpEfvD1KY0ywraSHx5ljXjxJ9u6Df4az/y9BDmQ5A==",
44
+      "requires": {
45
+        "@atlaskit/button": "7.2.5",
46
+        "@atlaskit/icon": "11.3.0",
47
+        "@atlaskit/theme": "3.2.2",
48
+        "babel-runtime": "6.26.0",
49
+        "prop-types": "15.6.0",
50
+        "styled-components": "1.4.6"
51
+      },
52
+      "dependencies": {
53
+        "@atlaskit/analytics-next": {
54
+          "version": "2.1.2",
55
+          "resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.1.2.tgz",
56
+          "integrity": "sha512-rAd+kLEEXZ8mu39qOf+E0kZu+k1RWNwd5vyfb0WXuBxq+jyCzAg19vhj99Uq2TblW/WgOX9ajBE+lEgbYqmvNw==",
57
+          "requires": {
58
+            "prop-types": "15.6.0"
59
+          }
60
+        },
61
+        "@atlaskit/button": {
62
+          "version": "7.2.5",
63
+          "resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.2.5.tgz",
64
+          "integrity": "sha512-YDH2wWxoMe9uGmyMy+zPQMbxkK0TrFplutu6bZ0n8Ojet8XcGOBPyWS5lf3Nt+DOPKLUXCI4pknR6fB6ZF1e/g==",
65
+          "requires": {
66
+            "@atlaskit/analytics-next": "2.1.2",
67
+            "@atlaskit/spinner": "5.0.2",
68
+            "@atlaskit/theme": "3.2.2",
69
+            "babel-runtime": "6.26.0",
70
+            "styled-components": "1.4.6"
71
+          }
72
+        },
73
+        "@atlaskit/icon": {
74
+          "version": "11.3.0",
75
+          "resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.3.0.tgz",
76
+          "integrity": "sha512-dFnpk3yT9EZUmCC8bUOP4WmENWMqLYezBOpv+mp/vKBbzT786c+ZVyDW5wZ9hSKmfb+aHjiZt+UuwUiVW5D+Wg==",
77
+          "requires": {
78
+            "@atlaskit/theme": "3.2.2",
79
+            "babel-runtime": "6.26.0",
80
+            "styled-components": "1.4.6",
81
+            "uuid": "3.1.0"
82
+          }
83
+        },
84
+        "@atlaskit/spinner": {
85
+          "version": "5.0.2",
86
+          "resolved": "https://registry.npmjs.org/@atlaskit/spinner/-/spinner-5.0.2.tgz",
87
+          "integrity": "sha512-n0j/urjG3FF9q/6Nae981GwsdvT44zAobPqFGnaeKDfqUzrFHcs1PmL0dqa36aFJzOPZHzl6ZfBl9Q3Vpl9PKQ==",
88
+          "requires": {
89
+            "@atlaskit/theme": "3.2.2",
90
+            "babel-runtime": "6.26.0",
91
+            "react-transition-group": "2.3.1",
92
+            "styled-components": "1.4.6"
93
+          }
94
+        },
95
+        "@atlaskit/theme": {
96
+          "version": "3.2.2",
97
+          "resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.2.2.tgz",
98
+          "integrity": "sha512-SQYgGe8WnO1aF991XraCzbMzOf8v1rMBkLYkwVb6BAjxTVgeSepMTVYOTQ7+KXzSAKFP0fDxgXnUB/VsSUR8Ig==",
99
+          "requires": {
100
+            "prop-types": "15.6.0",
101
+            "styled-components": "1.4.6"
102
+          }
103
+        },
104
+        "react-transition-group": {
105
+          "version": "2.3.1",
106
+          "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.1.tgz",
107
+          "integrity": "sha512-hu4/LAOFSKjWt1+1hgnOv3ldxmt6lvZGTWz4KUkFrqzXrNDIVSu6txIcPszw7PNduR8en9YTN55JLRyd/L1ZiQ==",
108
+          "requires": {
109
+            "dom-helpers": "3.3.1",
110
+            "loose-envify": "1.3.1",
111
+            "prop-types": "15.6.1"
112
+          },
113
+          "dependencies": {
114
+            "prop-types": {
115
+              "version": "15.6.1",
116
+              "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
117
+              "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
118
+              "requires": {
119
+                "fbjs": "0.8.16",
120
+                "loose-envify": "1.3.1",
121
+                "object-assign": "4.1.1"
122
+              }
123
+            }
124
+          }
125
+        }
126
+      }
127
+    },
40 128
     "@atlaskit/dropdown-menu": {
41 129
       "version": "3.10.2",
42 130
       "resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz",

+ 1
- 0
package.json 查看文件

@@ -17,6 +17,7 @@
17 17
   "dependencies": {
18 18
     "@atlaskit/avatar": "8.0.5",
19 19
     "@atlaskit/button": "5.4.2",
20
+    "@atlaskit/checkbox": "2.0.2",
20 21
     "@atlaskit/dropdown-menu": "3.10.2",
21 22
     "@atlaskit/droplist": "4.11.1",
22 23
     "@atlaskit/field-text": "4.0.1",

+ 12
- 0
react/features/base/conference/actionTypes.js 查看文件

@@ -1,3 +1,15 @@
1
+/**
2
+ * The type of (redux) action which signals that server authentication has
3
+ * becoming available or unavailable or logged in user has changed.
4
+ *
5
+ * {
6
+ *     type: AUTH_STATUS_CHANGED,
7
+ *     authEnabled: boolean,
8
+ *     authLogin: string
9
+ * }
10
+ */
11
+export const AUTH_STATUS_CHANGED = Symbol('AUTH_STATUS_CHANGED');
12
+
1 13
 /**
2 14
  * The type of (redux) action which signals that a specific conference failed.
3 15
  *

+ 21
- 0
react/features/base/conference/actions.js 查看文件

@@ -22,6 +22,7 @@ import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
22 22
 import { getJitsiMeetGlobalNS } from '../util';
23 23
 
24 24
 import {
25
+    AUTH_STATUS_CHANGED,
25 26
     CONFERENCE_FAILED,
26 27
     CONFERENCE_JOINED,
27 28
     CONFERENCE_LEFT,
@@ -178,6 +179,26 @@ function _addConferenceListeners(conference, dispatch) {
178 179
         })));
179 180
 }
180 181
 
182
+/**
183
+ * Updates the current known state of server-side authentication.
184
+ *
185
+ * @param {boolean} authEnabled - Whether or not server authentication is
186
+ * enabled.
187
+ * @param {string} authLogin - The current name of the logged in user, if any.
188
+ * @returns {{
189
+ *     type: AUTH_STATUS_CHANGED,
190
+ *     authEnabled: boolean,
191
+ *     authLogin: string
192
+ * }}
193
+ */
194
+export function authStatusChanged(authEnabled: boolean, authLogin: string) {
195
+    return {
196
+        type: AUTH_STATUS_CHANGED,
197
+        authEnabled,
198
+        authLogin
199
+    };
200
+}
201
+
181 202
 /**
182 203
  * Signals that a specific conference has failed.
183 204
  *

+ 21
- 0
react/features/base/conference/reducer.js 查看文件

@@ -6,6 +6,7 @@ import { assign, ReducerRegistry, set } from '../redux';
6 6
 import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
7 7
 
8 8
 import {
9
+    AUTH_STATUS_CHANGED,
9 10
     CONFERENCE_FAILED,
10 11
     CONFERENCE_JOINED,
11 12
     CONFERENCE_LEFT,
@@ -31,6 +32,9 @@ import { isRoomValid } from './functions';
31 32
  */
32 33
 ReducerRegistry.register('features/base/conference', (state = {}, action) => {
33 34
     switch (action.type) {
35
+    case AUTH_STATUS_CHANGED:
36
+        return _authStatusChanged(state, action);
37
+
34 38
     case CONFERENCE_FAILED:
35 39
         return _conferenceFailed(state, action);
36 40
 
@@ -85,6 +89,23 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
85 89
     return state;
86 90
 });
87 91
 
92
+/**
93
+ * Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
94
+ * base/conference.
95
+ *
96
+ * @param {Object} state - The Redux state of the feature base/conference.
97
+ * @param {Action} action - The Redux action AUTH_STATUS_CHANGED to reduce.
98
+ * @private
99
+ * @returns {Object} The new state of the feature base/conference after the
100
+ * reduction of the specified action.
101
+ */
102
+function _authStatusChanged(state, { authEnabled, authLogin }) {
103
+    return assign(state, {
104
+        authEnabled,
105
+        authLogin
106
+    });
107
+}
108
+
88 109
 /**
89 110
  * Reduces a specific Redux action CONFERENCE_FAILED of the feature
90 111
  * base/conference.

+ 67
- 0
react/features/base/dialog/components/AbstractDialogTab.js 查看文件

@@ -0,0 +1,67 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+/**
6
+ * The type of the React {@code Component} props of {@link AbstractDialogTab}.
7
+ */
8
+export type Props = {
9
+
10
+    /**
11
+     * Function that closes the dialog.
12
+     */
13
+    closeDialog: Function,
14
+
15
+    /**
16
+     * Callback to invoke on change.
17
+     */
18
+    onTabStateChange: Function,
19
+
20
+    /**
21
+     * The id of the tab.
22
+     */
23
+    tabId: number
24
+};
25
+
26
+
27
+/**
28
+ * Abstract React {@code Component} for tabs of the DialogWithTabs component.
29
+ *
30
+ * @extends Component
31
+ */
32
+class AbstractDialogTab extends Component<Props> {
33
+    /**
34
+     * Initializes a new {@code AbstractDialogTab} instance.
35
+     *
36
+     * @param {Object} props - The read-only properties with which the new
37
+     * instance is to be initialized.
38
+     */
39
+    constructor(props: Props) {
40
+        super(props);
41
+
42
+        // Bind event handler so it is only bound once for every instance.
43
+        this._onChange = this._onChange.bind(this);
44
+    }
45
+
46
+    _onChange: (Object) => {};
47
+
48
+    /**
49
+     * Uses the onTabStateChange function to pass the changed state of the
50
+     * controlled tab component to the controlling DialogWithTabs component.
51
+     *
52
+     * @param {Object} change - Object that contains the changed property and
53
+     * value.
54
+     * @returns {void}
55
+     */
56
+    _onChange(change) {
57
+        const { onTabStateChange, tabId } = this.props;
58
+
59
+        onTabStateChange(tabId, {
60
+            ...this.props,
61
+            ...change
62
+        });
63
+    }
64
+
65
+}
66
+
67
+export default AbstractDialogTab;

+ 0
- 0
react/features/base/dialog/components/DialogWithTabs.native.js 查看文件


+ 199
- 0
react/features/base/dialog/components/DialogWithTabs.web.js 查看文件

@@ -0,0 +1,199 @@
1
+// @flow
2
+
3
+import Tabs from '@atlaskit/tabs';
4
+import React, { Component } from 'react';
5
+
6
+import { StatelessDialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+
9
+const logger = require('jitsi-meet-logger').getLogger(__filename);
10
+
11
+/**
12
+ * The type of the React {@code Component} props of {@link DialogWithTabs}.
13
+ */
14
+export type Props = {
15
+
16
+    /**
17
+     * Function that closes the dialog.
18
+     */
19
+    closeDialog: Function,
20
+
21
+    /**
22
+     * Which settings tab should be initially displayed. If not defined then
23
+     * the first tab will be displayed.
24
+     */
25
+    defaultTab: number,
26
+
27
+    /**
28
+     * Disables dismissing the dialog when the blanket is clicked. Enabled
29
+     * by default.
30
+     */
31
+    disableBlanketClickDismiss: boolean,
32
+
33
+    /**
34
+     * Callback invoked when the Save button has been pressed.
35
+     */
36
+    onSubmit: Function,
37
+
38
+
39
+    /**
40
+     * Invoked to obtain translated strings.
41
+     */
42
+    t: Function,
43
+
44
+    /**
45
+     * Information about the tabs that will be rendered.
46
+     */
47
+    tabs: Array<Object>
48
+
49
+};
50
+
51
+/**
52
+ * The type of the React {@code Component} state of {@link DialogWithTabs}.
53
+ */
54
+type State = {
55
+
56
+    /**
57
+     * An array of the states of the tabs.
58
+     */
59
+    tabStates: Array<Object>
60
+};
61
+
62
+/**
63
+ * A React {@code Component} for displaying a dialog with tabs.
64
+ *
65
+ * @extends Component
66
+ */
67
+class DialogWithTabs extends Component<Props, State> {
68
+    /**
69
+     * Initializes a new {@code DialogWithTabs} instance.
70
+     *
71
+     * @param {Object} props - The read-only React {@code Component} props with
72
+     * which the new instance is to be initialized.
73
+     */
74
+    constructor(props: Props) {
75
+        super(props);
76
+        this.state = {
77
+            tabStates: this.props.tabs.map(tab => tab.props)
78
+        };
79
+        this._onSubmit = this._onSubmit.bind(this);
80
+        this._onTabStateChange = this._onTabStateChange.bind(this);
81
+    }
82
+
83
+    /**
84
+     * Implements React's {@link Component#render()}.
85
+     *
86
+     * @inheritdoc
87
+     * @returns {ReactElement}
88
+     */
89
+    render() {
90
+        const onCancel = this.props.closeDialog;
91
+
92
+        return (
93
+            <StatelessDialog
94
+                disableBlanketClickDismiss
95
+                    = { this.props.disableBlanketClickDismiss }
96
+                onCancel = { onCancel }
97
+                onSubmit = { this._onSubmit }
98
+                titleKey = 'settings.title'>
99
+                <div className = 'settings-dialog'>
100
+                    { this._renderTabs() }
101
+                </div>
102
+            </StatelessDialog>
103
+        );
104
+    }
105
+
106
+    /**
107
+     * Renders the tabs from the tab information passed on props.
108
+     *
109
+     * @returns {void}
110
+     */
111
+    _renderTabs() {
112
+        const { defaultTab = 0, t, tabs } = this.props;
113
+
114
+        if (tabs.length === 1) {
115
+            return this._renderTab({
116
+                ...tabs[0],
117
+                tabId: 0
118
+            });
119
+        }
120
+
121
+        if (tabs.length > 1) {
122
+            return (
123
+                <Tabs
124
+                    tabs = {
125
+                        tabs.map(({ component, label, styles }, idx) => {
126
+                            return {
127
+                                content: this._renderTab({
128
+                                    component,
129
+                                    styles,
130
+                                    tabId: idx
131
+                                }),
132
+                                defaultSelected: defaultTab === idx,
133
+                                label: t(label)
134
+                            };
135
+                        })
136
+                    } />);
137
+        }
138
+
139
+        logger.warn('No settings tabs configured to display.');
140
+
141
+        return null;
142
+    }
143
+
144
+    /**
145
+     * Renders a tab from the tab information passed as parameters.
146
+     *
147
+     * @param {Object} tabInfo - Information about the tab.
148
+     * @returns {Component} - The tab.
149
+     */
150
+    _renderTab({ component, styles, tabId }) {
151
+        const { closeDialog } = this.props;
152
+        const TabComponent = component;
153
+
154
+        return (
155
+            <div className = { styles }>
156
+                <TabComponent
157
+                    closeDialog = { closeDialog }
158
+                    onTabStateChange
159
+                        = { this._onTabStateChange }
160
+                    tabId = { tabId }
161
+                    { ...this.state.tabStates[tabId] } />
162
+            </div>);
163
+    }
164
+
165
+    _onTabStateChange: (number, Object) => void;
166
+
167
+    /**
168
+     * Changes the state for a tab.
169
+     *
170
+     * @param {number} tabId - The id of the tab which state will be changed.
171
+     * @param {Object} state - The new state.
172
+     * @returns {void}
173
+     */
174
+    _onTabStateChange(tabId, state) {
175
+        const tabStates = [ ...this.state.tabStates ];
176
+
177
+        tabStates[tabId] = state;
178
+        this.setState({ tabStates });
179
+    }
180
+
181
+    _onSubmit: () => void;
182
+
183
+    /**
184
+     * Submits the information filled in the dialog.
185
+     *
186
+     * @returns {void}
187
+     */
188
+    _onSubmit() {
189
+        const { onSubmit, tabs } = this.props;
190
+
191
+        tabs.forEach(({ submit }, idx) => {
192
+            submit(this.state.tabStates[idx]);
193
+        });
194
+
195
+        onSubmit();
196
+    }
197
+}
198
+
199
+export default translate(DialogWithTabs);

+ 5
- 0
react/features/base/dialog/components/index.js 查看文件

@@ -1,4 +1,9 @@
1
+// @flow
2
+
1 3
 export { default as BottomSheet } from './BottomSheet';
2 4
 export { default as DialogContainer } from './DialogContainer';
3 5
 export { default as Dialog } from './Dialog';
4 6
 export { default as StatelessDialog } from './StatelessDialog';
7
+export { default as DialogWithTabs } from './DialogWithTabs';
8
+export { default as AbstractDialogTab } from './AbstractDialogTab';
9
+export type { Props as AbstractDialogTabProps } from './AbstractDialogTab';

+ 35
- 52
react/features/device-selection/actions.js 查看文件

@@ -1,5 +1,3 @@
1
-/* globals APP, interfaceConfig */
2
-
3 1
 import { API_ID } from '../../../modules/API/constants';
4 2
 import {
5 3
     PostMessageTransportBackend,
@@ -12,66 +10,18 @@ import {
12 10
     setAudioOutputDevice,
13 11
     setVideoInputDevice
14 12
 } from '../base/devices';
15
-import { openDialog } from '../base/dialog';
16 13
 import { i18next } from '../base/i18n';
17 14
 import JitsiMeetJS from '../base/lib-jitsi-meet';
18 15
 
19 16
 import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
20
-import { DeviceSelectionDialog } from './components';
21
-
22
-/**
23
- * Open DeviceSelectionDialog with a configuration based on the environment's
24
- * supported abilities.
25
- *
26
- * @returns {Function}
27
- */
28
-export function openDeviceSelectionDialog() {
29
-    return dispatch => {
30
-        if (interfaceConfig.filmStripOnly) {
31
-            dispatch(_openDeviceSelectionDialogInPopup());
32
-        } else {
33
-            dispatch(_openDeviceSelectionDialogHere());
34
-        }
35
-    };
36
-}
37
-
38
-/**
39
- * Opens the DeviceSelectionDialog in the same window.
40
- *
41
- * @returns {Function}
42
- */
43
-function _openDeviceSelectionDialogHere() {
44
-    return dispatch =>
45
-        JitsiMeetJS.mediaDevices.isDeviceListAvailable()
46
-            .then(isDeviceListAvailable => {
47
-                const settings = APP.store.getState()['features/base/settings'];
48
-
49
-                dispatch(openDialog(DeviceSelectionDialog, {
50
-                    currentAudioInputId: settings.micDeviceId,
51
-                    currentAudioOutputId: getAudioOutputDeviceId(),
52
-                    currentVideoInputId: settings.cameraDeviceId,
53
-                    disableAudioInputChange:
54
-                        !JitsiMeetJS.isMultipleAudioInputSupported(),
55
-                    disableDeviceChange: !isDeviceListAvailable
56
-                        || !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
57
-                    hasAudioPermission: JitsiMeetJS.mediaDevices
58
-                        .isDevicePermissionGranted.bind(null, 'audio'),
59
-                    hasVideoPermission: JitsiMeetJS.mediaDevices
60
-                        .isDevicePermissionGranted.bind(null, 'video'),
61
-                    hideAudioInputPreview:
62
-                        !JitsiMeetJS.isCollectingLocalStats(),
63
-                    hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
64
-                        .isDeviceChangeAvailable('output')
65
-                }));
66
-            });
67
-}
17
+import { getDeviceSelectionDialogProps } from './functions';
68 18
 
69 19
 /**
70 20
  * Opens a popup window with the device selection dialog in it.
71 21
  *
72 22
  * @returns {Function}
73 23
  */
74
-function _openDeviceSelectionDialogInPopup() {
24
+export function openDeviceSelectionPopup() {
75 25
     return (dispatch, getState) => {
76 26
         const { popupDialogData } = getState()['features/device-selection'];
77 27
 
@@ -218,3 +168,36 @@ function _setDeviceSelectionPopupData(popupDialogData) {
218 168
         popupDialogData
219 169
     };
220 170
 }
171
+
172
+/**
173
+ * Submits the settings related to device selection.
174
+ *
175
+ * @param {Object} newState - The new settings.
176
+ * @returns {Function}
177
+ */
178
+export function submitDeviceSelectionTab(newState) {
179
+    return (dispatch, getState) => {
180
+        const currentState = getDeviceSelectionDialogProps(getState());
181
+
182
+        if (newState.selectedVideoInputId
183
+            && newState.selectedVideoInputId
184
+                !== currentState.selectedVideoInputId) {
185
+            dispatch(
186
+                setVideoInputDevice(newState.selectedVideoInputId));
187
+        }
188
+
189
+        if (newState.selectedAudioInputId
190
+                && newState.selectedAudioInputId
191
+                  !== currentState.selectedAudioInputId) {
192
+            dispatch(
193
+                setAudioInputDevice(newState.selectedAudioInputId));
194
+        }
195
+
196
+        if (newState.selectedAudioOutputId
197
+                && newState.selectedAudioOutputId
198
+                    !== currentState.selectedAudioOutputId) {
199
+            dispatch(
200
+                setAudioOutputDevice(newState.selectedAudioOutputId));
201
+        }
202
+    };
203
+}

+ 354
- 0
react/features/device-selection/components/DeviceSelection.js 查看文件

@@ -0,0 +1,354 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { AbstractDialogTab } from '../../base/dialog';
6
+import type { Props as AbstractDialogTabProps } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
8
+import { createLocalTrack } from '../../base/lib-jitsi-meet';
9
+
10
+import AudioInputPreview from './AudioInputPreview';
11
+import AudioOutputPreview from './AudioOutputPreview';
12
+import DeviceSelector from './DeviceSelector';
13
+import VideoInputPreview from './VideoInputPreview';
14
+
15
+/**
16
+ * The type of the React {@code Component} props of {@link DeviceSelection}.
17
+ */
18
+export type Props = {
19
+    ...$Exact<AbstractDialogTabProps>,
20
+
21
+    /**
22
+     * All known audio and video devices split by type. This prop comes from
23
+     * the app state.
24
+     */
25
+    availableDevices: Object,
26
+
27
+    /**
28
+     * Whether or not the audio selector can be interacted with. If true,
29
+     * the audio input selector will be rendered as disabled. This is
30
+     * specifically used to prevent audio device changing in Firefox, which
31
+     * currently does not work due to a browser-side regression.
32
+     */
33
+    disableAudioInputChange: boolean,
34
+
35
+    /**
36
+     * True if device changing is configured to be disallowed. Selectors
37
+     * will display as disabled.
38
+     */
39
+    disableDeviceChange: boolean,
40
+
41
+    /**
42
+     * Function that checks whether or not a new audio input source can be
43
+     * selected.
44
+     */
45
+    hasAudioPermission: Function,
46
+
47
+    /**
48
+     * Function that checks whether or not a new video input sources can be
49
+     * selected.
50
+     */
51
+    hasVideoPermission: Function,
52
+
53
+    /**
54
+     * If true, the audio meter will not display. Necessary for browsers or
55
+     * configurations that do not support local stats to prevent a
56
+     * non-responsive mic preview from displaying.
57
+     */
58
+    hideAudioInputPreview: boolean,
59
+
60
+    /**
61
+     * Whether or not the audio output source selector should display. If
62
+     * true, the audio output selector and test audio link will not be
63
+     * rendered. This is specifically used for hiding audio output on
64
+     * temasys browsers which do not support such change.
65
+     */
66
+    hideAudioOutputSelect: boolean,
67
+
68
+    /**
69
+     * The id of the audio input device to preview.
70
+     */
71
+    selectedAudioInputId: string,
72
+
73
+    /**
74
+     * The id of the audio output device to preview.
75
+     */
76
+    selectedAudioOutputId: string,
77
+
78
+    /**
79
+     * The id of the video input device to preview.
80
+     */
81
+    selectedVideoInputId: string,
82
+
83
+    /**
84
+     * Invoked to obtain translated strings.
85
+     */
86
+    t: Function
87
+};
88
+
89
+/**
90
+ * The type of the React {@code Component} state of {@link DeviceSelection}.
91
+ */
92
+type State = {
93
+
94
+    /**
95
+     * The JitsiTrack to use for previewing audio input.
96
+     */
97
+    previewAudioTrack: ?Object,
98
+
99
+    /**
100
+     * The JitsiTrack to use for previewing video input.
101
+     */
102
+    previewVideoTrack: ?Object,
103
+
104
+    /**
105
+     * The error message from trying to use a video input device.
106
+     */
107
+    previewVideoTrackError: ?string
108
+};
109
+
110
+/**
111
+ * React {@code Component} for previewing audio and video input/output devices.
112
+ *
113
+ * @extends Component
114
+ */
115
+class DeviceSelection extends AbstractDialogTab<Props, State> {
116
+    /**
117
+     * Initializes a new DeviceSelection instance.
118
+     *
119
+     * @param {Object} props - The read-only React Component props with which
120
+     * the new instance is to be initialized.
121
+     */
122
+    constructor(props: Props) {
123
+        super(props);
124
+
125
+        this.state = {
126
+            previewAudioTrack: null,
127
+            previewVideoTrack: null,
128
+            previewVideoTrackError: null
129
+        };
130
+    }
131
+
132
+    /**
133
+     * Generate the initial previews for audio input and video input.
134
+     *
135
+     * @inheritdoc
136
+     */
137
+    componentDidMount() {
138
+        this._createAudioInputTrack(this.props.selectedAudioInputId);
139
+        this._createVideoInputTrack(this.props.selectedVideoInputId);
140
+    }
141
+
142
+    /**
143
+     * Updates audio input and video input previews.
144
+     *
145
+     * @inheritdoc
146
+     * @param {Object} nextProps - The read-only props which this Component will
147
+     * receive.
148
+     * @returns {void}
149
+     */
150
+    componentWillReceiveProps(nextProps: Object) {
151
+        const { selectedAudioInputId, selectedVideoInputId } = this.props;
152
+
153
+        if (selectedAudioInputId !== nextProps.selectedAudioInputId) {
154
+            this._createAudioInputTrack(nextProps.selectedAudioInputId);
155
+        }
156
+
157
+        if (selectedVideoInputId !== nextProps.selectedVideoInputId) {
158
+            this._createVideoInputTrack(nextProps.selectedVideoInputId);
159
+        }
160
+    }
161
+
162
+    /**
163
+     * Ensure preview tracks are destroyed to prevent continued use.
164
+     *
165
+     * @inheritdoc
166
+     */
167
+    componentWillUnmount() {
168
+        this._disposeAudioInputPreview();
169
+        this._disposeVideoInputPreview();
170
+    }
171
+
172
+    /**
173
+     * Implements React's {@link Component#render()}.
174
+     *
175
+     * @inheritdoc
176
+     */
177
+    render() {
178
+        const {
179
+            hideAudioInputPreview,
180
+            hideAudioOutputSelect,
181
+            selectedAudioOutputId
182
+        } = this.props;
183
+
184
+        return (
185
+            <div className = 'device-selection'>
186
+                <div className = 'device-selection-column column-video'>
187
+                    <div className = 'device-selection-video-container'>
188
+                        <VideoInputPreview
189
+                            error = { this.state.previewVideoTrackError }
190
+                            track = { this.state.previewVideoTrack } />
191
+                    </div>
192
+                    { !hideAudioInputPreview
193
+                        && <AudioInputPreview
194
+                            track = { this.state.previewAudioTrack } /> }
195
+                </div>
196
+                <div className = 'device-selection-column column-selectors'>
197
+                    <div className = 'device-selectors'>
198
+                        { this._renderSelectors() }
199
+                    </div>
200
+                    { !hideAudioOutputSelect
201
+                        && <AudioOutputPreview
202
+                            deviceId = { selectedAudioOutputId } /> }
203
+                </div>
204
+            </div>
205
+        );
206
+    }
207
+
208
+    /**
209
+     * Creates the JitiTrack for the audio input preview.
210
+     *
211
+     * @param {string} deviceId - The id of audio input device to preview.
212
+     * @private
213
+     * @returns {void}
214
+     */
215
+    _createAudioInputTrack(deviceId) {
216
+        this._disposeAudioInputPreview()
217
+            .then(() => createLocalTrack('audio', deviceId))
218
+            .then(jitsiLocalTrack => {
219
+                this.setState({
220
+                    previewAudioTrack: jitsiLocalTrack
221
+                });
222
+            })
223
+            .catch(() => {
224
+                this.setState({
225
+                    previewAudioTrack: null
226
+                });
227
+            });
228
+    }
229
+
230
+    /**
231
+     * Creates the JitiTrack for the video input preview.
232
+     *
233
+     * @param {string} deviceId - The id of video device to preview.
234
+     * @private
235
+     * @returns {void}
236
+     */
237
+    _createVideoInputTrack(deviceId) {
238
+        this._disposeVideoInputPreview()
239
+            .then(() => createLocalTrack('video', deviceId))
240
+            .then(jitsiLocalTrack => {
241
+                if (!jitsiLocalTrack) {
242
+                    return Promise.reject();
243
+                }
244
+
245
+                this.setState({
246
+                    previewVideoTrack: jitsiLocalTrack,
247
+                    previewVideoTrackError: null
248
+                });
249
+            })
250
+            .catch(() => {
251
+                this.setState({
252
+                    previewVideoTrack: null,
253
+                    previewVideoTrackError:
254
+                        this.props.t('deviceSelection.previewUnavailable')
255
+                });
256
+            });
257
+    }
258
+
259
+    /**
260
+     * Utility function for disposing the current audio input preview.
261
+     *
262
+     * @private
263
+     * @returns {Promise}
264
+     */
265
+    _disposeAudioInputPreview(): Promise<*> {
266
+        return this.state.previewAudioTrack
267
+            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
268
+    }
269
+
270
+    /**
271
+     * Utility function for disposing the current video input preview.
272
+     *
273
+     * @private
274
+     * @returns {Promise}
275
+     */
276
+    _disposeVideoInputPreview(): Promise<*> {
277
+        return this.state.previewVideoTrack
278
+            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
279
+    }
280
+
281
+    /**
282
+     * Creates a DeviceSelector instance based on the passed in configuration.
283
+     *
284
+     * @private
285
+     * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
286
+     * @returns {ReactElement}
287
+     */
288
+    _renderSelector(deviceSelectorProps) {
289
+        return (
290
+            <div key = { deviceSelectorProps.label }>
291
+                <div className = 'device-selector-label'>
292
+                    { this.props.t(deviceSelectorProps.label) }
293
+                </div>
294
+                <DeviceSelector { ...deviceSelectorProps } />
295
+            </div>
296
+        );
297
+    }
298
+
299
+    /**
300
+     * Creates DeviceSelector instances for video output, audio input, and audio
301
+     * output.
302
+     *
303
+     * @private
304
+     * @returns {Array<ReactElement>} DeviceSelector instances.
305
+     */
306
+    _renderSelectors() {
307
+        const { availableDevices } = this.props;
308
+
309
+        const configurations = [
310
+            {
311
+                devices: availableDevices.videoInput,
312
+                hasPermission: this.props.hasVideoPermission(),
313
+                icon: 'icon-camera',
314
+                isDisabled: this.props.disableDeviceChange,
315
+                key: 'videoInput',
316
+                label: 'settings.selectCamera',
317
+                onSelect: selectedVideoInputId =>
318
+                    super._onChange({ selectedVideoInputId }),
319
+                selectedDeviceId: this.props.selectedVideoInputId
320
+            },
321
+            {
322
+                devices: availableDevices.audioInput,
323
+                hasPermission: this.props.hasAudioPermission(),
324
+                icon: 'icon-microphone',
325
+                isDisabled: this.props.disableAudioInputChange
326
+                    || this.props.disableDeviceChange,
327
+                key: 'audioInput',
328
+                label: 'settings.selectMic',
329
+                onSelect: selectedAudioInputId =>
330
+                    super._onChange({ selectedAudioInputId }),
331
+                selectedDeviceId: this.props.selectedAudioInputId
332
+            }
333
+        ];
334
+
335
+        if (!this.props.hideAudioOutputSelect) {
336
+            configurations.push({
337
+                devices: availableDevices.audioOutput,
338
+                hasPermission: this.props.hasAudioPermission()
339
+                    || this.props.hasVideoPermission(),
340
+                icon: 'icon-volume',
341
+                isDisabled: this.props.disableDeviceChange,
342
+                key: 'audioOutput',
343
+                label: 'settings.selectAudioOutput',
344
+                onSelect: selectedAudioOutputId =>
345
+                    super._onChange({ selectedAudioOutputId }),
346
+                selectedDeviceId: this.props.selectedAudioOutputId
347
+            });
348
+        }
349
+
350
+        return configurations.map(config => this._renderSelector(config));
351
+    }
352
+}
353
+
354
+export default translate(DeviceSelection);

+ 0
- 165
react/features/device-selection/components/DeviceSelectionDialog.js 查看文件

@@ -1,165 +0,0 @@
1
-import PropTypes from 'prop-types';
2
-import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4
-
5
-import {
6
-    setAudioInputDevice,
7
-    setAudioOutputDevice,
8
-    setVideoInputDevice
9
-} from '../../base/devices';
10
-import { hideDialog } from '../../base/dialog';
11
-
12
-import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
13
-
14
-/**
15
- * React component for previewing and selecting new audio and video sources.
16
- *
17
- * @extends Component
18
- */
19
-class DeviceSelectionDialog extends Component {
20
-    /**
21
-     * DeviceSelectionDialog component's property types.
22
-     *
23
-     * @static
24
-     */
25
-    static propTypes = {
26
-        /**
27
-         * All known audio and video devices split by type. This prop comes from
28
-         * the app state.
29
-         */
30
-        _availableDevices: PropTypes.object,
31
-
32
-        /**
33
-         * Device id for the current audio input device. This device will be set
34
-         * as the default audio input device to preview.
35
-         */
36
-        currentAudioInputId: PropTypes.string,
37
-
38
-        /**
39
-         * Device id for the current audio output device. This device will be
40
-         * set as the default audio output device to preview.
41
-         */
42
-        currentAudioOutputId: PropTypes.string,
43
-
44
-        /**
45
-         * Device id for the current video input device. This device will be set
46
-         * as the default video input device to preview.
47
-         */
48
-        currentVideoInputId: PropTypes.string,
49
-
50
-        /**
51
-         * Whether or not the audio selector can be interacted with. If true,
52
-         * the audio input selector will be rendered as disabled. This is
53
-         * specifically used to prevent audio device changing in Firefox, which
54
-         * currently does not work due to a browser-side regression.
55
-         */
56
-        disableAudioInputChange: PropTypes.bool,
57
-
58
-        /**
59
-         * True if device changing is configured to be disallowed. Selectors
60
-         * will display as disabled.
61
-         */
62
-        disableDeviceChange: PropTypes.bool,
63
-
64
-        /**
65
-         * Invoked to notify the store of app state changes.
66
-         */
67
-        dispatch: PropTypes.func,
68
-
69
-        /**
70
-         * Function that checks whether or not a new audio input source can be
71
-         * selected.
72
-         */
73
-        hasAudioPermission: PropTypes.func,
74
-
75
-        /**
76
-         * Function that checks whether or not a new video input sources can be
77
-         * selected.
78
-         */
79
-        hasVideoPermission: PropTypes.func,
80
-
81
-        /**
82
-         * If true, the audio meter will not display. Necessary for browsers or
83
-         * configurations that do not support local stats to prevent a
84
-         * non-responsive mic preview from displaying.
85
-         */
86
-        hideAudioInputPreview: PropTypes.bool,
87
-
88
-        /**
89
-         * Whether or not the audio output source selector should display. If
90
-         * true, the audio output selector and test audio link will not be
91
-         * rendered. This is specifically used for hiding audio output on
92
-         * temasys browsers which do not support such change.
93
-         */
94
-        hideAudioOutputSelect: PropTypes.bool
95
-    };
96
-
97
-    /**
98
-     * Implements React's {@link Component#render()}.
99
-     *
100
-     * @inheritdoc
101
-     */
102
-    render() {
103
-        const {
104
-            currentAudioInputId,
105
-            currentAudioOutputId,
106
-            currentVideoInputId,
107
-            disableAudioInputChange,
108
-            disableDeviceChange,
109
-            dispatch,
110
-            hasAudioPermission,
111
-            hasVideoPermission,
112
-            hideAudioInputPreview,
113
-            hideAudioOutputSelect
114
-        } = this.props;
115
-
116
-        const props = {
117
-            availableDevices: this.props._availableDevices,
118
-            closeModal: () => dispatch(hideDialog()),
119
-            currentAudioInputId,
120
-            currentAudioOutputId,
121
-            currentVideoInputId,
122
-            disableAudioInputChange,
123
-            disableDeviceChange,
124
-            hasAudioPermission,
125
-            hasVideoPermission,
126
-            hideAudioInputPreview,
127
-            hideAudioOutputSelect,
128
-            setAudioInputDevice: id => {
129
-                dispatch(setAudioInputDevice(id));
130
-
131
-                return Promise.resolve();
132
-            },
133
-            setAudioOutputDevice: id => {
134
-                dispatch(setAudioOutputDevice(id));
135
-
136
-                return Promise.resolve();
137
-            },
138
-            setVideoInputDevice: id => {
139
-                dispatch(setVideoInputDevice(id));
140
-
141
-                return Promise.resolve();
142
-            }
143
-        };
144
-
145
-        return <DeviceSelectionDialogBase { ...props } />;
146
-    }
147
-}
148
-
149
-/**
150
- * Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
151
- * props.
152
- *
153
- * @param {Object} state - The Redux state.
154
- * @private
155
- * @returns {{
156
- *     _availableDevices: Object
157
- * }}
158
- */
159
-function _mapStateToProps(state) {
160
-    return {
161
-        _availableDevices: state['features/base/devices']
162
-    };
163
-}
164
-
165
-export default connect(_mapStateToProps)(DeviceSelectionDialog);

+ 0
- 560
react/features/device-selection/components/DeviceSelectionDialogBase.js 查看文件

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

+ 4
- 3
react/features/device-selection/components/index.js 查看文件

@@ -1,3 +1,4 @@
1
-export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
2
-export { default as DeviceSelectionDialogBase }
3
-    from './DeviceSelectionDialogBase';
1
+// @flow
2
+
3
+export { default as DeviceSelection } from './DeviceSelection';
4
+export type { Props as DeviceSelectionProps } from './DeviceSelection';

+ 35
- 0
react/features/device-selection/functions.js 查看文件

@@ -0,0 +1,35 @@
1
+// @flow
2
+import { getAudioOutputDeviceId } from '../base/devices';
3
+import JitsiMeetJS from '../base/lib-jitsi-meet';
4
+import { toState } from '../base/redux';
5
+
6
+/**
7
+ * Returns the properties for the device selection dialog from Redux state.
8
+ *
9
+ * @param {(Function|Object)} stateful -The (whole) redux state, or redux's
10
+ * {@code getState} function to be used to retrieve the state.
11
+ * @returns {Object} - The properties for the device selection dialog.
12
+ */
13
+export function getDeviceSelectionDialogProps(stateful: Object | Function) {
14
+    const state = toState(stateful);
15
+    const settings = state['features/base/settings'];
16
+
17
+    return {
18
+        availableDevices: state['features/base/devices'],
19
+        disableAudioInputChange:
20
+            !JitsiMeetJS.isMultipleAudioInputSupported(),
21
+        disableDeviceChange:
22
+            !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
23
+        hasAudioPermission: JitsiMeetJS.mediaDevices
24
+            .isDevicePermissionGranted.bind(null, 'audio'),
25
+        hasVideoPermission: JitsiMeetJS.mediaDevices
26
+            .isDevicePermissionGranted.bind(null, 'video'),
27
+        hideAudioInputPreview:
28
+            !JitsiMeetJS.isCollectingLocalStats(),
29
+        hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
30
+                            .isDeviceChangeAvailable('output'),
31
+        selectedAudioInputId: settings.micDeviceId,
32
+        selectedAudioOutputId: getAudioOutputDeviceId(),
33
+        selectedVideoInputId: settings.cameraDeviceId
34
+    };
35
+}

+ 1
- 0
react/features/device-selection/index.js 查看文件

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

react/features/device-selection/DeviceSelectionPopup.js → react/features/settings/DeviceSelectionPopup.js 查看文件

@@ -11,8 +11,11 @@ import {
11 11
     Transport
12 12
 } from '../../../modules/transport';
13 13
 import { parseURLParams } from '../base/config';
14
+import { DeviceSelection } from '../device-selection';
14 15
 
15
-import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase';
16
+// Using the full path to the file to prevent adding unnecessary code into the
17
+// dialog popup bundle.
18
+import DialogWithTabs from '../base/dialog/components/DialogWithTabs';
16 19
 
17 20
 const logger = Logger.getLogger(__filename);
18 21
 
@@ -29,10 +32,9 @@ export default class DeviceSelectionPopup {
29 32
      */
30 33
     constructor(i18next) {
31 34
         this.close = this.close.bind(this);
32
-        this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
33
-        this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
34
-        this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
35 35
         this._i18next = i18next;
36
+        this._onSubmit = this._onSubmit.bind(this);
37
+
36 38
         const { scope } = parseURLParams(window.location);
37 39
 
38 40
         this._transport = new Transport({
@@ -56,10 +58,11 @@ export default class DeviceSelectionPopup {
56 58
 
57 59
         this._dialogProps = {
58 60
             availableDevices: {},
59
-            currentAudioInputId: '',
60
-            currentAudioOutputId: '',
61
-            currentVideoInputId: '',
61
+            selectedAudioInputId: '',
62
+            selectedAudioOutputId: '',
63
+            selectedVideoInputId: '',
62 64
             disableAudioInputChange: true,
65
+            disableBlanketClickDismiss: true,
63 66
             disableDeviceChange: true,
64 67
             hasAudioPermission: JitsiMeetJS.mediaDevices
65 68
                 .isDevicePermissionGranted.bind(null, 'audio'),
@@ -67,6 +70,7 @@ export default class DeviceSelectionPopup {
67 70
                 .isDevicePermissionGranted.bind(null, 'video'),
68 71
             hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
69 72
             hideAudioOutputSelect: true
73
+
70 74
         };
71 75
         this._initState();
72 76
     }
@@ -153,9 +157,9 @@ export default class DeviceSelectionPopup {
153 157
         ]) => {
154 158
             this._changeDialogProps({
155 159
                 availableDevices,
156
-                currentAudioInputId: currentDevices.audioInput,
157
-                currentAudioOutputId: currentDevices.audioOutput,
158
-                currentVideoInputId: currentDevices.videoInput,
160
+                selectedAudioInputId: currentDevices.audioInput,
161
+                selectedAudioOutputId: currentDevices.audioOutput,
162
+                selectedVideoInputId: currentDevices.videoInput,
159 163
                 disableAudioInputChange: !multiAudioInputSupported,
160 164
                 disableDeviceChange: !listAvailable || !changeAvailable,
161 165
                 hideAudioOutputSelect: !changeOutputAvailable
@@ -217,26 +221,58 @@ export default class DeviceSelectionPopup {
217 221
         });
218 222
     }
219 223
 
224
+    /**
225
+     * Callback invoked to save changes to selected devices and close the
226
+     * dialog.
227
+     *
228
+     * @param {Object} newSettings - The chosen device IDs.
229
+     * @private
230
+     * @returns {void}
231
+     */
232
+    _onSubmit(newSettings) {
233
+        const promises = [];
234
+
235
+        if (newSettings.selectedVideoInputId
236
+                !== this._dialogProps.selectedVideoInputId) {
237
+            promises.push(
238
+                this._setVideoInputDevice(newSettings.selectedVideoInputId));
239
+        }
240
+
241
+        if (newSettings.selectedAudioInputId
242
+                !== this._dialogProps.selectedAudioInputId) {
243
+            promises.push(
244
+                this._setAudioInputDevice(newSettings.selectedAudioInputId));
245
+        }
246
+
247
+        if (newSettings.selectedAudioOutputId
248
+                !== this._dialogProps.selectedAudioOutputId) {
249
+            promises.push(
250
+                this._setAudioOutputDevice(newSettings.selectedAudioOutputId));
251
+        }
252
+
253
+        Promise.all(promises).then(this.close, this.close);
254
+    }
255
+
220 256
     /**
221 257
      * Renders the React components for the popup page.
222 258
      *
223 259
      * @returns {void}
224 260
      */
225 261
     _render() {
226
-        const props = {
227
-            ...this._dialogProps,
228
-            closeModal: this.close,
229
-            disableBlanketClickDismiss: true,
230
-            setAudioInputDevice: this._setAudioInputDevice,
231
-            setAudioOutputDevice: this._setAudioOutputDevice,
232
-            setVideoInputDevice: this._setVideoInputDevice
233
-        };
262
+        const onSubmit = this.close;
234 263
 
235 264
         ReactDOM.render(
236
-            <I18nextProvider
237
-                i18n = { this._i18next }>
265
+            <I18nextProvider i18n = { this._i18next }>
238 266
                 <AtlasKitThemeProvider mode = 'dark'>
239
-                    <DeviceSelectionDialogBase { ...props } />
267
+                    <DialogWithTabs
268
+                        closeDialog = { this.close }
269
+                        onSubmit = { onSubmit }
270
+                        tabs = { [ {
271
+                            component: DeviceSelection,
272
+                            label: 'settings.devices',
273
+                            props: this._dialogProps,
274
+                            submit: this._onSubmit
275
+                        } ] } />
240 276
                 </AtlasKitThemeProvider>
241 277
             </I18nextProvider>,
242 278
             document.getElementById('react'));

+ 66
- 0
react/features/settings/actions.js 查看文件

@@ -1,6 +1,14 @@
1 1
 // @flow
2 2
 
3
+import { setFollowMe, setStartMutedPolicy } from '../base/conference';
4
+import { openDialog } from '../base/dialog';
5
+import { i18next } from '../base/i18n';
6
+
3 7
 import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
8
+import { SettingsDialog } from './components';
9
+import { getMoreTabProps, getProfileTabProps } from './functions';
10
+
11
+declare var APP: Object;
4 12
 
5 13
 /**
6 14
  * Sets the visibility of the view/UI which renders the app's settings.
@@ -18,3 +26,61 @@ export function setSettingsViewVisible(visible: boolean) {
18 26
         visible
19 27
     };
20 28
 }
29
+
30
+/**
31
+ * Opens {@code SettingsDialog}.
32
+ *
33
+ * @param {string} defaultTab - The tab in {@code SettingsDialog} that should be
34
+ * displayed initially.
35
+ * @returns {Function}
36
+ */
37
+export function openSettingsDialog(defaultTab: string) {
38
+    return openDialog(SettingsDialog, { defaultTab });
39
+}
40
+
41
+
42
+/**
43
+ * Submits the settings from the "More" tab of the settings dialog.
44
+ *
45
+ * @param {Object} newState - The new settings.
46
+ * @returns {Function}
47
+ */
48
+export function submitMoreTab(newState: Object): Function {
49
+    return (dispatch, getState) => {
50
+        const currentState = getMoreTabProps(getState());
51
+
52
+        if (newState.followMeEnabled !== currentState.followMeEnabled) {
53
+            dispatch(setFollowMe(newState.followMeEnabled));
54
+        }
55
+
56
+        if (newState.startAudioMuted !== currentState.startAudioMuted
57
+            || newState.startVideoMuted !== currentState.startVideoMuted) {
58
+            dispatch(setStartMutedPolicy(
59
+                newState.startAudioMuted, newState.startVideoMuted));
60
+        }
61
+
62
+        if (newState.currentLanguage !== currentState.currentLanguage) {
63
+            i18next.changeLanguage(newState.currentLanguage);
64
+        }
65
+    };
66
+}
67
+
68
+/**
69
+ * Submits the settings from the "Profile" tab of the settings dialog.
70
+ *
71
+ * @param {Object} newState - The new settings.
72
+ * @returns {Function}
73
+ */
74
+export function submitProfileTab(newState: Object): Function {
75
+    return (dispatch, getState) => {
76
+        const currentState = getProfileTabProps(getState());
77
+
78
+        if (newState.displayName !== currentState.displayName) {
79
+            APP.conference.changeLocalDisplayName(newState.displayName);
80
+        }
81
+
82
+        if (newState.email !== currentState.email) {
83
+            APP.conference.changeLocalEmail(newState.email);
84
+        }
85
+    };
86
+}

+ 0
- 87
react/features/settings/components/web/DeviceSelectionButton.js 查看文件

@@ -1,87 +0,0 @@
1
-import Button from '@atlaskit/button';
2
-import PropTypes from 'prop-types';
3
-import React, { Component } from 'react';
4
-import { connect } from 'react-redux';
5
-
6
-import { translate } from '../../../base/i18n';
7
-import { openDeviceSelectionDialog } from '../../../device-selection';
8
-
9
-/**
10
- * Implements a React {@link Component} which displays a button for opening the
11
- * {@code DeviceSelectionDialog}.
12
- *
13
- * @extends Component
14
- */
15
-class DeviceSelectionButton extends Component {
16
-    /**
17
-     * {@code DeviceSelectionButton} component's property types.
18
-     *
19
-     * @static
20
-     */
21
-    static propTypes = {
22
-        /**
23
-         * Invoked to display the {@code DeviceSelectionDialog}.
24
-         */
25
-        dispatch: PropTypes.func,
26
-
27
-        /**
28
-         * Whether or not the button's title should be displayed.
29
-         */
30
-        showTitle: PropTypes.bool,
31
-
32
-        /**
33
-         * Invoked to obtain translated strings.
34
-         */
35
-        t: PropTypes.func
36
-    };
37
-
38
-    /**
39
-     * Initializes a new {@code DeviceSelectionButton} instance.
40
-     *
41
-     * @param {Object} props - The read-only properties with which the new
42
-     * instance is to be initialized.
43
-     */
44
-    constructor(props) {
45
-        super(props);
46
-
47
-        // Bind event handler so it is only bound once for every instance.
48
-        this._onOpenDeviceSelectionDialog
49
-            = this._onOpenDeviceSelectionDialog.bind(this);
50
-    }
51
-
52
-    /**
53
-     * Implements React's {@link Component#render()}.
54
-     *
55
-     * @inheritdoc
56
-     * @returns {ReactElement}
57
-     */
58
-    render() {
59
-        return (
60
-            <div>
61
-                { this.props.showTitle
62
-                    ? <div className = 'subTitle'>
63
-                        { this.props.t('settings.audioVideo') }
64
-                    </div>
65
-                    : null }
66
-                <Button
67
-                    appearance = 'primary'
68
-                    onClick = { this._onOpenDeviceSelectionDialog }
69
-                    shouldFitContainer = { true }>
70
-                    { this.props.t('deviceSelection.deviceSettings') }
71
-                </Button>
72
-            </div>
73
-        );
74
-    }
75
-
76
-    /**
77
-     * Opens the {@code DeviceSelectionDialog}.
78
-     *
79
-     * @private
80
-     * @returns {void}
81
-     */
82
-    _onOpenDeviceSelectionDialog() {
83
-        this.props.dispatch(openDeviceSelectionDialog());
84
-    }
85
-}
86
-
87
-export default translate(connect()(DeviceSelectionButton));

+ 0
- 179
react/features/settings/components/web/LanguageSelectDropdown.js 查看文件

@@ -1,179 +0,0 @@
1
-import DropdownMenu, {
2
-    DropdownItem,
3
-    DropdownItemGroup
4
-} from '@atlaskit/dropdown-menu';
5
-import PropTypes from 'prop-types';
6
-import React, { Component } from 'react';
7
-
8
-import { DEFAULT_LANGUAGE, LANGUAGES, translate } from '../../../base/i18n';
9
-
10
-/**
11
- * Implements a React {@link Component} which displays a dropdown for changing
12
- * application text to another language.
13
- *
14
- * @extends Component
15
- */
16
-class LanguageSelectDropdown extends Component {
17
-    /**
18
-     * {@code LanguageSelectDropdown} component's property types.
19
-     *
20
-     * @static
21
-     */
22
-    static propTypes = {
23
-        /**
24
-         * The translation service.
25
-         */
26
-        i18n: PropTypes.object,
27
-
28
-        /**
29
-         * Invoked to obtain translated strings.
30
-         */
31
-        t: PropTypes.func
32
-    };
33
-
34
-
35
-    /**
36
-     * {@code LanguageSelectDropdown} component's local state.
37
-     *
38
-     * @type {Object}
39
-     * @property {string|null} currentLanguage - The currently selected language
40
-     * the application should be displayed in.
41
-     * @property {boolean} isLanguageSelectOpen - Whether or not the dropdown
42
-     * should be displayed as open.
43
-     */
44
-    state = {
45
-        currentLanguage: null,
46
-        isLanguageSelectOpen: false
47
-    };
48
-
49
-    /**
50
-     * Initializes a new {@code LanguageSelectDropdown} instance.
51
-     *
52
-     * @param {Object} props - The read-only properties with which the new
53
-     * instance is to be initialized.
54
-     */
55
-    constructor(props) {
56
-        super(props);
57
-
58
-        this.state.currentLanguage
59
-            = this.props.i18n.language || DEFAULT_LANGUAGE;
60
-
61
-        // Bind event handlers so they are only bound once for every instance.
62
-        this._onLanguageSelected = this._onLanguageSelected.bind(this);
63
-        this._onSetDropdownOpen = this._onSetDropdownOpen.bind(this);
64
-        this._setCurrentLanguage = this._setCurrentLanguage.bind(this);
65
-    }
66
-
67
-    /**
68
-     * Sets a listener to update the currently selected language if it is
69
-     * changed from somewhere else.
70
-     *
71
-     * @inheritdoc
72
-     * @returns {void}
73
-     */
74
-    componentDidMount() {
75
-        this.props.i18n.on('languageChanged', this._setCurrentLanguage);
76
-    }
77
-
78
-    /**
79
-     * Removes all listeners.
80
-     *
81
-     * @inheritdoc
82
-     * @returns {void}
83
-     */
84
-    componentWillUnmount() {
85
-        this.props.i18n.off('languageChanged', this._setCurrentLanguage);
86
-    }
87
-
88
-    /**
89
-     * Implements React's {@link Component#render()}.
90
-     *
91
-     * @inheritdoc
92
-     * @returns {ReactElement}
93
-     */
94
-    render() {
95
-        const { t } = this.props;
96
-        const { currentLanguage } = this.state;
97
-
98
-        const languageItems = LANGUAGES.map(language =>
99
-            // eslint-disable-next-line react/jsx-wrap-multilines
100
-            <DropdownItem
101
-                key = { language }
102
-                // eslint-disable-next-line react/jsx-no-bind
103
-                onClick = { () => this._onLanguageSelected(language) }>
104
-                { t(`languages:${language}`) }
105
-            </DropdownItem>
106
-        );
107
-
108
-        return (
109
-            <div>
110
-                <DropdownMenu
111
-                    isOpen = { this.state.isLanguageSelectOpen }
112
-                    onOpenChange = { this._onSetDropdownOpen }
113
-                    shouldFitContainer = { true }
114
-                    trigger = { currentLanguage
115
-                        ? t(`languages:${currentLanguage}`)
116
-                        : '' }
117
-                    triggerButtonProps = {{
118
-                        appearance: 'primary',
119
-                        shouldFitContainer: true
120
-                    }}
121
-                    triggerType = 'button'>
122
-                    <DropdownItemGroup>
123
-                        { languageItems }
124
-                    </DropdownItemGroup>
125
-                </DropdownMenu>
126
-            </div>
127
-        );
128
-    }
129
-
130
-    /**
131
-     * Updates the application's currently displayed language.
132
-     *
133
-     * @param {string} language - The language code for the language to display.
134
-     * @private
135
-     * @returns {void}
136
-     */
137
-    _onLanguageSelected(language) {
138
-        const previousLanguage = this.state.currentLanguage;
139
-
140
-        this.setState({
141
-            currentLanguage: language,
142
-            isLanguageSelectOpen: false
143
-        });
144
-
145
-        this.props.i18n.changeLanguage(language, error => {
146
-            if (error) {
147
-                this._setCurrentLanguage(previousLanguage);
148
-            }
149
-        });
150
-    }
151
-
152
-    /**
153
-     * Set whether or not the dropdown should be open.
154
-     *
155
-     * @param {Object} dropdownEvent - The event returned from requesting the
156
-     * open state of the dropdown be changed.
157
-     * @private
158
-     * @returns {void}
159
-     */
160
-    _onSetDropdownOpen(dropdownEvent) {
161
-        this.setState({
162
-            isLanguageSelectOpen: dropdownEvent.isOpen
163
-        });
164
-    }
165
-
166
-    /**
167
-     * Updates the known current language of the application.
168
-     *
169
-     * @param {string} currentLanguage - The language code for the current
170
-     * language.
171
-     * @private
172
-     * @returns {void}
173
-     */
174
-    _setCurrentLanguage(currentLanguage) {
175
-        this.setState({ currentLanguage });
176
-    }
177
-}
178
-
179
-export default translate(LanguageSelectDropdown);

+ 0
- 199
react/features/settings/components/web/ModeratorCheckboxes.js 查看文件

@@ -1,199 +0,0 @@
1
-import PropTypes from 'prop-types';
2
-import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4
-
5
-import { setFollowMe, setStartMutedPolicy } from '../../../base/conference';
6
-import { translate } from '../../../base/i18n';
7
-
8
-/**
9
- * Implements a React {@link Component} which displays checkboxes for enabling
10
- * and disabling moderator-only conference features.
11
- *
12
- * @extends Component
13
- */
14
-class ModeratorCheckboxes extends Component {
15
-    /**
16
-     * {@code ModeratorCheckboxes} component's property types.
17
-     *
18
-     * @static
19
-     */
20
-    static propTypes = {
21
-        /**
22
-         * Whether or not the Follow Me feature is currently enabled.
23
-         */
24
-        _followMeEnabled: PropTypes.bool,
25
-
26
-        /**
27
-         * Whether or not new members will join the conference as audio muted.
28
-         */
29
-        _startAudioMutedPolicy: PropTypes.bool,
30
-
31
-        /**
32
-         * Whether or note new member will join the conference as video muted.
33
-         */
34
-        _startVideoMutedPolicy: PropTypes.bool,
35
-
36
-        /**
37
-         * Invoked to enable and disable moderator-only conference features.
38
-         */
39
-        dispatch: PropTypes.func,
40
-
41
-        /**
42
-         * Whether or not the title should be displayed.
43
-         */
44
-        showTitle: PropTypes.bool,
45
-
46
-        /**
47
-         * Invokted to obtain translated strings.
48
-         */
49
-        t: PropTypes.func
50
-    };
51
-
52
-    /**
53
-     * Initializes a new {@code ModeratorCheckboxes} instance.
54
-     *
55
-     * @param {Object} props - The read-only properties with which the new
56
-     * instance is to be initialized.
57
-     */
58
-    constructor(props) {
59
-        super(props);
60
-
61
-        // Bind event handlers so they are only bound once for every instance.
62
-        this._onSetFollowMeSetting
63
-            = this._onSetFollowMeSetting.bind(this);
64
-        this._onSetStartAudioMutedPolicy
65
-            = this._onSetStartAudioMutedPolicy.bind(this);
66
-        this._onSetStartVideoMutedPolicy
67
-            = this._onSetStartVideoMutedPolicy.bind(this);
68
-    }
69
-
70
-    /**
71
-     * Implements React's {@link Component#render()}.
72
-     *
73
-     * @inheritdoc
74
-     * @returns {ReactElement}
75
-     */
76
-    render() {
77
-        const {
78
-            _followMeEnabled,
79
-            _startAudioMutedPolicy,
80
-            _startVideoMutedPolicy,
81
-            showTitle,
82
-            t
83
-        } = this.props;
84
-
85
-        return (
86
-            <div>
87
-                { showTitle
88
-                    ? <div className = 'subTitle'>
89
-                        { t('settings.moderator') }
90
-                    </div>
91
-                    : null }
92
-                <div className = 'moderator-option'>
93
-                    <input
94
-                        checked = { _startAudioMutedPolicy }
95
-                        className = 'moderator-checkbox'
96
-                        id = 'startAudioMuted'
97
-                        onChange = { this._onSetStartAudioMutedPolicy }
98
-                        type = 'checkbox' />
99
-                    <label
100
-                        className = 'moderator-checkbox-label'
101
-                        htmlFor = 'startAudioMuted'>
102
-                        { t('settings.startAudioMuted') }
103
-                    </label>
104
-                </div>
105
-                <div className = 'moderator-option'>
106
-                    <input
107
-                        checked = { _startVideoMutedPolicy }
108
-                        className = 'moderator-checkbox'
109
-                        id = 'startVideoMuted'
110
-                        onChange = { this._onSetStartVideoMutedPolicy }
111
-                        type = 'checkbox' />
112
-                    <label
113
-                        className = 'moderator-checkbox-label'
114
-                        htmlFor = 'startVideoMuted'>
115
-                        { t('settings.startVideoMuted') }
116
-                    </label>
117
-                </div>
118
-                <div className = 'moderator-option'>
119
-                    <input
120
-                        checked = { _followMeEnabled }
121
-                        className = 'moderator-checkbox'
122
-                        id = 'followMeCheckBox'
123
-                        onChange = { this._onSetFollowMeSetting }
124
-                        type = 'checkbox' />
125
-                    <label
126
-                        className = 'moderator-checkbox-label'
127
-                        htmlFor = 'followMeCheckBox'>
128
-                        { t('settings.followMe') }
129
-                    </label>
130
-                </div>
131
-            </div>
132
-        );
133
-    }
134
-
135
-    /**
136
-     * Toggles the Follow Me feature.
137
-     *
138
-     * @param {Object} event - The dom event returned from changes the checkbox.
139
-     * @private
140
-     * @returns {void}
141
-     */
142
-    _onSetFollowMeSetting(event) {
143
-        this.props.dispatch(setFollowMe(event.target.checked));
144
-    }
145
-
146
-    /**
147
-     * Toggles whether or not new members should join the conference as audio
148
-     * muted.
149
-     *
150
-     * @param {Object} event - The dom event returned from changes the checkbox.
151
-     * @private
152
-     * @returns {void}
153
-     */
154
-    _onSetStartAudioMutedPolicy(event) {
155
-        this.props.dispatch(setStartMutedPolicy(
156
-            event.target.checked, this.props._startVideoMutedPolicy));
157
-    }
158
-
159
-    /**
160
-     * Toggles whether or not new members should join the conference as video
161
-     * muted.
162
-     *
163
-     * @param {Object} event - The dom event returned from changes the checkbox.
164
-     * @private
165
-     * @returns {void}
166
-     */
167
-    _onSetStartVideoMutedPolicy(event) {
168
-        this.props.dispatch(setStartMutedPolicy(
169
-            this.props._startAudioMutedPolicy, event.target.checked));
170
-    }
171
-}
172
-
173
-/**
174
- * Maps (parts of) the Redux state to the associated props for the
175
- * {@code ModeratorCheckboxes} component.
176
- *
177
- * @param {Object} state - The Redux state.
178
- * @private
179
- * @returns {{
180
- *     _followMeEnabled: boolean,
181
- *     _startAudioMutedPolicy: boolean,
182
- *     _startVideoMutedPolicy: boolean
183
- * }}
184
- */
185
-function _mapStateToProps(state) {
186
-    const {
187
-        followMeEnabled,
188
-        startAudioMutedPolicy,
189
-        startVideoMutedPolicy
190
-    } = state['features/base/conference'];
191
-
192
-    return {
193
-        _followMeEnabled: Boolean(followMeEnabled),
194
-        _startAudioMutedPolicy: Boolean(startAudioMutedPolicy),
195
-        _startVideoMutedPolicy: Boolean(startVideoMutedPolicy)
196
-    };
197
-}
198
-
199
-export default translate(connect(_mapStateToProps)(ModeratorCheckboxes));

+ 239
- 0
react/features/settings/components/web/MoreTab.js 查看文件

@@ -0,0 +1,239 @@
1
+// @flow
2
+
3
+import { CheckboxGroup, CheckboxStateless } from '@atlaskit/checkbox';
4
+import DropdownMenu, {
5
+    DropdownItem,
6
+    DropdownItemGroup
7
+} from '@atlaskit/dropdown-menu';
8
+import React from 'react';
9
+
10
+import { AbstractDialogTab } from '../../../base/dialog';
11
+import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
12
+import { translate } from '../../../base/i18n';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of {@link MoreTab}.
16
+ */
17
+export type Props = {
18
+    ...$Exact<AbstractDialogTabProps>,
19
+
20
+    /**
21
+     * The currently selected language to display in the language select
22
+     * dropdown.
23
+     */
24
+    currentLanguage: string,
25
+
26
+    /**
27
+     * Whether or not the user has selected the Follow Me feature to be enabled.
28
+     */
29
+    followMeEnabled: boolean,
30
+
31
+    /**
32
+     * All available languages to display in the language select dropdown.
33
+     */
34
+    languages: Array<string>,
35
+
36
+    /**
37
+     * Whether or not to display the language select dropdown.
38
+     */
39
+    showLanguageSettings: boolean,
40
+
41
+    /**
42
+     * Whether or not to display moderator-only settings.
43
+     */
44
+    showModeratorSettings: boolean,
45
+
46
+    /**
47
+     * Whether or not the user has selected the Start Audio Muted feature to be
48
+     * enabled.
49
+     */
50
+    startAudioMuted: boolean,
51
+
52
+    /**
53
+     * Whether or not the user has selected the Start Video Muted feature to be
54
+     * enabled.
55
+     */
56
+    startVideoMuted: boolean,
57
+
58
+    /**
59
+     * Invoked to obtain translated strings.
60
+     */
61
+    t: Function
62
+};
63
+
64
+/**
65
+ * The type of the React {@code Component} state of {@link MoreTab}.
66
+ */
67
+type State = {
68
+
69
+    /**
70
+     * Whether or not the language select dropdown is open.
71
+     */
72
+    isLanguageSelectOpen: boolean
73
+};
74
+
75
+/**
76
+ * React {@code Component} for modifying language and moderator settings.
77
+ *
78
+ * @extends Component
79
+ */
80
+class MoreTab extends AbstractDialogTab<Props, State> {
81
+    /**
82
+     * Initializes a new {@code MoreTab} instance.
83
+     *
84
+     * @param {Object} props - The read-only properties with which the new
85
+     * instance is to be initialized.
86
+     */
87
+    constructor(props: Props) {
88
+        super(props);
89
+
90
+        this.state = {
91
+            isLanguageSelectOpen: false
92
+        };
93
+
94
+        // Bind event handler so it is only bound once for every instance.
95
+        this._onLanguageDropdownOpenChange
96
+            = this._onLanguageDropdownOpenChange.bind(this);
97
+    }
98
+
99
+    /**
100
+     * Implements React's {@link Component#render()}.
101
+     *
102
+     * @inheritdoc
103
+     * @returns {ReactElement}
104
+     */
105
+    render() {
106
+        const { showModeratorSettings, showLanguageSettings } = this.props;
107
+        const content = [];
108
+
109
+        if (showModeratorSettings) {
110
+            content.push(this._renderModeratorSettings());
111
+        }
112
+
113
+        if (showLanguageSettings) {
114
+            content.push(this._renderLangaugeSelect());
115
+        }
116
+
117
+        return <div className = 'more-tab'>{ content }</div>;
118
+    }
119
+
120
+    _onLanguageDropdownOpenChange: (Object) => void;
121
+
122
+    /**
123
+     * Callback invoked to toggle display of the language select dropdown.
124
+     *
125
+     * @param {Object} event - The event for opening or closing the dropdown.
126
+     * @private
127
+     * @returns {void}
128
+     */
129
+    _onLanguageDropdownOpenChange({ isOpen }) {
130
+        this.setState({ isLanguageSelectOpen: isOpen });
131
+    }
132
+
133
+    /**
134
+     * Returns the menu item for changing displayed language.
135
+     *
136
+     * @private
137
+     * @returns {ReactElement}
138
+     */
139
+    _renderLangaugeSelect() {
140
+        const {
141
+            currentLanguage,
142
+            languages,
143
+            t
144
+        } = this.props;
145
+
146
+        const languageItems = languages.map(language =>
147
+            // eslint-disable-next-line react/jsx-wrap-multilines
148
+            <DropdownItem
149
+                key = { language }
150
+                // eslint-disable-next-line react/jsx-no-bind
151
+                onClick = {
152
+                    () => super._onChange({ currentLanguage: language }) }>
153
+                { t(`languages:${language}`) }
154
+            </DropdownItem>
155
+        );
156
+
157
+        return (
158
+            <div
159
+                className = 'settings-sub-pane language-settings'
160
+                key = 'language'>
161
+                <div className = 'mock-atlaskit-label'>
162
+                    { t('settings.language') }
163
+                </div>
164
+                <DropdownMenu
165
+                    isOpen = { this.state.isLanguageSelectOpen }
166
+                    onOpenChange = { this._onLanguageDropdownOpenChange }
167
+                    shouldFitContainer = { true }
168
+                    trigger = { currentLanguage
169
+                        ? t(`languages:${currentLanguage}`)
170
+                        : '' }
171
+                    triggerButtonProps = {{
172
+                        appearance: 'primary',
173
+                        shouldFitContainer: true
174
+                    }}
175
+                    triggerType = 'button'>
176
+                    <DropdownItemGroup>
177
+                        { languageItems }
178
+                    </DropdownItemGroup>
179
+                </DropdownMenu>
180
+            </div>
181
+        );
182
+    }
183
+
184
+    /**
185
+     * Returns the React Element for modifying conference-wide settings.
186
+     *
187
+     * @private
188
+     * @returns {ReactElement}
189
+     */
190
+    _renderModeratorSettings() {
191
+        const {
192
+            followMeEnabled,
193
+            startAudioMuted,
194
+            startVideoMuted,
195
+            t
196
+        } = this.props;
197
+
198
+        return (
199
+            <div
200
+                className = 'settings-sub-pane'
201
+                key = 'moderator'>
202
+                <div className = 'mock-atlaskit-label'>
203
+                    { t('settings.moderator') }
204
+                </div>
205
+                <CheckboxGroup>
206
+                    <CheckboxStateless
207
+                        isChecked = { startAudioMuted }
208
+                        label = { t('settings.startAudioMuted') }
209
+                        name = 'start-audio-muted'
210
+                        // eslint-disable-next-line react/jsx-no-bind
211
+                        onChange = {
212
+                            ({ target: { checked } }) =>
213
+                                super._onChange({ startAudioMuted: checked })
214
+                        } />
215
+                    <CheckboxStateless
216
+                        isChecked = { startVideoMuted }
217
+                        label = { t('settings.startVideoMuted') }
218
+                        name = 'start-video-muted'
219
+                        // eslint-disable-next-line react/jsx-no-bind
220
+                        onChange = {
221
+                            ({ target: { checked } }) =>
222
+                                super._onChange({ startVideoMuted: checked })
223
+                        } />
224
+                    <CheckboxStateless
225
+                        isChecked = { followMeEnabled }
226
+                        label = { t('settings.followMe') }
227
+                        name = 'follow-me'
228
+                        // eslint-disable-next-line react/jsx-no-bind
229
+                        onChange = {
230
+                            ({ target: { checked } }) =>
231
+                                super._onChange({ followMeEnabled: checked })
232
+                        } />
233
+                </CheckboxGroup>
234
+            </div>
235
+        );
236
+    }
237
+}
238
+
239
+export default translate(MoreTab);

+ 188
- 0
react/features/settings/components/web/ProfileTab.js 查看文件

@@ -0,0 +1,188 @@
1
+// @flow
2
+
3
+import Button from '@atlaskit/button';
4
+import { FieldTextStateless } from '@atlaskit/field-text';
5
+import React from 'react';
6
+
7
+import { AbstractDialogTab } from '../../../base/dialog';
8
+import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
9
+import { translate } from '../../../base/i18n';
10
+import UIEvents from '../../../../../service/UI/UIEvents';
11
+import {
12
+    sendAnalytics,
13
+    createProfilePanelButtonEvent
14
+} from '../../../analytics';
15
+
16
+declare var APP: Object;
17
+
18
+/**
19
+ * The type of the React {@code Component} props of {@link ProfileTab}.
20
+ */
21
+export type Props = {
22
+    ...$Exact<AbstractDialogTabProps>,
23
+
24
+    /**
25
+     * Whether or not server-side authentication is available.
26
+     */
27
+    authEnabled: boolean,
28
+
29
+    /**
30
+     * The name of the currently (server-side) authenticated user.
31
+     */
32
+    authLogin: string,
33
+
34
+    /**
35
+     * The display name to display for the local participant.
36
+     */
37
+    displayName: string,
38
+
39
+    /**
40
+     * The email to display for the local participant.
41
+     */
42
+    email: string,
43
+
44
+    /**
45
+     * Invoked to obtain translated strings.
46
+     */
47
+    t: Function
48
+}
49
+
50
+/**
51
+ * React {@code Component} for modifying the local user's profile.
52
+ *
53
+ * @extends Component
54
+ */
55
+class ProfileTab extends AbstractDialogTab<Props> {
56
+    /**
57
+     * Initializes a new {@code ConnectedSettingsDialog} instance.
58
+     *
59
+     * @param {Props} props - The React {@code Component} props to initialize
60
+     * the new {@code ConnectedSettingsDialog} instance with.
61
+     */
62
+    constructor(props: Props) {
63
+        super(props);
64
+
65
+        // Bind event handlers so they are only bound once for every instance.
66
+        this._onAuthToggle = this._onAuthToggle.bind(this);
67
+    }
68
+
69
+    /**
70
+     * Implements React's {@link Component#render()}.
71
+     *
72
+     * @inheritdoc
73
+     * @returns {ReactElement}
74
+     */
75
+    render() {
76
+        const {
77
+            authEnabled,
78
+            displayName,
79
+            email,
80
+            t
81
+        } = this.props;
82
+
83
+        return (
84
+            <div>
85
+                <div className = 'profile-edit'>
86
+                    <div className = 'profile-edit-field'>
87
+                        <FieldTextStateless
88
+                            autoFocus = { true }
89
+                            compact = { true }
90
+                            id = 'setDisplayName'
91
+                            label = { t('profile.setDisplayNameLabel') }
92
+                            // eslint-disable-next-line react/jsx-no-bind
93
+                            onChange = {
94
+                                ({ target: { value } }) =>
95
+                                    super._onChange({ displayName: value })
96
+                            }
97
+                            placeholder = { t('settings.name') }
98
+                            shouldFitContainer = { true }
99
+                            type = 'text'
100
+                            value = { displayName } />
101
+                    </div>
102
+                    <div className = 'profile-edit-field'>
103
+                        <FieldTextStateless
104
+                            compact = { true }
105
+                            id = 'setEmail'
106
+                            label = { t('profile.setEmailLabel') }
107
+                            // eslint-disable-next-line react/jsx-no-bind
108
+                            onChange = {
109
+                                ({ target: { value } }) =>
110
+                                    super._onChange({ email: value })
111
+                            }
112
+                            placeholder = { t('profile.setEmailInput') }
113
+                            shouldFitContainer = { true }
114
+                            type = 'text'
115
+                            value = { email } />
116
+                    </div>
117
+                </div>
118
+                { authEnabled && this._renderAuth() }
119
+            </div>
120
+        );
121
+    }
122
+
123
+    _onAuthToggle: () => void;
124
+
125
+    /**
126
+     * Shows the dialog for logging in or out of a server and closes this
127
+     * dialog.
128
+     *
129
+     * @private
130
+     * @returns {void}
131
+     */
132
+    _onAuthToggle() {
133
+        if (this.props.authLogin) {
134
+            sendAnalytics(createProfilePanelButtonEvent('logout.button'));
135
+
136
+            APP.UI.messageHandler.openTwoButtonDialog({
137
+                leftButtonKey: 'dialog.Yes',
138
+                msgKey: 'dialog.logoutQuestion',
139
+                submitFunction(evt, yes) {
140
+                    if (yes) {
141
+                        APP.UI.emitEvent(UIEvents.LOGOUT);
142
+                    }
143
+                },
144
+                titleKey: 'dialog.logoutTitle'
145
+            });
146
+        } else {
147
+            sendAnalytics(createProfilePanelButtonEvent('login.button'));
148
+
149
+            APP.UI.emitEvent(UIEvents.AUTH_CLICKED);
150
+        }
151
+
152
+        this.props.closeDialog();
153
+    }
154
+
155
+    /**
156
+     * Returns a React Element for interacting with server-side authentication.
157
+     *
158
+     * @private
159
+     * @returns {ReactElement}
160
+     */
161
+    _renderAuth() {
162
+        const {
163
+            authLogin,
164
+            t
165
+        } = this.props;
166
+
167
+        return (
168
+            <div>
169
+                <div className = 'mock-atlaskit-label'>
170
+                    { t('toolbar.authenticate') }
171
+                </div>
172
+                { authLogin
173
+                    && <div className = 'auth-name'>
174
+                        { t('settings.loggedIn', { name: authLogin }) }
175
+                    </div> }
176
+                <Button
177
+                    appearance = 'primary'
178
+                    id = 'login_button'
179
+                    onClick = { this._onAuthToggle }
180
+                    type = 'button'>
181
+                    { authLogin ? t('toolbar.logout') : t('toolbar.login') }
182
+                </Button>
183
+            </div>
184
+        );
185
+    }
186
+}
187
+
188
+export default translate(ProfileTab);

+ 9
- 14
react/features/settings/components/web/SettingsButton.js 查看文件

@@ -6,8 +6,10 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
6 6
 import { translate } from '../../../base/i18n';
7 7
 import { AbstractButton } from '../../../base/toolbox';
8 8
 import type { AbstractButtonProps } from '../../../base/toolbox';
9
-import { openDeviceSelectionDialog } from '../../../device-selection';
10
-import { toggleSettings } from '../../../side-panel';
9
+import { openDeviceSelectionPopup } from '../../../device-selection';
10
+
11
+import { openSettingsDialog } from '../../actions';
12
+import { SETTINGS_TABS } from '../../constants';
11 13
 
12 14
 declare var interfaceConfig: Object;
13 15
 
@@ -21,11 +23,6 @@ type Props = AbstractButtonProps & {
21 23
      */
22 24
     _filmstripOnly: boolean,
23 25
 
24
-    /**
25
-     * Array containing the enabled settings sections.
26
-     */
27
-    _sections: Array<string>,
28
-
29 26
     /**
30 27
      * The redux {@code dispatch} function.
31 28
      */
@@ -48,14 +45,13 @@ class SettingsButton extends AbstractButton<Props, *> {
48 45
      * @returns {void}
49 46
      */
50 47
     _handleClick() {
51
-        const { _filmstripOnly, _sections, dispatch } = this.props;
48
+        const { _filmstripOnly, dispatch } = this.props;
52 49
 
53 50
         sendAnalytics(createToolbarEvent('settings'));
54
-        if (_filmstripOnly
55
-                || (_sections.length === 1 && _sections.includes('devices'))) {
56
-            dispatch(openDeviceSelectionDialog());
51
+        if (_filmstripOnly) {
52
+            dispatch(openDeviceSelectionPopup());
57 53
         } else {
58
-            dispatch(toggleSettings());
54
+            dispatch(openSettingsDialog(SETTINGS_TABS.DEVICES));
59 55
         }
60 56
     }
61 57
 }
@@ -75,8 +71,7 @@ function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
75 71
     // interfaceConfig is part of redux we will.
76 72
 
77 73
     return {
78
-        _filmstripOnly: Boolean(interfaceConfig.filmStripOnly),
79
-        _sections: interfaceConfig.SETTINGS_SECTIONS || []
74
+        _filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
80 75
     };
81 76
 }
82 77
 

+ 167
- 0
react/features/settings/components/web/SettingsDialog.js 查看文件

@@ -0,0 +1,167 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { DialogWithTabs, hideDialog } from '../../../base/dialog';
7
+import {
8
+    DeviceSelection,
9
+    getDeviceSelectionDialogProps,
10
+    submitDeviceSelectionTab
11
+} from '../../../device-selection';
12
+
13
+import MoreTab from './MoreTab';
14
+import ProfileTab from './ProfileTab';
15
+import { getMoreTabProps, getProfileTabProps } from '../../functions';
16
+import { submitMoreTab, submitProfileTab } from '../../actions';
17
+import { SETTINGS_TABS } from '../../constants';
18
+
19
+declare var APP: Object;
20
+declare var interfaceConfig: Object;
21
+
22
+/**
23
+ * The type of the React {@code Component} props of
24
+ * {@link ConnectedSettingsDialog}.
25
+ */
26
+type Props = {
27
+
28
+    /**
29
+     * Which settings tab should be initially displayed. If not defined then
30
+     * the first tab will be displayed.
31
+     */
32
+    defaultTab: string,
33
+
34
+    /**
35
+     * Information about the tabs to be rendered.
36
+     */
37
+    _tabs: Array<Object>,
38
+
39
+    /**
40
+     * Invoked to save changed settings.
41
+     */
42
+    dispatch: Function,
43
+};
44
+
45
+/**
46
+ * A React {@code Component} for displaying a dialog to modify local settings
47
+ * and conference-wide (moderator) settings. This version is connected to
48
+ * redux to get the current settings.
49
+ *
50
+ * @extends Component
51
+ */
52
+class SettingsDialog extends Component<Props> {
53
+    /**
54
+     * Initializes a new {@code ConnectedSettingsDialog} instance.
55
+     *
56
+     * @param {Props} props - The React {@code Component} props to initialize
57
+     * the new {@code ConnectedSettingsDialog} instance with.
58
+     */
59
+    constructor(props: Props) {
60
+        super(props);
61
+
62
+        // Bind event handlers so they are only bound once for every instance.
63
+        this._closeDialog = this._closeDialog.bind(this);
64
+    }
65
+
66
+    /**
67
+     * Implements React's {@link Component#render()}.
68
+     *
69
+     * @inheritdoc
70
+     * @returns {ReactElement}
71
+     */
72
+    render() {
73
+        const { _tabs, defaultTab, dispatch } = this.props;
74
+        const onSubmit = this._closeDialog;
75
+        const defaultTabIdx
76
+            = _tabs.findIndex(({ name }) => name === defaultTab);
77
+        const tabs = _tabs.map(tab => {
78
+            return {
79
+                ...tab,
80
+                submit: (...args) => dispatch(tab.submit(...args))
81
+            };
82
+        });
83
+
84
+        return (
85
+            <DialogWithTabs
86
+                closeDialog = { this._closeDialog }
87
+                defaultTab = {
88
+                    defaultTabIdx === -1 ? undefined : defaultTabIdx
89
+                }
90
+                onSubmit = { onSubmit }
91
+                tabs = { tabs } />
92
+        );
93
+    }
94
+
95
+    _closeDialog: () => void;
96
+
97
+    /**
98
+     * Callback invoked to close the dialog without saving changes.
99
+     *
100
+     * @private
101
+     * @returns {void}
102
+     */
103
+    _closeDialog() {
104
+        this.props.dispatch(hideDialog());
105
+    }
106
+}
107
+
108
+/**
109
+ * Maps (parts of) the Redux state to the associated props for the
110
+ * {@code ConnectedSettingsDialog} component.
111
+ *
112
+ * @param {Object} state - The Redux state.
113
+ * @private
114
+ * @returns {{
115
+ *     tabs: Array<Object>
116
+ * }}
117
+ */
118
+function _mapStateToProps(state) {
119
+    const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
120
+    const jwt = state['features/base/jwt'];
121
+
122
+    // The settings sections to display.
123
+    const showDeviceSettings = configuredTabs.includes('devices');
124
+    const moreTabProps = getMoreTabProps(state);
125
+    const { showModeratorSettings, showLanguageSettings } = moreTabProps;
126
+    const showProfileSettings
127
+        = configuredTabs.includes('profile') && jwt.isGuest;
128
+
129
+    const tabs = [];
130
+
131
+    if (showDeviceSettings) {
132
+        tabs.push({
133
+            name: SETTINGS_TABS.DEVICES,
134
+            component: DeviceSelection,
135
+            label: 'settings.devices',
136
+            props: getDeviceSelectionDialogProps(state),
137
+            styles: 'settings-pane devices-pane',
138
+            submit: submitDeviceSelectionTab
139
+        });
140
+    }
141
+
142
+    if (showProfileSettings) {
143
+        tabs.push({
144
+            name: SETTINGS_TABS.PROFILE,
145
+            component: ProfileTab,
146
+            label: 'profile.title',
147
+            props: getProfileTabProps(state),
148
+            styles: 'settings-pane profile-pane',
149
+            submit: submitProfileTab
150
+        });
151
+    }
152
+
153
+    if (showModeratorSettings || showLanguageSettings) {
154
+        tabs.push({
155
+            name: SETTINGS_TABS.MORE,
156
+            component: MoreTab,
157
+            label: 'settings.more',
158
+            props: moreTabProps,
159
+            styles: 'settings-pane more-pane',
160
+            submit: submitMoreTab
161
+        });
162
+    }
163
+
164
+    return { _tabs: tabs };
165
+}
166
+
167
+export default connect(_mapStateToProps)(SettingsDialog);

+ 0
- 111
react/features/settings/components/web/SettingsMenu.js 查看文件

@@ -1,111 +0,0 @@
1
-import PropTypes from 'prop-types';
2
-import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4
-
5
-import { translate } from '../../../base/i18n';
6
-import {
7
-    getLocalParticipant,
8
-    PARTICIPANT_ROLE
9
-} from '../../../base/participants';
10
-
11
-import DeviceSelectionButton from './DeviceSelectionButton';
12
-import LanguageSelectDropdown from './LanguageSelectDropdown';
13
-import ModeratorCheckboxes from './ModeratorCheckboxes';
14
-
15
-/**
16
- * Implements a React {@link Component} which various ways to change application
17
- * settings.
18
- *
19
- * @extends Component
20
- */
21
-class SettingsMenu extends Component {
22
-    /**
23
-     * {@code SettingsMenu} component's property types.
24
-     *
25
-     * @static
26
-     */
27
-    static propTypes = {
28
-        /**
29
-         * Whether or not the local user is a moderator.
30
-         */
31
-        _isModerator: PropTypes.bool,
32
-
33
-        /**
34
-         * Whether or not the button to open device selection should display.
35
-         */
36
-        showDeviceSettings: PropTypes.bool,
37
-
38
-        /**
39
-         * Whether or not the dropdown to change the current translated language
40
-         * should display.
41
-         */
42
-        showLanguageSettings: PropTypes.bool,
43
-
44
-        /**
45
-         * Whether or not moderator-only actions that affect the conference
46
-         * should display.
47
-         */
48
-        showModeratorSettings: PropTypes.bool,
49
-
50
-        /**
51
-         * Whether or not menu section should have section titles displayed.
52
-         */
53
-        showTitles: PropTypes.bool,
54
-
55
-        /**
56
-         * Invoked to obtain translated strings.
57
-         */
58
-        t: PropTypes.func
59
-    };
60
-
61
-    /**
62
-     * Implements React's {@link Component#render()}.
63
-     *
64
-     * @inheritdoc
65
-     * @returns {ReactElement}
66
-     */
67
-    render() {
68
-        const {
69
-            _isModerator,
70
-            showDeviceSettings,
71
-            showLanguageSettings,
72
-            showModeratorSettings,
73
-            showTitles,
74
-            t
75
-        } = this.props;
76
-
77
-        return (
78
-            <div className = 'settings-menu'>
79
-                <div className = 'title'>
80
-                    { t('settings.title') }
81
-                </div>
82
-                { showLanguageSettings
83
-                    ? <LanguageSelectDropdown />
84
-                    : null }
85
-                { showDeviceSettings
86
-                    ? <DeviceSelectionButton showTitle = { showTitles } />
87
-                    : null }
88
-                { _isModerator && showModeratorSettings
89
-                    ? <ModeratorCheckboxes showTitle = { showTitles } />
90
-                    : null }
91
-            </div>
92
-        );
93
-    }
94
-}
95
-
96
-/**
97
- * Maps parts of Redux store to component prop types.
98
- *
99
- * @param {Object} state - Snapshot of Redux store.
100
- * @returns {{
101
- *      _isModerator: boolean
102
- * }}
103
- */
104
-function _mapStateToProps(state) {
105
-    return {
106
-        _isModerator:
107
-            getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
108
-    };
109
-}
110
-
111
-export default translate(connect(_mapStateToProps)(SettingsMenu));

+ 1
- 1
react/features/settings/components/web/index.js 查看文件

@@ -1,2 +1,2 @@
1 1
 export { default as SettingsButton } from './SettingsButton';
2
-export { default as SettingsMenu } from './SettingsMenu';
2
+export { default as SettingsDialog } from './SettingsDialog';

+ 5
- 0
react/features/settings/constants.js 查看文件

@@ -0,0 +1,5 @@
1
+export const SETTINGS_TABS = {
2
+    DEVICES: 'devices_tab',
3
+    MORE: 'more_tab',
4
+    PROFILE: 'profile_tab'
5
+};

+ 57
- 1
react/features/settings/functions.js 查看文件

@@ -1,6 +1,8 @@
1 1
 // @flow
2
-
2
+import { toState } from '../base/redux';
3 3
 import { parseStandardURIString } from '../base/util';
4
+import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
5
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
4 6
 
5 7
 declare var interfaceConfig: Object;
6 8
 
@@ -60,3 +62,57 @@ export function shouldShowOnlyDeviceSelection() {
60 62
     return interfaceConfig.SETTINGS_SECTIONS.length === 1
61 63
         && isSettingEnabled('devices');
62 64
 }
65
+
66
+/**
67
+ * Returns the properties for the "More" tab from settings dialog from Redux
68
+ * state.
69
+ *
70
+ * @param {(Function|Object)} stateful -The (whole) redux state, or redux's
71
+ * {@code getState} function to be used to retrieve the state.
72
+ * @returns {Object} - The properties for the "More" tab from settings dialog.
73
+ */
74
+export function getMoreTabProps(stateful: Object | Function) {
75
+    const state = toState(stateful);
76
+    const language = i18next.language || DEFAULT_LANGUAGE;
77
+    const conference = state['features/base/conference'];
78
+    const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
79
+    const localParticipant = getLocalParticipant(state);
80
+
81
+
82
+    // The settings sections to display.
83
+    const showModeratorSettings
84
+        = configuredTabs.includes('moderator')
85
+            && localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
86
+
87
+    return {
88
+        currentLanguage: language,
89
+        followMeEnabled: Boolean(conference.followMeEnabled),
90
+        languages: LANGUAGES,
91
+        showLanguageSettings: configuredTabs.includes('language'),
92
+        showModeratorSettings,
93
+        startAudioMuted: Boolean(conference.startAudioMutedPolicy),
94
+        startVideoMuted: Boolean(conference.startVideoMutedPolicy)
95
+    };
96
+}
97
+
98
+/**
99
+ * Returns the properties for the "Profile" tab from settings dialog from Redux
100
+ * state.
101
+ *
102
+ * @param {(Function|Object)} stateful -The (whole) redux state, or redux's
103
+ * {@code getState} function to be used to retrieve the state.
104
+ * @returns {Object} - The properties for the "Profile" tab from settings
105
+ * dialog.
106
+ */
107
+export function getProfileTabProps(stateful: Object | Function) {
108
+    const state = toState(stateful);
109
+    const conference = state['features/base/conference'];
110
+    const localParticipant = getLocalParticipant(state);
111
+
112
+    return {
113
+        authEnabled: conference.authEnabled,
114
+        authLogin: conference.authLogin,
115
+        displayName: localParticipant.name,
116
+        email: localParticipant.email
117
+    };
118
+}

+ 1
- 0
react/features/settings/index.js 查看文件

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

react/features/device-selection/popup.js → react/features/settings/popup.js 查看文件


+ 0
- 20
react/features/side-panel/actionTypes.js 查看文件

@@ -27,23 +27,3 @@ export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
27 27
  * }
28 28
  */
29 29
 export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
30
-
31
-/**
32
- * The type of the action which signals to toggle the display of profile editing
33
- * in the side panel.
34
- *
35
- * {
36
- *     type: TOGGLE_PROFILE
37
- * }
38
- */
39
-export const TOGGLE_PROFILE = Symbol('TOGGLE_PROFILE');
40
-
41
-/**
42
- * The type of the action which signals to toggle the display of settings in the
43
- * side panel.
44
- *
45
- * {
46
- *     type: TOGGLE_SETTINGS
47
- * }
48
- */
49
-export const TOGGLE_SETTINGS = Symbol('TOGGLE_SETTINGS');

+ 1
- 29
react/features/side-panel/actions.js 查看文件

@@ -1,9 +1,7 @@
1 1
 import {
2 2
     CLOSE_PANEL,
3 3
     SET_VISIBLE_PANEL,
4
-    TOGGLE_CHAT,
5
-    TOGGLE_PROFILE,
6
-    TOGGLE_SETTINGS
4
+    TOGGLE_CHAT
7 5
 } from './actionTypes';
8 6
 
9 7
 /**
@@ -49,29 +47,3 @@ export function toggleChat() {
49 47
         type: TOGGLE_CHAT
50 48
     };
51 49
 }
52
-
53
-/**
54
- * Toggles display of the profile side panel.
55
- *
56
- * @returns {{
57
- *     type: TOGGLE_PROFILE
58
- * }}
59
- */
60
-export function toggleProfile() {
61
-    return {
62
-        type: TOGGLE_PROFILE
63
-    };
64
-}
65
-
66
-/**
67
- * Toggles display of the settings side panel.
68
- *
69
- * @returns {{
70
- *     type: TOGGLE_SETTINGS
71
- * }}
72
- */
73
-export function toggleSettings() {
74
-    return {
75
-        type: TOGGLE_SETTINGS
76
-    };
77
-}

+ 1
- 14
react/features/side-panel/middleware.js 查看文件

@@ -2,12 +2,7 @@
2 2
 
3 3
 import { MiddlewareRegistry } from '../base/redux';
4 4
 
5
-import {
6
-    CLOSE_PANEL,
7
-    TOGGLE_CHAT,
8
-    TOGGLE_PROFILE,
9
-    TOGGLE_SETTINGS
10
-} from './actionTypes';
5
+import { CLOSE_PANEL, TOGGLE_CHAT } from './actionTypes';
11 6
 
12 7
 declare var APP: Object;
13 8
 
@@ -31,14 +26,6 @@ MiddlewareRegistry.register(store => next => action => {
31 26
     case TOGGLE_CHAT:
32 27
         APP.UI.toggleChat();
33 28
         break;
34
-
35
-    case TOGGLE_PROFILE:
36
-        APP.UI.toggleSidePanel('profile_container');
37
-        break;
38
-
39
-    case TOGGLE_SETTINGS:
40
-        APP.UI.toggleSidePanel('settings_container');
41
-        break;
42 29
     }
43 30
 
44 31
     return next(action);

+ 1
- 0
react/features/toolbox/components/web/OverflowMenuProfileItem.js 查看文件

@@ -114,6 +114,7 @@ function _mapStateToProps(state) {
114 114
     return {
115 115
         _localParticipant: getLocalParticipant(state),
116 116
         _unclickable: !state['features/base/jwt'].isGuest
117
+            || !interfaceConfig.SETTINGS_SECTIONS.includes('profile')
117 118
     };
118 119
 }
119 120
 

+ 7
- 3
react/features/toolbox/components/web/Toolbox.js 查看文件

@@ -36,9 +36,13 @@ import {
36 36
     StopRecordingDialog,
37 37
     getActiveSession
38 38
 } from '../../../recording';
39
-import { SettingsButton } from '../../../settings';
39
+import {
40
+    SETTINGS_TABS,
41
+    SettingsButton,
42
+    openSettingsDialog
43
+} from '../../../settings';
40 44
 import { toggleSharedVideo } from '../../../shared-video';
41
-import { toggleChat, toggleProfile } from '../../../side-panel';
45
+import { toggleChat } from '../../../side-panel';
42 46
 import { SpeakerStats } from '../../../speaker-stats';
43 47
 import {
44 48
     OverflowMenuVideoQualityItem,
@@ -515,7 +519,7 @@ class Toolbox extends Component<Props> {
515 519
      * @returns {void}
516 520
      */
517 521
     _doToggleProfile() {
518
-        this.props.dispatch(toggleProfile());
522
+        this.props.dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
519 523
     }
520 524
 
521 525
     /**

+ 0
- 6
service/UI/UIEvents.js 查看文件

@@ -36,12 +36,6 @@ export default {
36 36
      */
37 37
     TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
38 38
     TOGGLE_CHAT: 'UI.toggle_chat',
39
-    TOGGLE_SETTINGS: 'UI.toggle_settings',
40
-
41
-    /**
42
-     * Notifies that the profile toolbar button has been clicked.
43
-     */
44
-    TOGGLE_PROFILE: 'UI.toggle_profile',
45 39
 
46 40
     /**
47 41
      * Notifies that a command to toggle the filmstrip has been issued. The

+ 1
- 1
webpack.config.js 查看文件

@@ -146,7 +146,7 @@ module.exports = [
146 146
             ],
147 147
 
148 148
             'device_selection_popup_bundle':
149
-                './react/features/device-selection/popup.js',
149
+                './react/features/settings/popup.js',
150 150
 
151 151
             'alwaysontop':
152 152
                 './react/features/always-on-top/index.js',

Loading…
取消
儲存