浏览代码

feat: lobby feature

The lobby feature adds the possibility to lock a meeting and only allow people in after virtually knocking and going through formal approval
master
Bettenbuk Zoltan 5 年前
父节点
当前提交
475a2ae596
共有 56 个文件被更改,包括 2399 次插入47 次删除
  1. 0
    14
      conference.js
  2. 211
    0
      css/_lobby.scss
  3. 1
    0
      css/main.scss
  4. 1
    1
      interface_config.js
  5. 27
    0
      lang/main.json
  6. 0
    13
      modules/UI/UI.js
  7. 5
    1
      react/features/app/actions.js
  8. 1
    0
      react/features/app/components/AbstractApp.js
  9. 1
    1
      react/features/base/avatar/components/native/styles.js
  10. 2
    1
      react/features/base/conference/actions.js
  11. 2
    2
      react/features/base/conference/functions.js
  12. 32
    3
      react/features/base/conference/middleware.js
  13. 9
    0
      react/features/base/conference/reducer.js
  14. 2
    2
      react/features/base/connection/actions.native.js
  15. 1
    0
      react/features/base/connection/actions.web.js
  16. 2
    3
      react/features/base/dialog/components/native/BaseDialog.js
  17. 1
    1
      react/features/base/dialog/components/native/BaseSubmitDialog.js
  18. 5
    0
      react/features/base/dialog/components/web/Dialog.js
  19. 6
    1
      react/features/base/dialog/components/web/StatelessDialog.js
  20. 1
    0
      react/features/base/icons/svg/edit.svg
  21. 3
    0
      react/features/base/icons/svg/index.js
  22. 1
    0
      react/features/base/icons/svg/meeting-locked.svg
  23. 1
    0
      react/features/base/icons/svg/meeting-unlocked.svg
  24. 11
    0
      react/features/base/react/functions.js
  25. 2
    0
      react/features/base/react/index.js
  26. 4
    1
      react/features/conference/components/native/Conference.js
  27. 3
    2
      react/features/conference/components/web/Conference.js
  28. 2
    1
      react/features/conference/middleware.js
  29. 21
    0
      react/features/lobby/actionTypes.js
  30. 189
    0
      react/features/lobby/actions.js
  31. 47
    0
      react/features/lobby/components/AbstractDisableLobbyModeDialog.js
  32. 75
    0
      react/features/lobby/components/AbstractEnableLobbyModeDialog.js
  33. 82
    0
      react/features/lobby/components/AbstractKnockingParticipantList.js
  34. 328
    0
      react/features/lobby/components/AbstractLobbyScreen.js
  35. 76
    0
      react/features/lobby/components/LobbyModeButton.js
  36. 5
    0
      react/features/lobby/components/index.native.js
  37. 5
    0
      react/features/lobby/components/index.web.js
  38. 30
    0
      react/features/lobby/components/native/DisableLobbyModeDialog.js
  39. 77
    0
      react/features/lobby/components/native/EnableLobbyModeDialog.js
  40. 78
    0
      react/features/lobby/components/native/KnockingParticipantList.js
  41. 234
    0
      react/features/lobby/components/native/LobbyScreen.js
  42. 6
    0
      react/features/lobby/components/native/index.js
  43. 139
    0
      react/features/lobby/components/native/styles.js
  44. 36
    0
      react/features/lobby/components/web/DisableLobbyModeDialog.js
  45. 51
    0
      react/features/lobby/components/web/EnableLobbyModeDialog.js
  46. 72
    0
      react/features/lobby/components/web/KnockingParticipantList.js
  47. 219
    0
      react/features/lobby/components/web/LobbyScreen.js
  48. 6
    0
      react/features/lobby/components/web/index.js
  49. 39
    0
      react/features/lobby/functions.js
  50. 6
    0
      react/features/lobby/index.js
  51. 5
    0
      react/features/lobby/logger.js
  52. 137
    0
      react/features/lobby/middleware.js
  53. 80
    0
      react/features/lobby/reducer.js
  54. 11
    0
      react/features/overlay/middleware.js
  55. 2
    0
      react/features/toolbox/components/native/OverflowMenu.js
  56. 6
    0
      react/features/toolbox/components/web/Toolbox.js

+ 0
- 14
conference.js 查看文件

@@ -296,12 +296,6 @@ class ConferenceConnector {
296 296
         logger.error('CONFERENCE FAILED:', err, ...params);
297 297
 
298 298
         switch (err) {
299
-        case JitsiConferenceErrors.CONNECTION_ERROR: {
300
-            const [ msg ] = params;
301
-
302
-            APP.UI.notifyConnectionFailed(msg);
303
-            break;
304
-        }
305 299
 
306 300
         case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
307 301
             // let's show some auth not allowed page
@@ -336,14 +330,6 @@ class ConferenceConnector {
336 330
             APP.UI.notifyGracefulShutdown();
337 331
             break;
338 332
 
339
-        case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
340
-            const [ reason ] = params;
341
-
342
-            APP.UI.hideStats();
343
-            APP.UI.notifyConferenceDestroyed(reason);
344
-            break;
345
-        }
346
-
347 333
         // FIXME FOCUS_DISCONNECTED is a confusing event name.
348 334
         // What really happens there is that the library is not ready yet,
349 335
         // because Jicofo is not available, but it is going to give it another

+ 211
- 0
css/_lobby.scss 查看文件

@@ -0,0 +1,211 @@
1
+#lobby-screen {
2
+    align-items: center;
3
+    color: $overflowMenuItemColor;
4
+    display: flex;
5
+    flex-direction: column;
6
+    font-size: 1.2em;
7
+    margin: 48px 36px;
8
+
9
+    span {
10
+        padding: 8px 0;
11
+    }
12
+
13
+    .title {
14
+        color: $defaultColor;
15
+        font-size: 2em;
16
+    }
17
+
18
+    .roomName {
19
+        font-size: 1em;
20
+    }
21
+
22
+    .participantInfo {
23
+        align-items: center;
24
+        align-self: stretch;
25
+        border: 1px solid #B8C7E0;
26
+        border-radius: 4px;
27
+        display: flex;
28
+        flex-direction: column;
29
+        margin: 24px 0;
30
+        padding: 34px 0;
31
+
32
+        &:hover {
33
+            padding-top: 0px;
34
+
35
+            .editButton {
36
+                display: flex;
37
+            }
38
+        }
39
+
40
+        .editButton {
41
+            align-self: stretch;
42
+            display: none;
43
+            justify-content: flex-end;
44
+            padding: 5px;
45
+            position: relative;
46
+
47
+            button {
48
+                background-color: transparent;
49
+                border-width: 0;
50
+                margin: 0;
51
+                padding: 0;
52
+            }
53
+        }
54
+
55
+        .displayName {
56
+            color: $defaultColor;
57
+            font-size: 1.3em;
58
+        }
59
+    }
60
+
61
+    .form {
62
+        align-self: stretch;
63
+        display: flex;
64
+        flex-direction: column;
65
+        margin: 32px 0;
66
+
67
+        input {
68
+            margin: 5px 0 15px 0;
69
+        }
70
+
71
+        span {
72
+            color: white;
73
+            font-size: 1.3em;
74
+            text-align: center;
75
+        }
76
+    }
77
+
78
+    .joiningContainer {
79
+        align-items: center;
80
+        display: flex;
81
+        flex-direction: column;
82
+        margin: 36px 0;
83
+
84
+        span {
85
+            margin-top: 36px;
86
+            text-align: center;
87
+        }
88
+    }
89
+}
90
+
91
+#lobby-dialog {
92
+    align-self: stretch;
93
+    display: flex;
94
+    flex-direction: column;
95
+    margin: 32px 0;
96
+
97
+    .description {
98
+        margin-bottom: 18px;
99
+    }
100
+
101
+    .field {
102
+        display: flex;
103
+        flex-direction: row;
104
+
105
+        :first-child {
106
+            align-items: center;
107
+            display: flex;
108
+            padding-right: 15px;
109
+        }
110
+
111
+        :last-child {
112
+            flex: 1;
113
+        }
114
+    }
115
+}
116
+
117
+#knocking-participant-list {
118
+    background-color: $newToolbarBackgroundColor;
119
+    border: 1px solid rgba(255, 255, 255, .4);
120
+    border-radius: 8px;
121
+    display: flex;
122
+    flex-direction: column;
123
+    left: 0;
124
+    margin: 20px;
125
+    position: fixed;
126
+    top: 20;
127
+    transition: top 1s ease;
128
+    z-index: 100;
129
+
130
+    &.toolbox-visible {
131
+        // Same as toolbox subject position
132
+        top: 120px;
133
+    }
134
+
135
+    .title {
136
+        background-color: rgba(0, 0, 0, .2);
137
+        font-size: 1.2em;
138
+        padding: 15px
139
+    }
140
+
141
+    ul {
142
+        list-style-type: none;
143
+        padding: 0 15px 15px 15px;
144
+
145
+        li {
146
+            align-items: center;
147
+            display: flex;
148
+            flex-direction: row;
149
+            margin: 8px 0;
150
+
151
+            .details {
152
+                display: flex;
153
+                flex: 1;
154
+                flex-direction: column;
155
+                justify-content: space-evenly;
156
+                margin: 0 30px 0 10px;
157
+            }
158
+
159
+            button {
160
+                align-self: unset;
161
+                margin: 0 5px;
162
+            }
163
+        }
164
+    }
165
+}
166
+
167
+// Common styles
168
+
169
+#lobby-dialog, #lobby-screen, #knocking-participant-list {
170
+    input {
171
+        align-self: stretch;
172
+        background-color: transparent;
173
+        border: 1px solid #B8C7E0;
174
+        border-radius: 4px;
175
+        color: white;
176
+        padding: 12px 8px;
177
+
178
+        &:focus {
179
+            border-color: rgb(3, 118, 218);
180
+        }
181
+    }
182
+
183
+    button {
184
+        align-self: stretch;
185
+        margin: 8px 0;
186
+        padding: 12px;
187
+        transition: .2s transform ease;
188
+
189
+        &:disabled {
190
+            opacity: .5;
191
+        }
192
+
193
+        &:hover {
194
+            transform: scale(1.05);
195
+
196
+            &:disabled {
197
+                transform: none;
198
+            }
199
+        }
200
+
201
+        &.borderLess {
202
+            background-color: transparent;
203
+            border-width: 0;
204
+        }
205
+
206
+        &.primary {
207
+            background-color: rgb(3, 118, 218);
208
+            border-width: 0;
209
+        }
210
+    }
211
+}

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

@@ -76,6 +76,7 @@ $flagsImagePath: "../images/";
76 76
 @import 'filmstrip/vertical_filmstrip';
77 77
 @import 'filmstrip/vertical_filmstrip_overrides';
78 78
 @import 'labels';
79
+@import 'lobby';
79 80
 @import 'unsupported-browser/main';
80 81
 @import 'modals/invite/add-people';
81 82
 @import 'deep-linking/main';

+ 1
- 1
interface_config.js 查看文件

@@ -48,7 +48,7 @@ var interfaceConfig = {
48 48
      */
49 49
     TOOLBAR_BUTTONS: [
50 50
         'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
51
-        'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
51
+        'fodeviceselection', 'hangup', 'lobby', 'profile', 'chat', 'recording',
52 52
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
53 53
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
54 54
         'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',

+ 27
- 0
lang/main.json 查看文件

@@ -675,6 +675,7 @@
675 675
             "help": "Help",
676 676
             "invite": "Invite people",
677 677
             "kick": "Kick participant",
678
+            "lobbyButton": "Enable/disable lobby mode",
678 679
             "localRecording": "Toggle local recording controls",
679 680
             "lockRoom": "Toggle meeting password",
680 681
             "moreActions": "Toggle more actions menu",
@@ -722,6 +723,8 @@
722 723
         "hangup": "Leave",
723 724
         "help": "Help",
724 725
         "invite": "Invite people",
726
+        "lobbyButtonDisable": "Disable lobby mode",
727
+        "lobbyButtonEnable": "Enable lobby mode",
725 728
         "login": "Login",
726 729
         "logout": "Logout",
727 730
         "lowerYourHand": "Lower your hand",
@@ -861,5 +864,29 @@
861 864
     },
862 865
     "helpView": {
863 866
         "header": "Help center"
867
+    },
868
+    "lobby": {
869
+        "allow": "Allow",
870
+        "backToKnockModeButton": "No password, knock instead",
871
+        "dialogTitle": "Lobby mode",
872
+        "disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
873
+        "disableDialogSubmit": "Disable",
874
+        "emailField": "Enter your email address",
875
+        "enableDialogPasswordField": "Set password (optional)",
876
+        "enableDialogSubmit": "Enable",
877
+        "enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approve of a moderator or by entering an optional predefined password.",
878
+        "enterPasswordButton": "Enter meeting password",
879
+        "joiningMessage": "You'll join the meeting as soon as someone accepts your request",
880
+        "joinWithPasswordMessage": "Trying to join with password, please wait...",
881
+        "joinRejectedMessage": "Your join request was rejected by a moderator.",
882
+        "joinTitle": "Join Meeting",
883
+        "joiningTitle": "Asking to join",
884
+        "joiningWithPasswordTitle": "Joining",
885
+        "knockButton": "Ask to Join",
886
+        "knockTitle": "Someone wants to join the meeting",
887
+        "nameField": "Enter your name",
888
+        "passwordField": "Enter password",
889
+        "passwordJoinButton": "Join",
890
+        "reject": "Reject"
864 891
     }
865 892
 }

+ 0
- 13
modules/UI/UI.js 查看文件

@@ -98,19 +98,6 @@ UI.notifyReservationError = function(code, msg) {
98 98
     });
99 99
 };
100 100
 
101
-/**
102
- * Notify user that conference was destroyed.
103
- * @param reason {string} the reason text
104
- */
105
-UI.notifyConferenceDestroyed = function(reason) {
106
-    // FIXME: use Session Terminated from translation, but
107
-    // 'reason' text comes from XMPP packet and is not translated
108
-    messageHandler.showError({
109
-        description: reason,
110
-        titleKey: 'dialog.sessTerminated'
111
-    });
112
-};
113
-
114 101
 /**
115 102
  * Change nickname for the user.
116 103
  * @param {string} id user id

+ 5
- 1
react/features/app/actions.js 查看文件

@@ -23,7 +23,7 @@ import {
23 23
     parseURIString,
24 24
     toURLString
25 25
 } from '../base/util';
26
-import { showNotification } from '../notifications';
26
+import { clearNotifications, showNotification } from '../notifications';
27 27
 import { setFatalError } from '../overlay';
28 28
 
29 29
 import {
@@ -79,6 +79,10 @@ export function appNavigate(uri: ?string) {
79 79
             dispatch(disconnect());
80 80
         }
81 81
 
82
+        // There are notifications now that gets displayed after we technically left
83
+        // the conference, but we're still on the conference screen.
84
+        dispatch(clearNotifications());
85
+
82 86
         dispatch(configWillLoad(locationURL, room));
83 87
 
84 88
         let protocol = location.protocol.toLowerCase();

+ 1
- 0
react/features/app/components/AbstractApp.js 查看文件

@@ -7,6 +7,7 @@ import '../../base/lastn'; // Register lastN middleware
7 7
 import { toURLString } from '../../base/util';
8 8
 import '../../follow-me';
9 9
 import { OverlayContainer } from '../../overlay';
10
+import '../../lobby'; // Import lobby function
10 11
 import '../../rejoin'; // Enable rejoin analytics
11 12
 import { appNavigate } from '../actions';
12 13
 import { getDefaultURL } from '../functions';

+ 1
- 1
react/features/base/avatar/components/native/styles.js 查看文件

@@ -70,7 +70,7 @@ export default {
70 70
 
71 71
     initialsText: (size: number = DEFAULT_SIZE) => {
72 72
         return {
73
-            color: 'rgba(255, 255, 255, 0.6)',
73
+            color: 'white',
74 74
             fontSize: size * 0.45,
75 75
             fontWeight: '100'
76 76
         };

+ 2
- 1
react/features/base/conference/actions.js 查看文件

@@ -256,7 +256,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
256 256
  * }}
257 257
  * @public
258 258
  */
259
-export function conferenceFailed(conference: Object, error: string) {
259
+export function conferenceFailed(conference: Object, error: string, ...params: any) {
260 260
     return {
261 261
         type: CONFERENCE_FAILED,
262 262
         conference,
@@ -265,6 +265,7 @@ export function conferenceFailed(conference: Object, error: string) {
265 265
         // jitsi-meet needs it).
266 266
         error: {
267 267
             name: error,
268
+            params,
268 269
             recoverable: undefined
269 270
         }
270 271
     };

+ 2
- 2
react/features/base/conference/functions.js 查看文件

@@ -203,7 +203,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
203 203
  * @returns {JitsiConference|undefined}
204 204
  */
205 205
 export function getCurrentConference(stateful: Function | Object) {
206
-    const { conference, joining, leaving, passwordRequired }
206
+    const { conference, joining, leaving, membersOnly, passwordRequired }
207 207
         = toState(stateful)['features/base/conference'];
208 208
 
209 209
     // There is a precendence
@@ -211,7 +211,7 @@ export function getCurrentConference(stateful: Function | Object) {
211 211
         return conference === leaving ? undefined : conference;
212 212
     }
213 213
 
214
-    return joining || passwordRequired;
214
+    return joining || passwordRequired || membersOnly;
215 215
 }
216 216
 
217 217
 /**

+ 32
- 3
react/features/base/conference/middleware.js 查看文件

@@ -8,7 +8,8 @@ import {
8 8
     sendAnalytics
9 9
 } from '../../analytics';
10 10
 import { openDisplayNamePrompt } from '../../display-name';
11
-import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
11
+import { showErrorNotification } from '../../notifications';
12
+import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
12 13
 import { JitsiConferenceErrors } from '../lib-jitsi-meet';
13 14
 import { MEDIA_TYPE } from '../media';
14 15
 import {
@@ -140,15 +141,43 @@ StateListenerRegistry.register(
140 141
  * @private
141 142
  * @returns {Object} The value returned by {@code next(action)}.
142 143
  */
143
-function _conferenceFailed(store, next, action) {
144
+function _conferenceFailed({ dispatch, getState }, next, action) {
144 145
     const result = next(action);
145
-
146 146
     const { conference, error } = action;
147 147
 
148 148
     if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
149 149
         sendAnalytics(createOfferAnswerFailedEvent());
150 150
     }
151 151
 
152
+    // Handle specific failure reasons.
153
+    switch (error.name) {
154
+    case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
155
+        const [ reason ] = error.params;
156
+
157
+        dispatch(showErrorNotification({
158
+            description: reason,
159
+            titleKey: 'dialog.sessTerminated'
160
+        }));
161
+
162
+        if (typeof APP !== 'undefined') {
163
+            APP.UI.hideStats();
164
+        }
165
+        break;
166
+    }
167
+    case JitsiConferenceErrors.CONNECTION_ERROR: {
168
+        const [ msg ] = error.params;
169
+
170
+        dispatch(connectionDisconnected(getState()['features/base/connection'].connection, 'Disconnected'));
171
+        dispatch(showErrorNotification({
172
+            descriptionArguments: { msg },
173
+            descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
174
+            titleKey: 'connection.CONNFAIL'
175
+        }));
176
+
177
+        break;
178
+    }
179
+    }
180
+
152 181
     // FIXME: Workaround for the web version. Currently, the creation of the
153 182
     // conference is handled by /conference.js and appropriate failure handlers
154 183
     // are set there.

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

@@ -36,6 +36,7 @@ const DEFAULT_STATE = {
36 36
     leaving: undefined,
37 37
     locked: undefined,
38 38
     maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
39
+    membersOnly: undefined,
39 40
     password: undefined,
40 41
     passwordRequired: undefined,
41 42
     preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
@@ -161,6 +162,7 @@ function _conferenceFailed(state, { conference, error }) {
161 162
     }
162 163
 
163 164
     let authRequired;
165
+    let membersOnly;
164 166
     let passwordRequired;
165 167
 
166 168
     switch (error.name) {
@@ -168,6 +170,11 @@ function _conferenceFailed(state, { conference, error }) {
168 170
         authRequired = conference;
169 171
         break;
170 172
 
173
+    case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
174
+    case JitsiConferenceErrors.MEMBERS_ONLY_ERROR:
175
+        membersOnly = conference;
176
+        break;
177
+
171 178
     case JitsiConferenceErrors.PASSWORD_REQUIRED:
172 179
         passwordRequired = conference;
173 180
         break;
@@ -189,6 +196,7 @@ function _conferenceFailed(state, { conference, error }) {
189 196
          * @type {string}
190 197
          */
191 198
         locked: passwordRequired ? LOCKED_REMOTELY : undefined,
199
+        membersOnly,
192 200
         password: undefined,
193 201
 
194 202
         /**
@@ -232,6 +240,7 @@ function _conferenceJoined(state, { conference }) {
232 240
         e2eeSupported: conference.isE2EESupported(),
233 241
 
234 242
         joining: undefined,
243
+        membersOnly: undefined,
235 244
         leaving: undefined,
236 245
 
237 246
         /**

+ 2
- 2
react/features/base/connection/actions.native.js 查看文件

@@ -119,7 +119,7 @@ export function connect(id: ?string, password: ?string) {
119 119
          */
120 120
         function _onConnectionDisconnected(message: string) {
121 121
             unsubscribe();
122
-            dispatch(_connectionDisconnected(connection, message));
122
+            dispatch(connectionDisconnected(connection, message));
123 123
         }
124 124
 
125 125
         /**
@@ -195,7 +195,7 @@ export function connect(id: ?string, password: ?string) {
195 195
  *     message: string
196 196
  * }}
197 197
  */
198
-function _connectionDisconnected(connection: Object, message: string) {
198
+export function connectionDisconnected(connection: Object, message: string) {
199 199
     return {
200 200
         type: CONNECTION_DISCONNECTED,
201 201
         connection,

+ 1
- 0
react/features/base/connection/actions.web.js 查看文件

@@ -9,6 +9,7 @@ import { configureInitialDevices } from '../devices';
9 9
 import { getBackendSafeRoomName } from '../util';
10 10
 
11 11
 export {
12
+    connectionDisconnected,
12 13
     connectionEstablished,
13 14
     connectionFailed,
14 15
     setLocationURL

+ 2
- 3
react/features/base/dialog/components/native/BaseDialog.js 查看文件

@@ -57,14 +57,13 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
57 57
                 <KeyboardAvoidingView
58 58
                     behavior = 'height'
59 59
                     style = { [
60
-                        styles.overlay,
61
-                        style
60
+                        styles.overlay
62 61
                     ] }>
63 62
                     <View
64 63
                         pointerEvents = 'box-none'
65 64
                         style = { [
66 65
                             _dialogStyles.dialog,
67
-                            this.props.style
66
+                            style
68 67
                         ] }>
69 68
                         <TouchableOpacity
70 69
                             onPress = { this._onCancel }

+ 1
- 1
react/features/base/dialog/components/native/BaseSubmitDialog.js 查看文件

@@ -34,7 +34,7 @@ class BaseSubmitDialog<P: Props, S: *> extends BaseDialog<P, S> {
34 34
      * @returns {string}
35 35
      */
36 36
     _getSubmitButtonKey() {
37
-        return 'dialog.Ok';
37
+        return this.props.okKey || 'dialog.Ok';
38 38
     }
39 39
 
40 40
     /**

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

@@ -13,6 +13,11 @@ import StatelessDialog from './StatelessDialog';
13 13
  */
14 14
 type Props = AbstractDialogProps & {
15 15
 
16
+    /**
17
+     * True if listening for the Enter key should be disabled.
18
+     */
19
+    disableEnter: boolean,
20
+
16 21
     /**
17 22
      * Whether the dialog is modal. This means clicking on the blanket will
18 23
      * leave the dialog open. No cancel button.

+ 6
- 1
react/features/base/dialog/components/web/StatelessDialog.js 查看文件

@@ -33,6 +33,11 @@ type Props = {
33 33
      */
34 34
     customHeader?: React$Element<any> | Function,
35 35
 
36
+    /*
37
+     * True if listening for the Enter key should be disabled.
38
+     */
39
+    disableEnter: boolean,
40
+
36 41
     /**
37 42
      * Disables dismissing the dialog when the blanket is clicked. Enabled
38 43
      * by default.
@@ -313,7 +318,7 @@ class StatelessDialog extends Component<Props> {
313 318
             return;
314 319
         }
315 320
 
316
-        if (event.key === 'Enter') {
321
+        if (event.key === 'Enter' && !this.props.disableEnter) {
317 322
             event.preventDefault();
318 323
             event.stopPropagation();
319 324
 

+ 1
- 0
react/features/base/icons/svg/edit.svg 查看文件

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

+ 3
- 0
react/features/base/icons/svg/index.js 查看文件

@@ -32,6 +32,7 @@ export { default as IconDownload } from './download.svg';
32 32
 export { default as IconDragHandle } from './drag-handle.svg';
33 33
 export { default as IconE2EE } from './e2ee.svg';
34 34
 export { default as IconEmail } from './envelope.svg';
35
+export { default as IconEdit } from './edit.svg';
35 36
 export { default as IconEventNote } from './event_note.svg';
36 37
 export { default as IconExclamation } from './exclamation.svg';
37 38
 export { default as IconExclamationSolid } from './exclamation-solid.svg';
@@ -46,6 +47,8 @@ export { default as IconInviteMore } from './user-plus.svg';
46 47
 export { default as IconKick } from './kick.svg';
47 48
 export { default as IconLiveStreaming } from './public.svg';
48 49
 export { default as IconLockPassword } from './lock.svg';
50
+export { default as IconMeetingLocked } from './meeting-locked.svg';
51
+export { default as IconMeetingUnlocked } from './meeting-unlocked.svg';
49 52
 export { default as IconMenu } from './menu.svg';
50 53
 export { default as IconMenuDown } from './menu-down.svg';
51 54
 export { default as IconMenuThumb } from './thumb-menu.svg';

+ 1
- 0
react/features/base/icons/svg/meeting-locked.svg 查看文件

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M11 11h-1v2h2v-1l9.73 9.73L20.46 23 14 16.54V21H3v-2h2V7.54l-4-4 1.27-1.27L11 11zm3 .49L5.51 3H14v1h5v12.49l-2-2V6h-3v5.49z"/></svg>

+ 1
- 0
react/features/base/icons/svg/meeting-unlocked.svg 查看文件

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M14 6v15H3v-2h2V3h9v1h5v15h2v2h-4V6h-3zm-4 5v2h2v-2h-2z"/></svg>

+ 11
- 0
react/features/base/react/functions.js 查看文件

@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+/**
4
+ * Returns the field value in a platform generic way.
5
+ *
6
+ * @param {Object | string} fieldParameter - The parameter passed through the change event function.
7
+ * @returns {string}
8
+ */
9
+export function getFieldValue(fieldParameter: Object | string) {
10
+    return typeof fieldParameter === 'string' ? fieldParameter : fieldParameter?.target?.value;
11
+}

+ 2
- 0
react/features/base/react/index.js 查看文件

@@ -1,3 +1,5 @@
1 1
 export * from './components';
2
+export * from './functions';
3
+
2 4
 export { default as Platform } from './Platform';
3 5
 export * from './Types';

+ 4
- 1
react/features/conference/components/native/Conference.js 查看文件

@@ -22,6 +22,7 @@ import {
22 22
 } from '../../../filmstrip';
23 23
 import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
24 24
 import { LargeVideo } from '../../../large-video';
25
+import { KnockingParticipantList } from '../../../lobby';
25 26
 import { BackButtonRegistry } from '../../../mobile/back-button';
26 27
 import { Captions } from '../../../subtitles';
27 28
 import { isToolboxVisible, setToolboxVisible, Toolbox } from '../../../toolbox';
@@ -320,6 +321,7 @@ class Conference extends AbstractConference<Props, *> {
320 321
                     style = { styles.navBarSafeView }>
321 322
                     <NavigationBar />
322 323
                     { this._renderNotificationsContainer() }
324
+                    <KnockingParticipantList />
323 325
                 </SafeAreaView>
324 326
 
325 327
                 <TestConnectionInfo />
@@ -414,6 +416,7 @@ function _mapStateToProps(state) {
414 416
     const {
415 417
         conference,
416 418
         joining,
419
+        membersOnly,
417 420
         leaving
418 421
     } = state['features/base/conference'];
419 422
     const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
@@ -428,7 +431,7 @@ function _mapStateToProps(state) {
428 431
     // - the XMPP connection is connected and we have no conference yet, nor we
429 432
     //   are leaving one.
430 433
     const connecting_
431
-        = connecting || (connection && (joining || (!conference && !leaving)));
434
+        = connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))));
432 435
 
433 436
     return {
434 437
         ...abstractMapStateToProps(state),

+ 3
- 2
react/features/conference/components/web/Conference.js 查看文件

@@ -12,6 +12,7 @@ import { Chat } from '../../../chat';
12 12
 import { Filmstrip } from '../../../filmstrip';
13 13
 import { CalleeInfoContainer } from '../../../invite';
14 14
 import { LargeVideo } from '../../../large-video';
15
+import { KnockingParticipantList } from '../../../lobby';
15 16
 import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
16 17
 import {
17 18
     Toolbox,
@@ -198,8 +199,8 @@ class Conference extends AbstractConference<Props, *> {
198 199
                 <InviteMore />
199 200
                 <div id = 'videospace'>
200 201
                     <LargeVideo />
201
-                    { hideLabels
202
-                        || <Labels /> }
202
+                    <KnockingParticipantList />
203
+                    { hideLabels || <Labels /> }
203 204
                     <Filmstrip filmstripOnly = { filmstripOnly } />
204 205
                 </div>
205 206
 

+ 2
- 1
react/features/conference/middleware.js 查看文件

@@ -66,7 +66,7 @@ MiddlewareRegistry.register(store => next => action => {
66 66
 StateListenerRegistry.register(
67 67
     state => getCurrentConference(state),
68 68
     (conference, { dispatch, getState }, prevConference) => {
69
-        const { authRequired, passwordRequired }
69
+        const { authRequired, membersOnly, passwordRequired }
70 70
             = getState()['features/base/conference'];
71 71
 
72 72
         if (conference !== prevConference) {
@@ -80,6 +80,7 @@ StateListenerRegistry.register(
80 80
             // and explicitly check.
81 81
             if (typeof authRequired === 'undefined'
82 82
                     && typeof passwordRequired === 'undefined'
83
+                    && typeof membersOnly === 'undefined'
83 84
                     && !isDialogOpen(getState(), FeedbackDialog)) {
84 85
                 // Conference changed, left or failed... and there is no
85 86
                 // pending authentication, nor feedback request, so close any

+ 21
- 0
react/features/lobby/actionTypes.js 查看文件

@@ -0,0 +1,21 @@
1
+// @flow
2
+
3
+/**
4
+ * Action type to signal the arriving or updating of a knocking participant.
5
+ */
6
+export const KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED = 'KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED';
7
+
8
+/**
9
+ * Action type to signal the leave of a knocking participant.
10
+ */
11
+export const KNOCKING_PARTICIPANT_LEFT = 'KNOCKING_PARTICIPANT_LEFT';
12
+
13
+/**
14
+ * Action type to set the new state of the lobby mode.
15
+ */
16
+export const SET_LOBBY_MODE_ENABLED = 'SET_LOBBY_MODE_ENABLED';
17
+
18
+/**
19
+ * Action type to set the knocking state of the participant.
20
+ */
21
+export const SET_KNOCKING_STATE = 'SET_KNOCKING_STATE';

+ 189
- 0
react/features/lobby/actions.js 查看文件

@@ -0,0 +1,189 @@
1
+// @flow
2
+
3
+import { type Dispatch } from 'redux';
4
+
5
+import { appNavigate, maybeRedirectToWelcomePage } from '../app';
6
+import { conferenceLeft, conferenceWillJoin, getCurrentConference } from '../base/conference';
7
+import { openDialog } from '../base/dialog';
8
+import { getLocalParticipant } from '../base/participants';
9
+
10
+import {
11
+    KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
12
+    KNOCKING_PARTICIPANT_LEFT,
13
+    SET_KNOCKING_STATE,
14
+    SET_LOBBY_MODE_ENABLED
15
+} from './actionTypes';
16
+import { DisableLobbyModeDialog, EnableLobbyModeDialog, LobbyScreen } from './components';
17
+
18
+declare var APP: Object;
19
+
20
+/**
21
+ * Cancels the ongoing knocking and abandones the join flow.
22
+ *
23
+ * @returns {Function}
24
+ */
25
+export function cancelKnocking() {
26
+    return async (dispatch: Dispatch<any>, getState: Function) => {
27
+        if (typeof APP !== 'undefined') {
28
+            // when we are redirecting the library should handle any
29
+            // unload and clean of the connection.
30
+            APP.API.notifyReadyToClose();
31
+            dispatch(maybeRedirectToWelcomePage());
32
+
33
+            return;
34
+        }
35
+
36
+        dispatch(conferenceLeft(getCurrentConference(getState)));
37
+        dispatch(appNavigate(undefined));
38
+    };
39
+}
40
+
41
+/**
42
+ * Action to be dispatched when a knocking poarticipant leaves before any response.
43
+ *
44
+ * @param {string} id - The ID of the participant.
45
+ * @returns {{
46
+ *     id: string,
47
+ *     type: KNOCKING_PARTICIPANT_LEFT
48
+ * }}
49
+ */
50
+export function knockingParticipantLeft(id: string) {
51
+    return {
52
+        id,
53
+        type: KNOCKING_PARTICIPANT_LEFT
54
+    };
55
+}
56
+
57
+/**
58
+ * Action to set the knocking state of the participant.
59
+ *
60
+ * @param {boolean} knocking - The new state.
61
+ * @returns {{
62
+ *     state: boolean,
63
+ *     type: SET_KNOCKING_STATE
64
+ * }}
65
+ */
66
+export function setKnockingState(knocking: boolean) {
67
+    return {
68
+        knocking,
69
+        type: SET_KNOCKING_STATE
70
+    };
71
+}
72
+
73
+/**
74
+ * Starts knocking and waiting for approval.
75
+ *
76
+ * @param {string} password - The password to bypass knocking, if any.
77
+ * @returns {Function}
78
+ */
79
+export function startKnocking(password?: string) {
80
+    return async (dispatch: Dispatch<any>, getState: Function) => {
81
+        const state = getState();
82
+        const { membersOnly } = state['features/base/conference'];
83
+        const localParticipant = getLocalParticipant(state);
84
+
85
+        dispatch(setKnockingState(true));
86
+        dispatch(conferenceWillJoin(membersOnly));
87
+        membersOnly
88
+            && membersOnly.joinLobby(localParticipant.name, localParticipant.email, password ? password : undefined);
89
+    };
90
+}
91
+
92
+/**
93
+ * Action to open the lobby screen.
94
+ *
95
+ * @returns {openDialog}
96
+ */
97
+export function openLobbyScreen() {
98
+    return openDialog(LobbyScreen);
99
+}
100
+
101
+/**
102
+ * Action to be executed when a participant starts knocking or an already knocking participant gets updated.
103
+ *
104
+ * @param {Object} participant - The knocking participant.
105
+ * @returns {{
106
+ *     participant: Object,
107
+ *     type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
108
+ * }}
109
+ */
110
+export function participantIsKnockingOrUpdated(participant: Object) {
111
+    return {
112
+        participant,
113
+        type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
114
+    };
115
+}
116
+
117
+/**
118
+ * Approves (lets in) or rejects a knocking participant.
119
+ *
120
+ * @param {string} id - The id of the knocking participant.
121
+ * @param {boolean} approved - True if the participant is approved, false otherwise.
122
+ * @returns {Function}
123
+ */
124
+export function setKnockingParticipantApproval(id: string, approved: boolean) {
125
+    return async (dispatch: Dispatch<any>, getState: Function) => {
126
+        const { conference } = getState()['features/base/conference'];
127
+
128
+        if (conference) {
129
+            if (approved) {
130
+                conference.lobbyApproveAccess(id);
131
+            } else {
132
+                conference.lobbyDenyAccess(id);
133
+            }
134
+        }
135
+    };
136
+}
137
+
138
+/**
139
+ * Action to set the new state of the lobby mode.
140
+ *
141
+ * @param {boolean} enabled - The new state to set.
142
+ * @returns {{
143
+ *     enabled: boolean,
144
+ *     type: SET_LOBBY_MODE_ENABLED
145
+ * }}
146
+ */
147
+export function setLobbyModeEnabled(enabled: boolean) {
148
+    return {
149
+        enabled,
150
+        type: SET_LOBBY_MODE_ENABLED
151
+    };
152
+}
153
+
154
+/**
155
+ * Action to show the dialog to disable lobby mode.
156
+ *
157
+ * @returns {showNotification}
158
+ */
159
+export function showDisableLobbyModeDialog() {
160
+    return openDialog(DisableLobbyModeDialog);
161
+}
162
+
163
+/**
164
+ * Action to show the dialog to enable lobby mode.
165
+ *
166
+ * @returns {showNotification}
167
+ */
168
+export function showEnableLobbyModeDialog() {
169
+    return openDialog(EnableLobbyModeDialog);
170
+}
171
+
172
+/**
173
+ * Action to toggle lobby mode on or off.
174
+ *
175
+ * @param {boolean} enabled - The desired (new) state of the lobby mode.
176
+ * @param {string} password - Optional password to be set.
177
+ * @returns {Function}
178
+ */
179
+export function toggleLobbyMode(enabled: boolean, password?: string) {
180
+    return async (dispatch: Dispatch<any>, getState: Function) => {
181
+        const { conference } = getState()['features/base/conference'];
182
+
183
+        if (enabled) {
184
+            conference.enableLobby(password);
185
+        } else {
186
+            conference.disableLobby();
187
+        }
188
+    };
189
+}

+ 47
- 0
react/features/lobby/components/AbstractDisableLobbyModeDialog.js 查看文件

@@ -0,0 +1,47 @@
1
+// @flow
2
+
3
+import { PureComponent } from 'react';
4
+
5
+import { toggleLobbyMode } from '../actions';
6
+
7
+export type Props = {
8
+
9
+    /**
10
+     * The Redux Dispatch function.
11
+     */
12
+    dispatch: Function,
13
+
14
+    /**
15
+     * Function to be used to translate i18n labels.
16
+     */
17
+    t: Function
18
+};
19
+
20
+/**
21
+ * Abstract class to encapsulate the platform common code of the {@code DisableLobbyModeDialog}.
22
+ */
23
+export default class AbstractDisableLobbyModeDialog<P: Props = Props> extends PureComponent<P> {
24
+    /**
25
+     * Instantiates a new component.
26
+     *
27
+     * @inheritdoc
28
+     */
29
+    constructor(props: P) {
30
+        super(props);
31
+
32
+        this._onDisableLobbyMode = this._onDisableLobbyMode.bind(this);
33
+    }
34
+
35
+    _onDisableLobbyMode: () => void;
36
+
37
+    /**
38
+     * Callback to be invoked when the user initiates the lobby mode disable flow.
39
+     *
40
+     * @returns {void}
41
+     */
42
+    _onDisableLobbyMode() {
43
+        this.props.dispatch(toggleLobbyMode(false));
44
+
45
+        return true;
46
+    }
47
+}

+ 75
- 0
react/features/lobby/components/AbstractEnableLobbyModeDialog.js 查看文件

@@ -0,0 +1,75 @@
1
+// @flow
2
+
3
+import { PureComponent } from 'react';
4
+
5
+import { getFieldValue } from '../../base/react';
6
+import { toggleLobbyMode } from '../actions';
7
+
8
+export type Props = {
9
+
10
+    /**
11
+     * The Redux Dispatch function.
12
+     */
13
+    dispatch: Function,
14
+
15
+    /**
16
+     * Function to be used to translate i18n labels.
17
+     */
18
+    t: Function
19
+};
20
+
21
+type State = {
22
+
23
+    /**
24
+     * The password value entered into the field.
25
+     */
26
+    password: string
27
+};
28
+
29
+/**
30
+ * Abstract class to encapsulate the platform common code of the {@code EnableLobbyModeDialog}.
31
+ */
32
+export default class AbstractEnableLobbyModeDialog<P: Props = Props> extends PureComponent<P, State> {
33
+    /**
34
+     * Instantiates a new component.
35
+     *
36
+     * @inheritdoc
37
+     */
38
+    constructor(props: P) {
39
+        super(props);
40
+
41
+        this.state = {
42
+            password: ''
43
+        };
44
+
45
+        this._onEnableLobbyMode = this._onEnableLobbyMode.bind(this);
46
+        this._onChangePassword = this._onChangePassword.bind(this);
47
+    }
48
+
49
+    _onChangePassword: Object => void;
50
+
51
+    /**
52
+     * Callback to be invoked when the user changes the password.
53
+     *
54
+     * @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
55
+     * @returns {void}
56
+     */
57
+    _onChangePassword(event) {
58
+        this.setState({
59
+            password: getFieldValue(event)
60
+        });
61
+    }
62
+
63
+    _onEnableLobbyMode: () => void;
64
+
65
+    /**
66
+     * Callback to be invoked when the user initiates the lobby mode enable flow.
67
+     *
68
+     * @returns {void}
69
+     */
70
+    _onEnableLobbyMode() {
71
+        this.props.dispatch(toggleLobbyMode(true, this.state.password));
72
+
73
+        return true;
74
+    }
75
+}

+ 82
- 0
react/features/lobby/components/AbstractKnockingParticipantList.js 查看文件

@@ -0,0 +1,82 @@
1
+// @flow
2
+
3
+import { PureComponent } from 'react';
4
+
5
+import { isLocalParticipantModerator } from '../../base/participants';
6
+import { isToolboxVisible } from '../../toolbox';
7
+import { setKnockingParticipantApproval } from '../actions';
8
+
9
+type Props = {
10
+
11
+    /**
12
+     * The list of participants.
13
+     */
14
+    _participants: Array<Object>,
15
+
16
+    /**
17
+     * True if the toolbox is visible, so we need to adjust the position.
18
+     */
19
+    _toolboxVisible: boolean,
20
+
21
+    /**
22
+     * True if the list should be rendered.
23
+     */
24
+    _visible: boolean,
25
+
26
+    /**
27
+     * The Redux Dispatch function.
28
+     */
29
+    dispatch: Function,
30
+
31
+    /**
32
+     * Function to be used to translate i18n labels.
33
+     */
34
+    t: Function
35
+};
36
+
37
+/**
38
+ * Abstract class to encapsulate the platform common code of the {@code KnockingParticipantList}.
39
+ */
40
+export default class AbstractKnockingParticipantList extends PureComponent<Props> {
41
+    /**
42
+     * Instantiates a new component.
43
+     *
44
+     * @inheritdoc
45
+     */
46
+    constructor(props: Props) {
47
+        super(props);
48
+
49
+        this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
50
+    }
51
+
52
+    _onRespondToParticipant: (string, boolean) => Function;
53
+
54
+    /**
55
+     * Function that constructs a callback for the response handler button.
56
+     *
57
+     * @param {string} id - The id of the knocking participant.
58
+     * @param {boolean} approve - The response for the knocking.
59
+     * @returns {Function}
60
+     */
61
+    _onRespondToParticipant(id, approve) {
62
+        return () => {
63
+            this.props.dispatch(setKnockingParticipantApproval(id, approve));
64
+        };
65
+    }
66
+}
67
+
68
+/**
69
+ * Maps part of the Redux state to the props of this component.
70
+ *
71
+ * @param {Object} state - The Redux state.
72
+ * @returns {Props}
73
+ */
74
+export function mapStateToProps(state: Object): $Shape<Props> {
75
+    const _participants = state['features/lobby'].knockingParticipants;
76
+
77
+    return {
78
+        _participants,
79
+        _toolboxVisible: isToolboxVisible(state),
80
+        _visible: isLocalParticipantModerator(state) && Boolean(_participants?.length)
81
+    };
82
+}

+ 328
- 0
react/features/lobby/components/AbstractLobbyScreen.js 查看文件

@@ -0,0 +1,328 @@
1
+// @flow
2
+// eslint-disable-next-line no-unused-vars
3
+import React, { PureComponent } from 'react';
4
+
5
+import { getConferenceName } from '../../base/conference';
6
+import { getLocalParticipant } from '../../base/participants';
7
+import { getFieldValue } from '../../base/react';
8
+import { updateSettings } from '../../base/settings';
9
+import { cancelKnocking, startKnocking } from '../actions';
10
+
11
+export const SCREEN_STATES = {
12
+    EDIT: 1,
13
+    PASSWORD: 2,
14
+    VIEW: 3
15
+};
16
+
17
+export type Props = {
18
+
19
+    /**
20
+     * True if knocking is already happening, so we're waiting for a response.
21
+     */
22
+    _knocking: boolean,
23
+
24
+    /**
25
+     * The name of the meeting we're about to join.
26
+     */
27
+    _meetingName: string,
28
+
29
+    /**
30
+     * The email of the participant about to knock/join.
31
+     */
32
+    _participantEmail: string,
33
+
34
+    /**
35
+     * The id of the participant about to knock/join. This is the participant ID in the lobby room, at this point.
36
+     */
37
+    _participantId: string,
38
+
39
+    /**
40
+     * The name of the participant about to knock/join.
41
+     */
42
+    _participantName: string;
43
+
44
+    /**
45
+     * The Redux dispatch function.
46
+     */
47
+    dispatch: Function,
48
+
49
+    /**
50
+     * Function to be used to translate i18n labels.
51
+     */
52
+    t: Function
53
+};
54
+
55
+type State = {
56
+
57
+    /**
58
+     * The display name value entered into the field.
59
+     */
60
+    displayName: string,
61
+
62
+    /**
63
+     * The email value entered into the field.
64
+     */
65
+    email: string,
66
+
67
+    /**
68
+     * The password value entered into the field.
69
+     */
70
+    password: string,
71
+
72
+    /**
73
+     * The state of the screen. One of {@code SCREEN_STATES[*]}
74
+     */
75
+    screenState: number
76
+}
77
+
78
+/**
79
+ * Abstract class to encapsulate the platform common code of the {@code LobbyScreen}.
80
+ */
81
+export default class AbstractLobbyScreen extends PureComponent<Props, State> {
82
+    /**
83
+     * Instantiates a new component.
84
+     *
85
+     * @inheritdoc
86
+     */
87
+    constructor(props: Props) {
88
+        super(props);
89
+
90
+        this.state = {
91
+            displayName: props._participantName || '',
92
+            email: props._participantEmail || '',
93
+            password: '',
94
+            screenState: props._participantName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
95
+        };
96
+
97
+        this._onAskToJoin = this._onAskToJoin.bind(this);
98
+        this._onCancel = this._onCancel.bind(this);
99
+        this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
100
+        this._onChangeEmail = this._onChangeEmail.bind(this);
101
+        this._onChangePassword = this._onChangePassword.bind(this);
102
+        this._onEnableEdit = this._onEnableEdit.bind(this);
103
+        this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
104
+        this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
105
+    }
106
+
107
+    /**
108
+     * Returns the screen title.
109
+     *
110
+     * @returns {string}
111
+     */
112
+    _getScreenTitleKey() {
113
+        const withPassword = Boolean(this.state.password);
114
+
115
+        return this.props._knocking
116
+            ? withPassword ? 'lobby.joiningWithPasswordTitle' : 'lobby.joiningTitle'
117
+            : 'lobby.joinTitle';
118
+    }
119
+
120
+    _onAskToJoin: () => void;
121
+
122
+    /**
123
+     * Callback to be invoked when the user submits the joining request.
124
+     *
125
+     * @returns {void}
126
+     */
127
+    _onAskToJoin() {
128
+        this.props.dispatch(startKnocking(this.state.password));
129
+
130
+        return false;
131
+    }
132
+
133
+    _onCancel: () => boolean;
134
+
135
+    /**
136
+     * Callback to be invoked when the user cancels the dialog.
137
+     *
138
+     * @private
139
+     * @returns {boolean}
140
+     */
141
+    _onCancel() {
142
+        this.props.dispatch(cancelKnocking());
143
+
144
+        return true;
145
+    }
146
+
147
+    _onChangeDisplayName: Object => void;
148
+
149
+    /**
150
+     * Callback to be invoked when the user changes its display name.
151
+     *
152
+     * @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
153
+     * @returns {void}
154
+     */
155
+    _onChangeDisplayName(event) {
156
+        const displayName = getFieldValue(event);
157
+
158
+        this.setState({
159
+            displayName
160
+        }, () => {
161
+            this.props.dispatch(updateSettings({
162
+                displayName
163
+            }));
164
+        });
165
+    }
166
+
167
+    _onChangeEmail: Object => void;
168
+
169
+    /**
170
+     * Callback to be invoked when the user changes its email.
171
+     *
172
+     * @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
173
+     * @returns {void}
174
+     */
175
+    _onChangeEmail(event) {
176
+        const email = getFieldValue(event);
177
+
178
+        this.setState({
179
+            email
180
+        }, () => {
181
+            this.props.dispatch(updateSettings({
182
+                email
183
+            }));
184
+        });
185
+    }
186
+
187
+    _onChangePassword: Object => void;
188
+
189
+    /**
190
+     * Callback to be invoked when the user changes the password.
191
+     *
192
+     * @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
193
+     * @returns {void}
194
+     */
195
+    _onChangePassword(event) {
196
+        this.setState({
197
+            password: getFieldValue(event)
198
+        });
199
+    }
200
+
201
+    _onEnableEdit: () => void;
202
+
203
+    /**
204
+     * Callback to be invoked for the edit button.
205
+     *
206
+     * @returns {void}
207
+     */
208
+    _onEnableEdit() {
209
+        this.setState({
210
+            screenState: SCREEN_STATES.EDIT
211
+        });
212
+    }
213
+
214
+    _onSwitchToKnockMode: () => void;
215
+
216
+    /**
217
+     * Callback to be invoked for the enter (go back to) knocking mode button.
218
+     *
219
+     * @returns {void}
220
+     */
221
+    _onSwitchToKnockMode() {
222
+        this.setState({
223
+            screenState: this.state.displayName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
224
+        });
225
+    }
226
+
227
+    _onSwitchToPasswordMode: () => void;
228
+
229
+    /**
230
+     * Callback to be invoked for the enter password button.
231
+     *
232
+     * @returns {void}
233
+     */
234
+    _onSwitchToPasswordMode() {
235
+        this.setState({
236
+            screenState: SCREEN_STATES.PASSWORD
237
+        });
238
+    }
239
+
240
+    /**
241
+     * Renders the content of the dialog.
242
+     *
243
+     * @returns {React$Element}
244
+     */
245
+    _renderContent() {
246
+        const { _knocking } = this.props;
247
+        const { password, screenState } = this.state;
248
+        const withPassword = Boolean(password);
249
+
250
+        if (_knocking) {
251
+            return this._renderJoining(withPassword);
252
+        }
253
+
254
+        return (
255
+            <>
256
+                { screenState === SCREEN_STATES.VIEW && this._renderParticipantInfo() }
257
+                { screenState === SCREEN_STATES.EDIT && this._renderParticipantForm() }
258
+                { screenState === SCREEN_STATES.PASSWORD && this._renderPasswordForm() }
259
+
260
+                { (screenState === SCREEN_STATES.VIEW || screenState === SCREEN_STATES.EDIT)
261
+                    && this._renderStandardButtons() }
262
+                { screenState === SCREEN_STATES.PASSWORD && this._renderPasswordJoinButtons() }
263
+            </>
264
+        );
265
+    }
266
+
267
+    /**
268
+     * Renders the joining (waiting) fragment of the screen.
269
+     *
270
+     * @param {boolean} withPassword - True if we're joining with a password. False otherwise.
271
+     * @returns {React$Element}
272
+     */
273
+    _renderJoining: boolean => React$Element<*>;
274
+
275
+    /**
276
+     * Renders the participant form to let the knocking participant enter its details.
277
+     *
278
+     * @returns {React$Element}
279
+     */
280
+    _renderParticipantForm: () => React$Element<*>;
281
+
282
+    /**
283
+     * Renders the participant info fragment when we have all the required details of the user.
284
+     *
285
+     * @returns {React$Element}
286
+     */
287
+    _renderParticipantInfo: () => React$Element<*>;
288
+
289
+    /**
290
+     * Renders the password form to let the participant join by using a password instead of knocking.
291
+     *
292
+     * @returns {React$Element}
293
+     */
294
+    _renderPasswordForm: () => React$Element<*>;
295
+
296
+    /**
297
+     * Renders the password join button (set).
298
+     *
299
+     * @returns {React$Element}
300
+     */
301
+    _renderPasswordJoinButtons: () => React$Element<*>;
302
+
303
+    /**
304
+     * Renders the standard button set.
305
+     *
306
+     * @returns {React$Element}
307
+     */
308
+    _renderStandardButtons: () => React$Element<*>;
309
+}
310
+
311
+/**
312
+ * Maps part of the Redux state to the props of this component.
313
+ *
314
+ * @param {Object} state - The Redux state.
315
+ * @returns {Props}
316
+ */
317
+export function _mapStateToProps(state: Object): $Shape<Props> {
318
+    const localParticipant = getLocalParticipant(state);
319
+    const participantId = localParticipant?.id;
320
+
321
+    return {
322
+        _knocking: state['features/lobby'].knocking,
323
+        _meetingName: getConferenceName(state),
324
+        _participantEmail: localParticipant.email,
325
+        _participantId: participantId,
326
+        _participantName: localParticipant.name
327
+    };
328
+}

+ 76
- 0
react/features/lobby/components/LobbyModeButton.js 查看文件

@@ -0,0 +1,76 @@
1
+// @flow
2
+
3
+import { translate } from '../../base/i18n';
4
+import { IconMeetingUnlocked, IconMeetingLocked } from '../../base/icons';
5
+import { isLocalParticipantModerator } from '../../base/participants';
6
+import { connect } from '../../base/redux';
7
+import AbstractButton, { type Props as AbstractProps } from '../../base/toolbox/components/AbstractButton';
8
+import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../actions';
9
+
10
+type Props = AbstractProps & {
11
+
12
+    /**
13
+     * The Redux Dispatch function.
14
+     */
15
+    dispatch: Function,
16
+
17
+    /**
18
+     * True if the lobby mode is currently enabled for this conference.
19
+     */
20
+    lobbyEnabled: boolean
21
+};
22
+
23
+/**
24
+ * Component to render the lobby mode initiator button.
25
+ */
26
+class LobbyModeButton extends AbstractButton<Props, any> {
27
+    accessibilityLabel = 'toolbar.accessibilityLabel.lobbyButton';
28
+    icon = IconMeetingUnlocked;
29
+    label = 'toolbar.lobbyButtonEnable';
30
+    toggledLabel = 'toolbar.lobbyButtonDisable'
31
+    toggledIcon = IconMeetingLocked;
32
+
33
+    /**
34
+     * Callback for the click event of the button.
35
+     *
36
+     * @returns {void}
37
+     */
38
+    _handleClick() {
39
+        const { dispatch } = this.props;
40
+
41
+        if (this._isToggled()) {
42
+            dispatch(showDisableLobbyModeDialog());
43
+        } else {
44
+            dispatch(showEnableLobbyModeDialog());
45
+        }
46
+    }
47
+
48
+    /**
49
+     * Function to define the button state.
50
+     *
51
+     * @returns {boolean}
52
+     */
53
+    _isToggled() {
54
+        return this.props.lobbyEnabled;
55
+    }
56
+}
57
+
58
+/**
59
+ * Maps part of the Redux store to the props of this component.
60
+ *
61
+ * @param {Object} state - The Redux state.
62
+ * @param {Props} ownProps - The own props of the component.
63
+ * @returns {Props}
64
+ */
65
+export function _mapStateToProps(state: Object): $Shape<Props> {
66
+    const { conference } = state['features/base/conference'];
67
+    const { lobbyEnabled } = state['features/lobby'];
68
+    const lobbySupported = conference && conference.isLobbySupported();
69
+
70
+    return {
71
+        lobbyEnabled,
72
+        visible: lobbySupported && isLocalParticipantModerator(state)
73
+    };
74
+}
75
+
76
+export default translate(connect(_mapStateToProps)(LobbyModeButton));

+ 5
- 0
react/features/lobby/components/index.native.js 查看文件

@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+export * from './native';
4
+
5
+export { default as LobbyModeButton } from './LobbyModeButton';

+ 5
- 0
react/features/lobby/components/index.web.js 查看文件

@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+export * from './web';
4
+
5
+export { default as LobbyModeButton } from './LobbyModeButton';

+ 30
- 0
react/features/lobby/components/native/DisableLobbyModeDialog.js 查看文件

@@ -0,0 +1,30 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { ConfirmDialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
9
+
10
+/**
11
+ * Implements a dialog that lets the user disable the lobby mode.
12
+ */
13
+class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
14
+    /**
15
+     * Implements {@code PureComponent#render}.
16
+     *
17
+     * @inheritdoc
18
+     */
19
+    render() {
20
+        return (
21
+            <ConfirmDialog
22
+                contentKey = 'lobby.disableDialogContent'
23
+                onSubmit = { this._onDisableLobbyMode } />
24
+        );
25
+    }
26
+
27
+    _onDisableLobbyMode: () => void;
28
+}
29
+
30
+export default translate(connect()(DisableLobbyModeDialog));

+ 77
- 0
react/features/lobby/components/native/EnableLobbyModeDialog.js 查看文件

@@ -0,0 +1,77 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, TextInput, View } from 'react-native';
5
+
6
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
7
+import { CustomSubmitDialog } from '../../../base/dialog';
8
+import { translate } from '../../../base/i18n';
9
+import { connect } from '../../../base/redux';
10
+import { StyleType } from '../../../base/styles';
11
+import AbstractEnableLobbyModeDialog, { type Props as AbstractProps } from '../AbstractEnableLobbyModeDialog';
12
+
13
+import styles from './styles';
14
+
15
+type Props = AbstractProps & {
16
+
17
+    /**
18
+     * Color schemed common style of the dialog feature.
19
+     */
20
+    _dialogStyles: StyleType
21
+};
22
+
23
+/**
24
+ * Implements a dialog that lets the user enable the lobby mode.
25
+ */
26
+class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog<Props> {
27
+    /**
28
+     * Implements {@code PureComponent#render}.
29
+     *
30
+     * @inheritdoc
31
+     */
32
+    render() {
33
+        const { _dialogStyles, t } = this.props;
34
+
35
+        return (
36
+            <CustomSubmitDialog
37
+                okKey = 'lobby.enableDialogSubmit'
38
+                onSubmit = { this._onEnableLobbyMode }
39
+                titleKey = 'lobby.dialogTitle'>
40
+                <View style = { styles.formWrapper }>
41
+                    <Text>
42
+                        { t('lobby.enableDialogText') }
43
+                    </Text>
44
+                    <View style = { styles.fieldRow }>
45
+                        <Text>
46
+                            { t('lobby.enableDialogPasswordField') }
47
+                        </Text>
48
+                        <TextInput
49
+                            autoCapitalize = 'none'
50
+                            autoCompleteType = 'off'
51
+                            onChangeText = { this._onChangePassword }
52
+                            secureTextEntry = { true }
53
+                            style = { _dialogStyles.field } />
54
+                    </View>
55
+                </View>
56
+            </CustomSubmitDialog>
57
+        );
58
+    }
59
+
60
+    _onChangePassword: Object => void;
61
+
62
+    _onEnableLobbyMode: () => void;
63
+}
64
+
65
+/**
66
+ * Maps part of the Redux state to the props of this component.
67
+ *
68
+ * @param {Object} state - The Redux state.
69
+ * @returns {Props}
70
+ */
71
+function _mapStateToProps(state: Object): Object {
72
+    return {
73
+        _dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
74
+    };
75
+}
76
+
77
+export default translate(connect(_mapStateToProps)(EnableLobbyModeDialog));

+ 78
- 0
react/features/lobby/components/native/KnockingParticipantList.js 查看文件

@@ -0,0 +1,78 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
5
+
6
+import { Avatar } from '../../../base/avatar';
7
+import { translate } from '../../../base/i18n';
8
+import { connect } from '../../../base/redux';
9
+import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
10
+
11
+import styles from './styles';
12
+
13
+/**
14
+ * Component to render a list for the actively knocking participants.
15
+ */
16
+class KnockingParticipantList extends AbstractKnockingParticipantList {
17
+    /**
18
+     * Implements {@code PureComponent#render}.
19
+     *
20
+     * @inheritdoc
21
+     */
22
+    render() {
23
+        const { _participants, t } = this.props;
24
+
25
+        // On mobile we only show a portion of the list for screen real estate reasons
26
+        const participants = _participants.slice(0, 2);
27
+
28
+        return (
29
+            <ScrollView
30
+                style = { styles.knockingParticipantList }>
31
+                { participants.map(p => (
32
+                    <View
33
+                        key = { p.id }
34
+                        style = { styles.knockingParticipantListEntry }>
35
+                        <Avatar
36
+                            displayName = { p.name }
37
+                            size = { 48 }
38
+                            url = { p.loadableAvatarUrl } />
39
+                        <View style = { styles.knockingParticipantListDetails }>
40
+                            <Text style = { styles.knockingParticipantListText }>
41
+                                { p.name }
42
+                            </Text>
43
+                            { p.email && (
44
+                                <Text style = { styles.knockingParticipantListText }>
45
+                                    { p.email }
46
+                                </Text>
47
+                            ) }
48
+                        </View>
49
+                        <TouchableOpacity
50
+                            onPress = { this._onRespondToParticipant(p.id, true) }
51
+                            style = { [
52
+                                styles.knockingParticipantListButton,
53
+                                styles.knockingParticipantListPrimaryButton
54
+                            ] }>
55
+                            <Text style = { styles.knockingParticipantListText }>
56
+                                { t('lobby.allow') }
57
+                            </Text>
58
+                        </TouchableOpacity>
59
+                        <TouchableOpacity
60
+                            onPress = { this._onRespondToParticipant(p.id, false) }
61
+                            style = { [
62
+                                styles.knockingParticipantListButton,
63
+                                styles.knockingParticipantListSecondaryButton
64
+                            ] }>
65
+                            <Text style = { styles.knockingParticipantListText }>
66
+                                { t('lobby.reject') }
67
+                            </Text>
68
+                        </TouchableOpacity>
69
+                    </View>
70
+                )) }
71
+            </ScrollView>
72
+        );
73
+    }
74
+
75
+    _onRespondToParticipant: (string, boolean) => Function;
76
+}
77
+
78
+export default translate(connect(mapStateToProps)(KnockingParticipantList));

+ 234
- 0
react/features/lobby/components/native/LobbyScreen.js 查看文件

@@ -0,0 +1,234 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, View, TouchableOpacity, TextInput } from 'react-native';
5
+
6
+import { Avatar } from '../../../base/avatar';
7
+import { CustomDialog } from '../../../base/dialog';
8
+import { translate } from '../../../base/i18n';
9
+import { Icon, IconEdit } from '../../../base/icons';
10
+import { LoadingIndicator } from '../../../base/react';
11
+import { connect } from '../../../base/redux';
12
+import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
13
+
14
+import styles from './styles';
15
+
16
+/**
17
+ * Implements a waiting screen that represents the participant being in the lobby.
18
+ */
19
+class LobbyScreen extends AbstractLobbyScreen {
20
+    /**
21
+     * Implements {@code PureComponent#render}.
22
+     *
23
+     * @inheritdoc
24
+     */
25
+    render() {
26
+        const { _meetingName, t } = this.props;
27
+
28
+        return (
29
+            <CustomDialog
30
+                onCancel = { this._onCancel }
31
+                style = { styles.contentWrapper }>
32
+                <Text style = { styles.dialogTitle }>
33
+                    { t(this._getScreenTitleKey()) }
34
+                </Text>
35
+                <Text style = { styles.secondaryText }>
36
+                    { _meetingName }
37
+                </Text>
38
+                { this._renderContent() }
39
+            </CustomDialog>
40
+        );
41
+    }
42
+
43
+    _getScreenTitleKey: () => string;
44
+
45
+    _onAskToJoin: () => void;
46
+
47
+    _onCancel: () => boolean;
48
+
49
+    _onChangeDisplayName: Object => void;
50
+
51
+    _onChangeEmail: Object => void;
52
+
53
+    _onChangePassword: Object => void;
54
+
55
+    _onEnableEdit: () => void;
56
+
57
+    _onSwitchToKnockMode: () => void;
58
+
59
+    _onSwitchToPasswordMode: () => void;
60
+
61
+    _renderContent: () => React$Element<*>;
62
+
63
+    /**
64
+     * Renders the joining (waiting) fragment of the screen.
65
+     *
66
+     * @inheritdoc
67
+     */
68
+    _renderJoining() {
69
+        return (
70
+            <>
71
+                <LoadingIndicator
72
+                    color = 'black'
73
+                    style = { styles.loadingIndicator } />
74
+                <Text style = { styles.joiningMessage }>
75
+                    { this.props.t('lobby.joiningMessage') }
76
+                </Text>
77
+            </>
78
+        );
79
+    }
80
+
81
+    /**
82
+     * Renders the participant form to let the knocking participant enter its details.
83
+     *
84
+     * @inheritdoc
85
+     */
86
+    _renderParticipantForm() {
87
+        const { t } = this.props;
88
+        const { displayName, email } = this.state;
89
+
90
+        return (
91
+            <View style = { styles.formWrapper }>
92
+                <Text style = { styles.fieldLabel }>
93
+                    { t('lobby.nameField') }
94
+                </Text>
95
+                <TextInput
96
+                    onChangeText = { this._onChangeDisplayName }
97
+                    style = { styles.field }
98
+                    value = { displayName } />
99
+                <Text style = { styles.fieldLabel }>
100
+                    { t('lobby.emailField') }
101
+                </Text>
102
+                <TextInput
103
+                    onChangeText = { this._onChangeEmail }
104
+                    style = { styles.field }
105
+                    value = { email } />
106
+            </View>
107
+        );
108
+    }
109
+
110
+    /**
111
+     * Renders the participant info fragment when we have all the required details of the user.
112
+     *
113
+     * @inheritdoc
114
+     */
115
+    _renderParticipantInfo() {
116
+        const { displayName, email } = this.state;
117
+
118
+        return (
119
+            <View style = { styles.participantBox }>
120
+                <TouchableOpacity
121
+                    onPress = { this._onEnableEdit }
122
+                    style = { styles.editButton }>
123
+                    <Icon
124
+                        src = { IconEdit }
125
+                        style = { styles.editIcon } />
126
+                </TouchableOpacity>
127
+                <Avatar
128
+                    participantId = { this.props._participantId }
129
+                    size = { 64 }
130
+                    style = { styles.avatar } />
131
+                <Text style = { styles.displayNameText }>
132
+                    { displayName }
133
+                </Text>
134
+                { Boolean(email) && <Text style = { styles.secondaryText }>
135
+                    { email }
136
+                </Text> }
137
+            </View>
138
+        );
139
+    }
140
+
141
+    /**
142
+     * Renders the password form to let the participant join by using a password instead of knocking.
143
+     *
144
+     * @inheritdoc
145
+     */
146
+    _renderPasswordForm() {
147
+        return (
148
+            <View style = { styles.formWrapper }>
149
+                <Text style = { styles.fieldLabel }>
150
+                    { this.props.t('lobby.passwordField') }
151
+                </Text>
152
+                <TextInput
153
+                    autoCapitalize = 'none'
154
+                    autoCompleteType = 'off'
155
+                    onChangeText = { this._onChangePassword }
156
+                    secureTextEntry = { true }
157
+                    style = { styles.field }
158
+                    value = { this.state.password } />
159
+            </View>
160
+        );
161
+    }
162
+
163
+    /**
164
+     * Renders the password join button (set).
165
+     *
166
+     * @inheritdoc
167
+     */
168
+    _renderPasswordJoinButtons() {
169
+        const { t } = this.props;
170
+
171
+        return (
172
+            <>
173
+                <TouchableOpacity
174
+                    disabled = { !this.state.password }
175
+                    onPress = { this._onAskToJoin }
176
+                    style = { [
177
+                        styles.button,
178
+                        styles.primaryButton
179
+                    ] }>
180
+                    <Text style = { styles.primaryButtonText }>
181
+                        { t('lobby.passwordJoinButton') }
182
+                    </Text>
183
+                </TouchableOpacity>
184
+                <TouchableOpacity
185
+                    onPress = { this._onSwitchToKnockMode }
186
+                    style = { [
187
+                        styles.button,
188
+                        styles.secondaryButton
189
+                    ] }>
190
+                    <Text>
191
+                        { t('lobby.backToKnockModeButton') }
192
+                    </Text>
193
+                </TouchableOpacity>
194
+            </>
195
+        );
196
+    }
197
+
198
+    /**
199
+     * Renders the standard button set.
200
+     *
201
+     * @inheritdoc
202
+     */
203
+    _renderStandardButtons() {
204
+        const { t } = this.props;
205
+
206
+        return (
207
+            <>
208
+                <TouchableOpacity
209
+                    disabled = { !this.state.displayName }
210
+                    onPress = { this._onAskToJoin }
211
+                    style = { [
212
+                        styles.button,
213
+                        styles.primaryButton
214
+                    ] }>
215
+                    <Text style = { styles.primaryButtonText }>
216
+                        { t('lobby.knockButton') }
217
+                    </Text>
218
+                </TouchableOpacity>
219
+                <TouchableOpacity
220
+                    onPress = { this._onSwitchToPasswordMode }
221
+                    style = { [
222
+                        styles.button,
223
+                        styles.secondaryButton
224
+                    ] }>
225
+                    <Text>
226
+                        { t('lobby.enterPasswordButton') }
227
+                    </Text>
228
+                </TouchableOpacity>
229
+            </>
230
+        );
231
+    }
232
+}
233
+
234
+export default translate(connect(_mapStateToProps)(LobbyScreen));

+ 6
- 0
react/features/lobby/components/native/index.js 查看文件

@@ -0,0 +1,6 @@
1
+// @flow
2
+
3
+export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
4
+export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
5
+export { default as KnockingParticipantList } from './KnockingParticipantList';
6
+export { default as LobbyScreen } from './LobbyScreen';

+ 139
- 0
react/features/lobby/components/native/styles.js 查看文件

@@ -0,0 +1,139 @@
1
+// @flow
2
+
3
+const SECONDARY_COLOR = '#B8C7E0';
4
+
5
+export default {
6
+    avatar: {
7
+        borderColor: 'red'
8
+    },
9
+
10
+    button: {
11
+        alignItems: 'center',
12
+        borderRadius: 4,
13
+        marginVertical: 8,
14
+        paddingVertical: 10
15
+    },
16
+
17
+    contentWrapper: {
18
+        alignItems: 'center',
19
+        flexDirection: 'column',
20
+        padding: 32
21
+    },
22
+
23
+    dialogTitle: {
24
+        fontSize: 18,
25
+        fontWeight: 'bold',
26
+        marginBottom: 10
27
+    },
28
+
29
+    displayNameText: {
30
+        fontWeight: 'bold',
31
+        marginVertical: 10
32
+    },
33
+
34
+    editButton: {
35
+        alignSelf: 'flex-end',
36
+        paddingHorizontal: 10
37
+    },
38
+
39
+    editIcon: {
40
+        color: 'black',
41
+        fontSize: 16
42
+    },
43
+
44
+    field: {
45
+        borderColor: SECONDARY_COLOR,
46
+        borderRadius: 4,
47
+        borderWidth: 1,
48
+        marginVertical: 8,
49
+        padding: 8
50
+    },
51
+
52
+    fieldRow: {
53
+        paddingTop: 16
54
+    },
55
+
56
+    fieldLabel: {
57
+        textAlign: 'center'
58
+    },
59
+
60
+    formWrapper: {
61
+        alignItems: 'stretch',
62
+        alignSelf: 'stretch',
63
+        paddingVertical: 16
64
+    },
65
+
66
+    joiningMessage: {
67
+        textAlign: 'center'
68
+    },
69
+
70
+    loadingIndicator: {
71
+        marginVertical: 36
72
+    },
73
+
74
+    participantBox: {
75
+        alignItems: 'center',
76
+        alignSelf: 'stretch',
77
+        borderColor: SECONDARY_COLOR,
78
+        borderRadius: 4,
79
+        borderWidth: 1,
80
+        marginVertical: 18,
81
+        paddingVertical: 12
82
+    },
83
+
84
+    primaryButton: {
85
+        alignSelf: 'stretch',
86
+        backgroundColor: 'rgb(3, 118, 218)'
87
+    },
88
+
89
+    primaryButtonText: {
90
+        color: 'white'
91
+    },
92
+
93
+    secondaryButton: {
94
+        alignSelf: 'stretch',
95
+        backgroundColor: 'transparent'
96
+    },
97
+
98
+    secondaryText: {
99
+        color: 'rgba(0, 0, 0, .7)'
100
+    },
101
+
102
+    // KnockingParticipantList
103
+
104
+    knockingParticipantList: {
105
+        alignSelf: 'stretch',
106
+        backgroundColor: 'rgba(22, 38, 55, 0.8)',
107
+        flexDirection: 'column'
108
+    },
109
+
110
+    knockingParticipantListButton: {
111
+        borderRadius: 4,
112
+        marginHorizontal: 3,
113
+        paddingHorizontal: 10,
114
+        paddingVertical: 5
115
+    },
116
+
117
+    knockingParticipantListDetails: {
118
+        flex: 1,
119
+        marginLeft: 10
120
+    },
121
+
122
+    knockingParticipantListEntry: {
123
+        alignItems: 'center',
124
+        flexDirection: 'row',
125
+        padding: 10
126
+    },
127
+
128
+    knockingParticipantListPrimaryButton: {
129
+        backgroundColor: 'rgb(3, 118, 218)'
130
+    },
131
+
132
+    knockingParticipantListSecondaryButton: {
133
+        backgroundColor: 'transparent'
134
+    },
135
+
136
+    knockingParticipantListText: {
137
+        color: 'white'
138
+    }
139
+};

+ 36
- 0
react/features/lobby/components/web/DisableLobbyModeDialog.js 查看文件

@@ -0,0 +1,36 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
9
+
10
+/**
11
+ * Implements a dialog that lets the user disable the lobby mode.
12
+ */
13
+class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
14
+    /**
15
+     * Implements {@code PureComponent#render}.
16
+     *
17
+     * @inheritdoc
18
+     */
19
+    render() {
20
+        const { t } = this.props;
21
+
22
+        return (
23
+            <Dialog
24
+                className = 'lobby-screen'
25
+                okKey = 'lobby.disableDialogSubmit'
26
+                onSubmit = { this._onDisableLobbyMode }
27
+                titleKey = 'lobby.dialogTitle'>
28
+                { t('lobby.disableDialogContent') }
29
+            </Dialog>
30
+        );
31
+    }
32
+
33
+    _onDisableLobbyMode: () => void;
34
+}
35
+
36
+export default translate(connect()(DisableLobbyModeDialog));

+ 51
- 0
react/features/lobby/components/web/EnableLobbyModeDialog.js 查看文件

@@ -0,0 +1,51 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractEnableLobbyModeDialog from '../AbstractEnableLobbyModeDialog';
9
+
10
+/**
11
+ * Implements a dialog that lets the user enable the lobby mode.
12
+ */
13
+class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog {
14
+    /**
15
+     * Implements {@code PureComponent#render}.
16
+     *
17
+     * @inheritdoc
18
+     */
19
+    render() {
20
+        const { t } = this.props;
21
+
22
+        return (
23
+            <Dialog
24
+                className = 'lobby-screen'
25
+                okKey = 'lobby.enableDialogSubmit'
26
+                onSubmit = { this._onEnableLobbyMode }
27
+                titleKey = 'lobby.dialogTitle'>
28
+                <div id = 'lobby-dialog'>
29
+                    <span className = 'description'>
30
+                        { t('lobby.enableDialogText') }
31
+                    </span>
32
+                    <div className = 'field'>
33
+                        <label htmlFor = 'password'>
34
+                            { t('lobby.enableDialogPasswordField') }
35
+                        </label>
36
+                        <input
37
+                            onChange = { this._onChangePassword }
38
+                            type = 'password'
39
+                            value = { this.state.password } />
40
+                    </div>
41
+                </div>
42
+            </Dialog>
43
+        );
44
+    }
45
+
46
+    _onChangePassword: Object => void;
47
+
48
+    _onEnableLobbyMode: () => void;
49
+}
50
+
51
+export default translate(connect()(EnableLobbyModeDialog));

+ 72
- 0
react/features/lobby/components/web/KnockingParticipantList.js 查看文件

@@ -0,0 +1,72 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Avatar } from '../../../base/avatar';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
9
+
10
+/**
11
+ * Component to render a list for the actively knocking participants.
12
+ */
13
+class KnockingParticipantList extends AbstractKnockingParticipantList {
14
+    /**
15
+     * Implements {@code PureComponent#render}.
16
+     *
17
+     * @inheritdoc
18
+     */
19
+    render() {
20
+        const { _participants, _toolboxVisible, _visible, t } = this.props;
21
+
22
+        if (!_visible) {
23
+            return null;
24
+        }
25
+
26
+        return (
27
+            <div
28
+                className = { _toolboxVisible ? 'toolbox-visible' : '' }
29
+                id = 'knocking-participant-list'>
30
+                <span className = 'title'>
31
+                    Knocking participant list
32
+                </span>
33
+                <ul>
34
+                    { _participants.map(p => (
35
+                        <li key = { p.id }>
36
+                            <Avatar
37
+                                displayName = { p.name }
38
+                                size = { 48 }
39
+                                url = { p.loadableAvatarUrl } />
40
+                            <div className = 'details'>
41
+                                <span>
42
+                                    { p.name }
43
+                                </span>
44
+                                { p.email && (
45
+                                    <span>
46
+                                        { p.email }
47
+                                    </span>
48
+                                ) }
49
+                            </div>
50
+                            <button
51
+                                className = 'primary'
52
+                                onClick = { this._onRespondToParticipant(p.id, true) }
53
+                                type = 'button'>
54
+                                { t('lobby.allow') }
55
+                            </button>
56
+                            <button
57
+                                className = 'borderLess'
58
+                                onClick = { this._onRespondToParticipant(p.id, false) }
59
+                                type = 'button'>
60
+                                { t('lobby.reject') }
61
+                            </button>
62
+                        </li>
63
+                    )) }
64
+                </ul>
65
+            </div>
66
+        );
67
+    }
68
+
69
+    _onRespondToParticipant: (string, boolean) => Function;
70
+}
71
+
72
+export default translate(connect(mapStateToProps)(KnockingParticipantList));

+ 219
- 0
react/features/lobby/components/web/LobbyScreen.js 查看文件

@@ -0,0 +1,219 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Avatar } from '../../../base/avatar';
6
+import { Dialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+import { Icon, IconEdit } from '../../../base/icons';
9
+import { LoadingIndicator } from '../../../base/react';
10
+import { connect } from '../../../base/redux';
11
+import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
12
+
13
+/**
14
+ * Implements a waiting screen that represents the participant being in the lobby.
15
+ */
16
+class LobbyScreen extends AbstractLobbyScreen {
17
+    /**
18
+     * Implements {@code PureComponent#render}.
19
+     *
20
+     * @inheritdoc
21
+     */
22
+    render() {
23
+        const { _meetingName, t } = this.props;
24
+
25
+        return (
26
+            <Dialog
27
+                disableBlanketClickDismiss = { false }
28
+                disableEnter = { true }
29
+                hideCancelButton = { true }
30
+                isModal = { false }
31
+                onCancel = { this._onCancel }
32
+                submitDisabled = { true }
33
+                width = 'small'>
34
+                <div id = 'lobby-screen'>
35
+                    <span className = 'title'>
36
+                        { t(this._getScreenTitleKey()) }
37
+                    </span>
38
+                    <span className = 'roomName'>
39
+                        { _meetingName }
40
+                    </span>
41
+                    { this._renderContent() }
42
+                </div>
43
+            </Dialog>
44
+        );
45
+    }
46
+
47
+    _getScreenTitleKey: () => string;
48
+
49
+    _onAskToJoin: () => boolean;
50
+
51
+    _onCancel: () => boolean;
52
+
53
+    _onChangeDisplayName: Object => void;
54
+
55
+    _onChangeEmail: Object => void;
56
+
57
+    _onChangePassword: Object => void;
58
+
59
+    _onEnableEdit: () => void;
60
+
61
+    _onSubmit: () => boolean;
62
+
63
+    _onSwitchToKnockMode: () => void;
64
+
65
+    _onSwitchToPasswordMode: () => void;
66
+
67
+    _renderContent: () => React$Element<*>;
68
+
69
+    /**
70
+     * Renders the joining (waiting) fragment of the screen.
71
+     *
72
+     * @inheritdoc
73
+     */
74
+    _renderJoining(withPassword) {
75
+        return (
76
+            <div className = 'joiningContainer'>
77
+                <LoadingIndicator />
78
+                <span>
79
+                    { this.props.t(`lobby.${withPassword ? 'joinWithPasswordMessage' : 'joiningMessage'}`) }
80
+                </span>
81
+            </div>
82
+        );
83
+    }
84
+
85
+    /**
86
+     * Renders the participant form to let the knocking participant enter its details.
87
+     *
88
+     * @inheritdoc
89
+     */
90
+    _renderParticipantForm() {
91
+        const { t } = this.props;
92
+        const { displayName, email } = this.state;
93
+
94
+        return (
95
+            <div className = 'form'>
96
+                <span>
97
+                    { t('lobby.nameField') }
98
+                </span>
99
+                <input
100
+                    onChange = { this._onChangeDisplayName }
101
+                    type = 'text'
102
+                    value = { displayName } />
103
+                <span>
104
+                    { t('lobby.emailField') }
105
+                </span>
106
+                <input
107
+                    onChange = { this._onChangeEmail }
108
+                    type = 'email'
109
+                    value = { email } />
110
+            </div>
111
+        );
112
+    }
113
+
114
+    /**
115
+     * Renders the participant info fragment when we have all the required details of the user.
116
+     *
117
+     * @inheritdoc
118
+     */
119
+    _renderParticipantInfo() {
120
+        const { displayName, email } = this.state;
121
+        const { _participantId } = this.props;
122
+
123
+        return (
124
+            <div className = 'participantInfo'>
125
+                <div className = 'editButton'>
126
+                    <button
127
+                        onClick = { this._onEnableEdit }
128
+                        type = 'button'>
129
+                        <Icon src = { IconEdit } />
130
+                    </button>
131
+                </div>
132
+                <Avatar
133
+                    participantId = { _participantId }
134
+                    size = { 64 } />
135
+                <span className = 'displayName'>
136
+                    { displayName }
137
+                </span>
138
+                <span className = 'email'>
139
+                    { email }
140
+                </span>
141
+            </div>
142
+        );
143
+    }
144
+
145
+    /**
146
+     * Renders the password form to let the participant join by using a password instead of knocking.
147
+     *
148
+     * @inheritdoc
149
+     */
150
+    _renderPasswordForm() {
151
+        return (
152
+            <div className = 'form'>
153
+                <span>
154
+                    { this.props.t('lobby.passwordField') }
155
+                </span>
156
+                <input
157
+                    onChange = { this._onChangePassword }
158
+                    type = 'password'
159
+                    value = { this.state.password } />
160
+            </div>
161
+        );
162
+    }
163
+
164
+    /**
165
+     * Renders the password join button (set).
166
+     *
167
+     * @inheritdoc
168
+     */
169
+    _renderPasswordJoinButtons() {
170
+        const { t } = this.props;
171
+
172
+        return (
173
+            <>
174
+                <button
175
+                    className = 'primary'
176
+                    disabled = { !this.state.password }
177
+                    onClick = { this._onAskToJoin }
178
+                    type = 'submit'>
179
+                    { t('lobby.passwordJoinButton') }
180
+                </button>
181
+                <button
182
+                    className = 'borderLess'
183
+                    onClick = { this._onSwitchToKnockMode }
184
+                    type = 'button'>
185
+                    { t('lobby.backToKnockModeButton') }
186
+                </button>
187
+            </>
188
+        );
189
+    }
190
+
191
+    /**
192
+     * Renders the standard button set.
193
+     *
194
+     * @inheritdoc
195
+     */
196
+    _renderStandardButtons() {
197
+        const { t } = this.props;
198
+
199
+        return (
200
+            <>
201
+                <button
202
+                    className = 'primary'
203
+                    disabled = { !this.state.displayName }
204
+                    onClick = { this._onAskToJoin }
205
+                    type = 'submit'>
206
+                    { t('lobby.knockButton') }
207
+                </button>
208
+                <button
209
+                    className = 'borderLess'
210
+                    onClick = { this._onSwitchToPasswordMode }
211
+                    type = 'button'>
212
+                    { t('lobby.enterPasswordButton') }
213
+                </button>
214
+            </>
215
+        );
216
+    }
217
+}
218
+
219
+export default translate(connect(_mapStateToProps)(LobbyScreen));

+ 6
- 0
react/features/lobby/components/web/index.js 查看文件

@@ -0,0 +1,6 @@
1
+// @flow
2
+
3
+export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
4
+export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
5
+export { default as KnockingParticipantList } from './KnockingParticipantList';
6
+export { default as LobbyScreen } from './LobbyScreen';

+ 39
- 0
react/features/lobby/functions.js 查看文件

@@ -0,0 +1,39 @@
1
+// @flow
2
+
3
+declare var interfaceConfig: Object;
4
+
5
+/**
6
+ * Returns a displayable name for the knocking participant.
7
+ *
8
+ * @param {string} name - The received name.
9
+ * @returns {string}
10
+ */
11
+export function getKnockingParticipantDisplayName(name: string) {
12
+    if (name) {
13
+        return name;
14
+    }
15
+
16
+    return typeof interfaceConfig === 'object'
17
+        ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
18
+        : 'Fellow Jitster';
19
+}
20
+
21
+/**
22
+ * Approves (lets in) or rejects a knocking participant.
23
+ *
24
+ * @param {Function} getState - Function to get the Redux state.
25
+ * @param {string} id - The id of the knocking participant.
26
+ * @param {boolean} approved - True if the participant is approved, false otherwise.
27
+ * @returns {Function}
28
+ */
29
+export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
30
+    const { conference } = getState()['features/base/conference'];
31
+
32
+    if (conference) {
33
+        if (approved) {
34
+            conference.lobbyApproveAccess(id);
35
+        } else {
36
+            conference.lobbyDenyAccess(id);
37
+        }
38
+    }
39
+}

+ 6
- 0
react/features/lobby/index.js 查看文件

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

+ 5
- 0
react/features/lobby/logger.js 查看文件

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

+ 137
- 0
react/features/lobby/middleware.js 查看文件

@@ -0,0 +1,137 @@
1
+// @flow
2
+
3
+import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
4
+import { hideDialog } from '../base/dialog';
5
+import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6
+import { getFirstLoadableAvatarUrl } from '../base/participants';
7
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
8
+import { NOTIFICATION_TYPE, showNotification } from '../notifications';
9
+
10
+import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
11
+import {
12
+    knockingParticipantLeft,
13
+    openLobbyScreen,
14
+    participantIsKnockingOrUpdated,
15
+    setLobbyModeEnabled
16
+} from './actions';
17
+import { LobbyScreen } from './components';
18
+
19
+MiddlewareRegistry.register(store => next => action => {
20
+    switch (action.type) {
21
+    case CONFERENCE_FAILED:
22
+        return _conferenceFailed(store, next, action);
23
+    case CONFERENCE_JOINED:
24
+        return _conferenceJoined(store, next, action);
25
+    case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
26
+        // We need the full update result to be in the store already
27
+        const result = next(action);
28
+
29
+        _findLoadableAvatarForKnockingParticipant(store, action.participant);
30
+
31
+        return result;
32
+    }
33
+    }
34
+
35
+    return next(action);
36
+});
37
+
38
+/**
39
+ * Registers a change handler for state['features/base/conference'].conference to
40
+ * set the event listeners needed for the lobby feature to operate.
41
+ */
42
+StateListenerRegistry.register(
43
+    state => state['features/base/conference'].conference,
44
+    (conference, { dispatch }, previousConference) => {
45
+        if (conference && !previousConference) {
46
+            conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
47
+                dispatch(setLobbyModeEnabled(enabled));
48
+            });
49
+
50
+            conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
51
+                dispatch(participantIsKnockingOrUpdated({
52
+                    id,
53
+                    name
54
+                }));
55
+            });
56
+
57
+            conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {
58
+                dispatch(participantIsKnockingOrUpdated({
59
+                    ...participant,
60
+                    id
61
+                }));
62
+            });
63
+
64
+            conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
65
+                dispatch(knockingParticipantLeft(id));
66
+            });
67
+        }
68
+    });
69
+
70
+/**
71
+ * Function to handle the conference failed event and navigate the user to the lobby screen
72
+ * based on the failure reason.
73
+ *
74
+ * @param {Object} store - The Redux store.
75
+ * @param {Function} next - The Redux next function.
76
+ * @param {Object} action - The Redux action.
77
+ * @returns {Object}
78
+ */
79
+function _conferenceFailed({ dispatch }, next, action) {
80
+    const { error } = action;
81
+
82
+    if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
83
+        if (typeof error.recoverable === 'undefined') {
84
+            error.recoverable = true;
85
+        }
86
+
87
+        dispatch(openLobbyScreen());
88
+    } else {
89
+        dispatch(hideDialog(LobbyScreen));
90
+
91
+        if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
92
+            dispatch(showNotification({
93
+                appearance: NOTIFICATION_TYPE.ERROR,
94
+                hideErrorSupportLink: true,
95
+                titleKey: 'lobby.joinRejectedMessage'
96
+            }));
97
+        }
98
+    }
99
+
100
+    return next(action);
101
+}
102
+
103
+/**
104
+ * Handles cleanup of lobby state when a conference is joined.
105
+ *
106
+ * @param {Object} store - The Redux store.
107
+ * @param {Function} next - The Redux next function.
108
+ * @param {Object} action - The Redux action.
109
+ * @returns {Object}
110
+ */
111
+function _conferenceJoined({ dispatch }, next, action) {
112
+    dispatch(hideDialog(LobbyScreen));
113
+
114
+    return next(action);
115
+}
116
+
117
+/**
118
+ * Finds the loadable avatar URL and updates the participant accordingly.
119
+ *
120
+ * @param {Object} store - The Redux store.
121
+ * @param {Object} participant - The knocking participant.
122
+ * @returns {void}
123
+ */
124
+function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id }) {
125
+    const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
126
+
127
+    if (updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
128
+        getFirstLoadableAvatarUrl(updatedParticipant).then(loadableAvatarUrl => {
129
+            if (loadableAvatarUrl) {
130
+                dispatch(participantIsKnockingOrUpdated({
131
+                    loadableAvatarUrl,
132
+                    id
133
+                }));
134
+            }
135
+        });
136
+    }
137
+}

+ 80
- 0
react/features/lobby/reducer.js 查看文件

@@ -0,0 +1,80 @@
1
+// @flow
2
+
3
+import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT } from '../base/conference';
4
+import { ReducerRegistry } from '../base/redux';
5
+
6
+import {
7
+    KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
8
+    KNOCKING_PARTICIPANT_LEFT,
9
+    SET_KNOCKING_STATE,
10
+    SET_LOBBY_MODE_ENABLED
11
+} from './actionTypes';
12
+
13
+const DEFAULT_STATE = {
14
+    knocking: false,
15
+    knockingParticipants: [],
16
+    lobbyEnabled: false
17
+};
18
+
19
+/**
20
+ * Reduces redux actions which affect the display of notifications.
21
+ *
22
+ * @param {Object} state - The current redux state.
23
+ * @param {Object} action - The redux action to reduce.
24
+ * @returns {Object} The next redux state which is the result of reducing the
25
+ * specified {@code action}.
26
+ */
27
+ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
28
+    switch (action.type) {
29
+    case CONFERENCE_FAILED:
30
+    case CONFERENCE_JOINED:
31
+    case CONFERENCE_LEFT:
32
+        return {
33
+            ...state,
34
+            knocking: false
35
+        };
36
+    case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
37
+        return _knockingParticipantArrivedOrUpdated(action.participant, state);
38
+    case KNOCKING_PARTICIPANT_LEFT:
39
+        return {
40
+            ...state,
41
+            knockingParticipants: state.knockingParticipants.filter(p => p.id !== action.id)
42
+        };
43
+    case SET_KNOCKING_STATE:
44
+        return {
45
+            ...state,
46
+            knocking: action.knocking
47
+        };
48
+    case SET_LOBBY_MODE_ENABLED:
49
+        return {
50
+            ...state,
51
+            lobbyEnabled: action.enabled
52
+        };
53
+    }
54
+
55
+    return state;
56
+});
57
+
58
+/**
59
+ * Stores or updates a knocking participant.
60
+ *
61
+ * @param {Object} participant - The arrived or updated knocking participant.
62
+ * @param {Object} state - The current Redux state of the feature.
63
+ * @returns {Object}
64
+ */
65
+function _knockingParticipantArrivedOrUpdated(participant, state) {
66
+    let existingParticipant = state.knockingParticipants.find(p => p.id === participant.id);
67
+
68
+    existingParticipant = {
69
+        ...existingParticipant,
70
+        ...participant
71
+    };
72
+
73
+    return {
74
+        ...state,
75
+        knockingParticipants: [
76
+            ...state.knockingParticipants.filter(p => p.id !== participant.id),
77
+            existingParticipant
78
+        ]
79
+    };
80
+}

+ 11
- 0
react/features/overlay/middleware.js 查看文件

@@ -1,11 +1,21 @@
1 1
 // @flow
2 2
 
3
+import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
3 4
 import { StateListenerRegistry } from '../base/redux';
4 5
 
5 6
 import { setFatalError } from './actions';
6 7
 
7 8
 declare var APP: Object;
8 9
 
10
+/**
11
+ * List of errors that are not fatal (or handled differently) so then the overlays won't kick in.
12
+ */
13
+const NON_FATAR_ERRORS = [
14
+    JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
15
+    JitsiConferenceErrors.CONFERENCE_DESTROYED,
16
+    JitsiConferenceErrors.CONNECTION_ERROR
17
+];
18
+
9 19
 /**
10 20
  * State listener which emits the {@code fatalErrorOccurred} action which works
11 21
  * as a catch all for critical errors which have not been claimed by any other
@@ -21,6 +31,7 @@ StateListenerRegistry.register(
21 31
     },
22 32
     /* listener */ (error, { dispatch }) => {
23 33
         error
34
+            && NON_FATAR_ERRORS.indexOf(error.name) === -1
24 35
             && typeof error.recoverable === 'undefined'
25 36
             && dispatch(setFatalError(error));
26 37
     }

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js 查看文件

@@ -11,6 +11,7 @@ import { connect } from '../../../base/redux';
11 11
 import { StyleType } from '../../../base/styles';
12 12
 import { SharedDocumentButton } from '../../../etherpad';
13 13
 import { InviteButton } from '../../../invite';
14
+import { LobbyModeButton } from '../../../lobby';
14 15
 import { AudioRouteButton } from '../../../mobile/audio-mode';
15 16
 import { LiveStreamButton, RecordButton } from '../../../recording';
16 17
 import { RoomLockButton } from '../../../room-lock';
@@ -128,6 +129,7 @@ class OverflowMenu extends PureComponent<Props, State> {
128 129
                 <InviteButton { ...buttonProps } />
129 130
                 <AudioOnlyButton { ...buttonProps } />
130 131
                 <RaiseHandButton { ...buttonProps } />
132
+                <LobbyModeButton { ...buttonProps } />
131 133
                 <MoreOptionsButton { ...moreOptionsButtonProps } />
132 134
                 <Collapsible collapsed = { !showMore }>
133 135
                     <ToggleCameraButton { ...buttonProps } />

+ 6
- 0
react/features/toolbox/components/web/Toolbox.js 查看文件

@@ -38,6 +38,7 @@ import { SharedDocumentButton } from '../../../etherpad';
38 38
 import { openFeedbackDialog } from '../../../feedback';
39 39
 import { beginAddPeople } from '../../../invite';
40 40
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
41
+import { LobbyModeButton } from '../../../lobby';
41 42
 import {
42 43
     LocalRecordingButton,
43 44
     LocalRecordingInfoDialog
@@ -1188,6 +1189,9 @@ class Toolbox extends Component<Props, State> {
1188 1189
         if (this._shouldShowButton('closedcaptions')) {
1189 1190
             buttonsLeft.push('closedcaptions');
1190 1191
         }
1192
+        if (this._shouldShowButton('lobby')) {
1193
+            buttonsRight.push('lobby');
1194
+        }
1191 1195
         if (overflowHasItems) {
1192 1196
             buttonsRight.push('overflowmenu');
1193 1197
         }
@@ -1271,6 +1275,8 @@ class Toolbox extends Component<Props, State> {
1271 1275
                     { this._renderVideoButton() }
1272 1276
                 </div>
1273 1277
                 <div className = 'button-group-right'>
1278
+                    { (buttonsRight.indexOf('lobby') !== -1)
1279
+                        && <LobbyModeButton /> }
1274 1280
                     { buttonsRight.indexOf('localrecording') !== -1
1275 1281
                         && <LocalRecordingButton
1276 1282
                             onClick = {

正在加载...
取消
保存