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
This commit is contained in:
parent
338c960215
commit
475a2ae596
|
@ -296,12 +296,6 @@ class ConferenceConnector {
|
||||||
logger.error('CONFERENCE FAILED:', err, ...params);
|
logger.error('CONFERENCE FAILED:', err, ...params);
|
||||||
|
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
|
||||||
const [ msg ] = params;
|
|
||||||
|
|
||||||
APP.UI.notifyConnectionFailed(msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
||||||
// let's show some auth not allowed page
|
// let's show some auth not allowed page
|
||||||
|
@ -336,14 +330,6 @@ class ConferenceConnector {
|
||||||
APP.UI.notifyGracefulShutdown();
|
APP.UI.notifyGracefulShutdown();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
|
|
||||||
const [ reason ] = params;
|
|
||||||
|
|
||||||
APP.UI.hideStats();
|
|
||||||
APP.UI.notifyConferenceDestroyed(reason);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME FOCUS_DISCONNECTED is a confusing event name.
|
// FIXME FOCUS_DISCONNECTED is a confusing event name.
|
||||||
// What really happens there is that the library is not ready yet,
|
// What really happens there is that the library is not ready yet,
|
||||||
// because Jicofo is not available, but it is going to give it another
|
// because Jicofo is not available, but it is going to give it another
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
#lobby-screen {
|
||||||
|
align-items: center;
|
||||||
|
color: $overflowMenuItemColor;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 48px 36px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $defaultColor;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomName {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participantInfo {
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border: 1px solid #B8C7E0;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 24px 0;
|
||||||
|
padding: 34px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
padding-top: 0px;
|
||||||
|
|
||||||
|
.editButton {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editButton {
|
||||||
|
align-self: stretch;
|
||||||
|
display: none;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 5px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayName {
|
||||||
|
color: $defaultColor;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 32px 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin: 5px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joiningContainer {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 36px 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#lobby-dialog {
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 32px 0;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#knocking-participant-list {
|
||||||
|
background-color: $newToolbarBackgroundColor;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .4);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
left: 0;
|
||||||
|
margin: 20px;
|
||||||
|
position: fixed;
|
||||||
|
top: 20;
|
||||||
|
transition: top 1s ease;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
&.toolbox-visible {
|
||||||
|
// Same as toolbox subject position
|
||||||
|
top: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
background-color: rgba(0, 0, 0, .2);
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 15px
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0 15px 15px 15px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 8px 0;
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin: 0 30px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-self: unset;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common styles
|
||||||
|
|
||||||
|
#lobby-dialog, #lobby-screen, #knocking-participant-list {
|
||||||
|
input {
|
||||||
|
align-self: stretch;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #B8C7E0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 8px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgb(3, 118, 218);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 12px;
|
||||||
|
transition: .2s transform ease;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.borderLess {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: rgb(3, 118, 218);
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,7 @@ $flagsImagePath: "../images/";
|
||||||
@import 'filmstrip/vertical_filmstrip';
|
@import 'filmstrip/vertical_filmstrip';
|
||||||
@import 'filmstrip/vertical_filmstrip_overrides';
|
@import 'filmstrip/vertical_filmstrip_overrides';
|
||||||
@import 'labels';
|
@import 'labels';
|
||||||
|
@import 'lobby';
|
||||||
@import 'unsupported-browser/main';
|
@import 'unsupported-browser/main';
|
||||||
@import 'modals/invite/add-people';
|
@import 'modals/invite/add-people';
|
||||||
@import 'deep-linking/main';
|
@import 'deep-linking/main';
|
||||||
|
|
|
@ -48,7 +48,7 @@ var interfaceConfig = {
|
||||||
*/
|
*/
|
||||||
TOOLBAR_BUTTONS: [
|
TOOLBAR_BUTTONS: [
|
||||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
'fodeviceselection', 'hangup', 'lobby', 'profile', 'chat', 'recording',
|
||||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
||||||
|
|
|
@ -675,6 +675,7 @@
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
"kick": "Kick participant",
|
"kick": "Kick participant",
|
||||||
|
"lobbyButton": "Enable/disable lobby mode",
|
||||||
"localRecording": "Toggle local recording controls",
|
"localRecording": "Toggle local recording controls",
|
||||||
"lockRoom": "Toggle meeting password",
|
"lockRoom": "Toggle meeting password",
|
||||||
"moreActions": "Toggle more actions menu",
|
"moreActions": "Toggle more actions menu",
|
||||||
|
@ -722,6 +723,8 @@
|
||||||
"hangup": "Leave",
|
"hangup": "Leave",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
|
"lobbyButtonDisable": "Disable lobby mode",
|
||||||
|
"lobbyButtonEnable": "Enable lobby mode",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"lowerYourHand": "Lower your hand",
|
"lowerYourHand": "Lower your hand",
|
||||||
|
@ -861,5 +864,29 @@
|
||||||
},
|
},
|
||||||
"helpView": {
|
"helpView": {
|
||||||
"header": "Help center"
|
"header": "Help center"
|
||||||
|
},
|
||||||
|
"lobby": {
|
||||||
|
"allow": "Allow",
|
||||||
|
"backToKnockModeButton": "No password, knock instead",
|
||||||
|
"dialogTitle": "Lobby mode",
|
||||||
|
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
|
||||||
|
"disableDialogSubmit": "Disable",
|
||||||
|
"emailField": "Enter your email address",
|
||||||
|
"enableDialogPasswordField": "Set password (optional)",
|
||||||
|
"enableDialogSubmit": "Enable",
|
||||||
|
"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.",
|
||||||
|
"enterPasswordButton": "Enter meeting password",
|
||||||
|
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
|
||||||
|
"joinWithPasswordMessage": "Trying to join with password, please wait...",
|
||||||
|
"joinRejectedMessage": "Your join request was rejected by a moderator.",
|
||||||
|
"joinTitle": "Join Meeting",
|
||||||
|
"joiningTitle": "Asking to join",
|
||||||
|
"joiningWithPasswordTitle": "Joining",
|
||||||
|
"knockButton": "Ask to Join",
|
||||||
|
"knockTitle": "Someone wants to join the meeting",
|
||||||
|
"nameField": "Enter your name",
|
||||||
|
"passwordField": "Enter password",
|
||||||
|
"passwordJoinButton": "Join",
|
||||||
|
"reject": "Reject"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,19 +98,6 @@ UI.notifyReservationError = function(code, msg) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify user that conference was destroyed.
|
|
||||||
* @param reason {string} the reason text
|
|
||||||
*/
|
|
||||||
UI.notifyConferenceDestroyed = function(reason) {
|
|
||||||
// FIXME: use Session Terminated from translation, but
|
|
||||||
// 'reason' text comes from XMPP packet and is not translated
|
|
||||||
messageHandler.showError({
|
|
||||||
description: reason,
|
|
||||||
titleKey: 'dialog.sessTerminated'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change nickname for the user.
|
* Change nickname for the user.
|
||||||
* @param {string} id user id
|
* @param {string} id user id
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
parseURIString,
|
parseURIString,
|
||||||
toURLString
|
toURLString
|
||||||
} from '../base/util';
|
} from '../base/util';
|
||||||
import { showNotification } from '../notifications';
|
import { clearNotifications, showNotification } from '../notifications';
|
||||||
import { setFatalError } from '../overlay';
|
import { setFatalError } from '../overlay';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -79,6 +79,10 @@ export function appNavigate(uri: ?string) {
|
||||||
dispatch(disconnect());
|
dispatch(disconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There are notifications now that gets displayed after we technically left
|
||||||
|
// the conference, but we're still on the conference screen.
|
||||||
|
dispatch(clearNotifications());
|
||||||
|
|
||||||
dispatch(configWillLoad(locationURL, room));
|
dispatch(configWillLoad(locationURL, room));
|
||||||
|
|
||||||
let protocol = location.protocol.toLowerCase();
|
let protocol = location.protocol.toLowerCase();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import '../../base/lastn'; // Register lastN middleware
|
||||||
import { toURLString } from '../../base/util';
|
import { toURLString } from '../../base/util';
|
||||||
import '../../follow-me';
|
import '../../follow-me';
|
||||||
import { OverlayContainer } from '../../overlay';
|
import { OverlayContainer } from '../../overlay';
|
||||||
|
import '../../lobby'; // Import lobby function
|
||||||
import '../../rejoin'; // Enable rejoin analytics
|
import '../../rejoin'; // Enable rejoin analytics
|
||||||
import { appNavigate } from '../actions';
|
import { appNavigate } from '../actions';
|
||||||
import { getDefaultURL } from '../functions';
|
import { getDefaultURL } from '../functions';
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
||||||
|
|
||||||
initialsText: (size: number = DEFAULT_SIZE) => {
|
initialsText: (size: number = DEFAULT_SIZE) => {
|
||||||
return {
|
return {
|
||||||
color: 'rgba(255, 255, 255, 0.6)',
|
color: 'white',
|
||||||
fontSize: size * 0.45,
|
fontSize: size * 0.45,
|
||||||
fontWeight: '100'
|
fontWeight: '100'
|
||||||
};
|
};
|
||||||
|
|
|
@ -256,7 +256,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
|
||||||
* }}
|
* }}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function conferenceFailed(conference: Object, error: string) {
|
export function conferenceFailed(conference: Object, error: string, ...params: any) {
|
||||||
return {
|
return {
|
||||||
type: CONFERENCE_FAILED,
|
type: CONFERENCE_FAILED,
|
||||||
conference,
|
conference,
|
||||||
|
@ -265,6 +265,7 @@ export function conferenceFailed(conference: Object, error: string) {
|
||||||
// jitsi-meet needs it).
|
// jitsi-meet needs it).
|
||||||
error: {
|
error: {
|
||||||
name: error,
|
name: error,
|
||||||
|
params,
|
||||||
recoverable: undefined
|
recoverable: undefined
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -203,7 +203,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
|
||||||
* @returns {JitsiConference|undefined}
|
* @returns {JitsiConference|undefined}
|
||||||
*/
|
*/
|
||||||
export function getCurrentConference(stateful: Function | Object) {
|
export function getCurrentConference(stateful: Function | Object) {
|
||||||
const { conference, joining, leaving, passwordRequired }
|
const { conference, joining, leaving, membersOnly, passwordRequired }
|
||||||
= toState(stateful)['features/base/conference'];
|
= toState(stateful)['features/base/conference'];
|
||||||
|
|
||||||
// There is a precendence
|
// There is a precendence
|
||||||
|
@ -211,7 +211,7 @@ export function getCurrentConference(stateful: Function | Object) {
|
||||||
return conference === leaving ? undefined : conference;
|
return conference === leaving ? undefined : conference;
|
||||||
}
|
}
|
||||||
|
|
||||||
return joining || passwordRequired;
|
return joining || passwordRequired || membersOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../analytics';
|
} from '../../analytics';
|
||||||
import { openDisplayNamePrompt } from '../../display-name';
|
import { openDisplayNamePrompt } from '../../display-name';
|
||||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
|
import { showErrorNotification } from '../../notifications';
|
||||||
|
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
|
||||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE } from '../media';
|
import { MEDIA_TYPE } from '../media';
|
||||||
import {
|
import {
|
||||||
|
@ -140,15 +141,43 @@ StateListenerRegistry.register(
|
||||||
* @private
|
* @private
|
||||||
* @returns {Object} The value returned by {@code next(action)}.
|
* @returns {Object} The value returned by {@code next(action)}.
|
||||||
*/
|
*/
|
||||||
function _conferenceFailed(store, next, action) {
|
function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
|
|
||||||
const { conference, error } = action;
|
const { conference, error } = action;
|
||||||
|
|
||||||
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
|
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
|
||||||
sendAnalytics(createOfferAnswerFailedEvent());
|
sendAnalytics(createOfferAnswerFailedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle specific failure reasons.
|
||||||
|
switch (error.name) {
|
||||||
|
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
|
||||||
|
const [ reason ] = error.params;
|
||||||
|
|
||||||
|
dispatch(showErrorNotification({
|
||||||
|
description: reason,
|
||||||
|
titleKey: 'dialog.sessTerminated'
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (typeof APP !== 'undefined') {
|
||||||
|
APP.UI.hideStats();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||||
|
const [ msg ] = error.params;
|
||||||
|
|
||||||
|
dispatch(connectionDisconnected(getState()['features/base/connection'].connection, 'Disconnected'));
|
||||||
|
dispatch(showErrorNotification({
|
||||||
|
descriptionArguments: { msg },
|
||||||
|
descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
|
||||||
|
titleKey: 'connection.CONNFAIL'
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||||
// conference is handled by /conference.js and appropriate failure handlers
|
// conference is handled by /conference.js and appropriate failure handlers
|
||||||
// are set there.
|
// are set there.
|
||||||
|
|
|
@ -36,6 +36,7 @@ const DEFAULT_STATE = {
|
||||||
leaving: undefined,
|
leaving: undefined,
|
||||||
locked: undefined,
|
locked: undefined,
|
||||||
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
|
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
|
||||||
|
membersOnly: undefined,
|
||||||
password: undefined,
|
password: undefined,
|
||||||
passwordRequired: undefined,
|
passwordRequired: undefined,
|
||||||
preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
|
preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
|
||||||
|
@ -161,6 +162,7 @@ function _conferenceFailed(state, { conference, error }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let authRequired;
|
let authRequired;
|
||||||
|
let membersOnly;
|
||||||
let passwordRequired;
|
let passwordRequired;
|
||||||
|
|
||||||
switch (error.name) {
|
switch (error.name) {
|
||||||
|
@ -168,6 +170,11 @@ function _conferenceFailed(state, { conference, error }) {
|
||||||
authRequired = conference;
|
authRequired = conference;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
|
||||||
|
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR:
|
||||||
|
membersOnly = conference;
|
||||||
|
break;
|
||||||
|
|
||||||
case JitsiConferenceErrors.PASSWORD_REQUIRED:
|
case JitsiConferenceErrors.PASSWORD_REQUIRED:
|
||||||
passwordRequired = conference;
|
passwordRequired = conference;
|
||||||
break;
|
break;
|
||||||
|
@ -189,6 +196,7 @@ function _conferenceFailed(state, { conference, error }) {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
|
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
|
||||||
|
membersOnly,
|
||||||
password: undefined,
|
password: undefined,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -232,6 +240,7 @@ function _conferenceJoined(state, { conference }) {
|
||||||
e2eeSupported: conference.isE2EESupported(),
|
e2eeSupported: conference.isE2EESupported(),
|
||||||
|
|
||||||
joining: undefined,
|
joining: undefined,
|
||||||
|
membersOnly: undefined,
|
||||||
leaving: undefined,
|
leaving: undefined,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -119,7 +119,7 @@ export function connect(id: ?string, password: ?string) {
|
||||||
*/
|
*/
|
||||||
function _onConnectionDisconnected(message: string) {
|
function _onConnectionDisconnected(message: string) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
dispatch(_connectionDisconnected(connection, message));
|
dispatch(connectionDisconnected(connection, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -195,7 +195,7 @@ export function connect(id: ?string, password: ?string) {
|
||||||
* message: string
|
* message: string
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _connectionDisconnected(connection: Object, message: string) {
|
export function connectionDisconnected(connection: Object, message: string) {
|
||||||
return {
|
return {
|
||||||
type: CONNECTION_DISCONNECTED,
|
type: CONNECTION_DISCONNECTED,
|
||||||
connection,
|
connection,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { configureInitialDevices } from '../devices';
|
||||||
import { getBackendSafeRoomName } from '../util';
|
import { getBackendSafeRoomName } from '../util';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
connectionDisconnected,
|
||||||
connectionEstablished,
|
connectionEstablished,
|
||||||
connectionFailed,
|
connectionFailed,
|
||||||
setLocationURL
|
setLocationURL
|
||||||
|
|
|
@ -57,14 +57,13 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior = 'height'
|
behavior = 'height'
|
||||||
style = { [
|
style = { [
|
||||||
styles.overlay,
|
styles.overlay
|
||||||
style
|
|
||||||
] }>
|
] }>
|
||||||
<View
|
<View
|
||||||
pointerEvents = 'box-none'
|
pointerEvents = 'box-none'
|
||||||
style = { [
|
style = { [
|
||||||
_dialogStyles.dialog,
|
_dialogStyles.dialog,
|
||||||
this.props.style
|
style
|
||||||
] }>
|
] }>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress = { this._onCancel }
|
onPress = { this._onCancel }
|
||||||
|
|
|
@ -34,7 +34,7 @@ class BaseSubmitDialog<P: Props, S: *> extends BaseDialog<P, S> {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
_getSubmitButtonKey() {
|
_getSubmitButtonKey() {
|
||||||
return 'dialog.Ok';
|
return this.props.okKey || 'dialog.Ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,6 +13,11 @@ import StatelessDialog from './StatelessDialog';
|
||||||
*/
|
*/
|
||||||
type Props = AbstractDialogProps & {
|
type Props = AbstractDialogProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if listening for the Enter key should be disabled.
|
||||||
|
*/
|
||||||
|
disableEnter: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the dialog is modal. This means clicking on the blanket will
|
* Whether the dialog is modal. This means clicking on the blanket will
|
||||||
* leave the dialog open. No cancel button.
|
* leave the dialog open. No cancel button.
|
||||||
|
|
|
@ -33,6 +33,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
customHeader?: React$Element<any> | Function,
|
customHeader?: React$Element<any> | Function,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* True if listening for the Enter key should be disabled.
|
||||||
|
*/
|
||||||
|
disableEnter: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||||
* by default.
|
* by default.
|
||||||
|
@ -313,7 +318,7 @@ class StatelessDialog extends Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter' && !this.props.disableEnter) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +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>
|
After Width: | Height: | Size: 287 B |
|
@ -32,6 +32,7 @@ export { default as IconDownload } from './download.svg';
|
||||||
export { default as IconDragHandle } from './drag-handle.svg';
|
export { default as IconDragHandle } from './drag-handle.svg';
|
||||||
export { default as IconE2EE } from './e2ee.svg';
|
export { default as IconE2EE } from './e2ee.svg';
|
||||||
export { default as IconEmail } from './envelope.svg';
|
export { default as IconEmail } from './envelope.svg';
|
||||||
|
export { default as IconEdit } from './edit.svg';
|
||||||
export { default as IconEventNote } from './event_note.svg';
|
export { default as IconEventNote } from './event_note.svg';
|
||||||
export { default as IconExclamation } from './exclamation.svg';
|
export { default as IconExclamation } from './exclamation.svg';
|
||||||
export { default as IconExclamationSolid } from './exclamation-solid.svg';
|
export { default as IconExclamationSolid } from './exclamation-solid.svg';
|
||||||
|
@ -46,6 +47,8 @@ export { default as IconInviteMore } from './user-plus.svg';
|
||||||
export { default as IconKick } from './kick.svg';
|
export { default as IconKick } from './kick.svg';
|
||||||
export { default as IconLiveStreaming } from './public.svg';
|
export { default as IconLiveStreaming } from './public.svg';
|
||||||
export { default as IconLockPassword } from './lock.svg';
|
export { default as IconLockPassword } from './lock.svg';
|
||||||
|
export { default as IconMeetingLocked } from './meeting-locked.svg';
|
||||||
|
export { default as IconMeetingUnlocked } from './meeting-unlocked.svg';
|
||||||
export { default as IconMenu } from './menu.svg';
|
export { default as IconMenu } from './menu.svg';
|
||||||
export { default as IconMenuDown } from './menu-down.svg';
|
export { default as IconMenuDown } from './menu-down.svg';
|
||||||
export { default as IconMenuThumb } from './thumb-menu.svg';
|
export { default as IconMenuThumb } from './thumb-menu.svg';
|
||||||
|
|
|
@ -0,0 +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>
|
After Width: | Height: | Size: 224 B |
|
@ -0,0 +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>
|
After Width: | Height: | Size: 156 B |
|
@ -0,0 +1,11 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the field value in a platform generic way.
|
||||||
|
*
|
||||||
|
* @param {Object | string} fieldParameter - The parameter passed through the change event function.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFieldValue(fieldParameter: Object | string) {
|
||||||
|
return typeof fieldParameter === 'string' ? fieldParameter : fieldParameter?.target?.value;
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './functions';
|
||||||
|
|
||||||
export { default as Platform } from './Platform';
|
export { default as Platform } from './Platform';
|
||||||
export * from './Types';
|
export * from './Types';
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
} from '../../../filmstrip';
|
} from '../../../filmstrip';
|
||||||
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
|
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
|
||||||
import { LargeVideo } from '../../../large-video';
|
import { LargeVideo } from '../../../large-video';
|
||||||
|
import { KnockingParticipantList } from '../../../lobby';
|
||||||
import { BackButtonRegistry } from '../../../mobile/back-button';
|
import { BackButtonRegistry } from '../../../mobile/back-button';
|
||||||
import { Captions } from '../../../subtitles';
|
import { Captions } from '../../../subtitles';
|
||||||
import { isToolboxVisible, setToolboxVisible, Toolbox } from '../../../toolbox';
|
import { isToolboxVisible, setToolboxVisible, Toolbox } from '../../../toolbox';
|
||||||
|
@ -320,6 +321,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||||
style = { styles.navBarSafeView }>
|
style = { styles.navBarSafeView }>
|
||||||
<NavigationBar />
|
<NavigationBar />
|
||||||
{ this._renderNotificationsContainer() }
|
{ this._renderNotificationsContainer() }
|
||||||
|
<KnockingParticipantList />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
||||||
<TestConnectionInfo />
|
<TestConnectionInfo />
|
||||||
|
@ -414,6 +416,7 @@ function _mapStateToProps(state) {
|
||||||
const {
|
const {
|
||||||
conference,
|
conference,
|
||||||
joining,
|
joining,
|
||||||
|
membersOnly,
|
||||||
leaving
|
leaving
|
||||||
} = state['features/base/conference'];
|
} = state['features/base/conference'];
|
||||||
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
|
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
|
||||||
|
@ -428,7 +431,7 @@ function _mapStateToProps(state) {
|
||||||
// - the XMPP connection is connected and we have no conference yet, nor we
|
// - the XMPP connection is connected and we have no conference yet, nor we
|
||||||
// are leaving one.
|
// are leaving one.
|
||||||
const connecting_
|
const connecting_
|
||||||
= connecting || (connection && (joining || (!conference && !leaving)));
|
= connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...abstractMapStateToProps(state),
|
...abstractMapStateToProps(state),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Chat } from '../../../chat';
|
||||||
import { Filmstrip } from '../../../filmstrip';
|
import { Filmstrip } from '../../../filmstrip';
|
||||||
import { CalleeInfoContainer } from '../../../invite';
|
import { CalleeInfoContainer } from '../../../invite';
|
||||||
import { LargeVideo } from '../../../large-video';
|
import { LargeVideo } from '../../../large-video';
|
||||||
|
import { KnockingParticipantList } from '../../../lobby';
|
||||||
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
||||||
import {
|
import {
|
||||||
Toolbox,
|
Toolbox,
|
||||||
|
@ -198,8 +199,8 @@ class Conference extends AbstractConference<Props, *> {
|
||||||
<InviteMore />
|
<InviteMore />
|
||||||
<div id = 'videospace'>
|
<div id = 'videospace'>
|
||||||
<LargeVideo />
|
<LargeVideo />
|
||||||
{ hideLabels
|
<KnockingParticipantList />
|
||||||
|| <Labels /> }
|
{ hideLabels || <Labels /> }
|
||||||
<Filmstrip filmstripOnly = { filmstripOnly } />
|
<Filmstrip filmstripOnly = { filmstripOnly } />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
StateListenerRegistry.register(
|
StateListenerRegistry.register(
|
||||||
state => getCurrentConference(state),
|
state => getCurrentConference(state),
|
||||||
(conference, { dispatch, getState }, prevConference) => {
|
(conference, { dispatch, getState }, prevConference) => {
|
||||||
const { authRequired, passwordRequired }
|
const { authRequired, membersOnly, passwordRequired }
|
||||||
= getState()['features/base/conference'];
|
= getState()['features/base/conference'];
|
||||||
|
|
||||||
if (conference !== prevConference) {
|
if (conference !== prevConference) {
|
||||||
|
@ -80,6 +80,7 @@ StateListenerRegistry.register(
|
||||||
// and explicitly check.
|
// and explicitly check.
|
||||||
if (typeof authRequired === 'undefined'
|
if (typeof authRequired === 'undefined'
|
||||||
&& typeof passwordRequired === 'undefined'
|
&& typeof passwordRequired === 'undefined'
|
||||||
|
&& typeof membersOnly === 'undefined'
|
||||||
&& !isDialogOpen(getState(), FeedbackDialog)) {
|
&& !isDialogOpen(getState(), FeedbackDialog)) {
|
||||||
// Conference changed, left or failed... and there is no
|
// Conference changed, left or failed... and there is no
|
||||||
// pending authentication, nor feedback request, so close any
|
// pending authentication, nor feedback request, so close any
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to signal the arriving or updating of a knocking participant.
|
||||||
|
*/
|
||||||
|
export const KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED = 'KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to signal the leave of a knocking participant.
|
||||||
|
*/
|
||||||
|
export const KNOCKING_PARTICIPANT_LEFT = 'KNOCKING_PARTICIPANT_LEFT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to set the new state of the lobby mode.
|
||||||
|
*/
|
||||||
|
export const SET_LOBBY_MODE_ENABLED = 'SET_LOBBY_MODE_ENABLED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to set the knocking state of the participant.
|
||||||
|
*/
|
||||||
|
export const SET_KNOCKING_STATE = 'SET_KNOCKING_STATE';
|
|
@ -0,0 +1,189 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { type Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { appNavigate, maybeRedirectToWelcomePage } from '../app';
|
||||||
|
import { conferenceLeft, conferenceWillJoin, getCurrentConference } from '../base/conference';
|
||||||
|
import { openDialog } from '../base/dialog';
|
||||||
|
import { getLocalParticipant } from '../base/participants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
|
||||||
|
KNOCKING_PARTICIPANT_LEFT,
|
||||||
|
SET_KNOCKING_STATE,
|
||||||
|
SET_LOBBY_MODE_ENABLED
|
||||||
|
} from './actionTypes';
|
||||||
|
import { DisableLobbyModeDialog, EnableLobbyModeDialog, LobbyScreen } from './components';
|
||||||
|
|
||||||
|
declare var APP: Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the ongoing knocking and abandones the join flow.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function cancelKnocking() {
|
||||||
|
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||||
|
if (typeof APP !== 'undefined') {
|
||||||
|
// when we are redirecting the library should handle any
|
||||||
|
// unload and clean of the connection.
|
||||||
|
APP.API.notifyReadyToClose();
|
||||||
|
dispatch(maybeRedirectToWelcomePage());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(conferenceLeft(getCurrentConference(getState)));
|
||||||
|
dispatch(appNavigate(undefined));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to be dispatched when a knocking poarticipant leaves before any response.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the participant.
|
||||||
|
* @returns {{
|
||||||
|
* id: string,
|
||||||
|
* type: KNOCKING_PARTICIPANT_LEFT
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function knockingParticipantLeft(id: string) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: KNOCKING_PARTICIPANT_LEFT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to set the knocking state of the participant.
|
||||||
|
*
|
||||||
|
* @param {boolean} knocking - The new state.
|
||||||
|
* @returns {{
|
||||||
|
* state: boolean,
|
||||||
|
* type: SET_KNOCKING_STATE
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setKnockingState(knocking: boolean) {
|
||||||
|
return {
|
||||||
|
knocking,
|
||||||
|
type: SET_KNOCKING_STATE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts knocking and waiting for approval.
|
||||||
|
*
|
||||||
|
* @param {string} password - The password to bypass knocking, if any.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function startKnocking(password?: string) {
|
||||||
|
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||||
|
const state = getState();
|
||||||
|
const { membersOnly } = state['features/base/conference'];
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
dispatch(setKnockingState(true));
|
||||||
|
dispatch(conferenceWillJoin(membersOnly));
|
||||||
|
membersOnly
|
||||||
|
&& membersOnly.joinLobby(localParticipant.name, localParticipant.email, password ? password : undefined);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to open the lobby screen.
|
||||||
|
*
|
||||||
|
* @returns {openDialog}
|
||||||
|
*/
|
||||||
|
export function openLobbyScreen() {
|
||||||
|
return openDialog(LobbyScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to be executed when a participant starts knocking or an already knocking participant gets updated.
|
||||||
|
*
|
||||||
|
* @param {Object} participant - The knocking participant.
|
||||||
|
* @returns {{
|
||||||
|
* participant: Object,
|
||||||
|
* type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function participantIsKnockingOrUpdated(participant: Object) {
|
||||||
|
return {
|
||||||
|
participant,
|
||||||
|
type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approves (lets in) or rejects a knocking participant.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the knocking participant.
|
||||||
|
* @param {boolean} approved - True if the participant is approved, false otherwise.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function setKnockingParticipantApproval(id: string, approved: boolean) {
|
||||||
|
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||||
|
const { conference } = getState()['features/base/conference'];
|
||||||
|
|
||||||
|
if (conference) {
|
||||||
|
if (approved) {
|
||||||
|
conference.lobbyApproveAccess(id);
|
||||||
|
} else {
|
||||||
|
conference.lobbyDenyAccess(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to set the new state of the lobby mode.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - The new state to set.
|
||||||
|
* @returns {{
|
||||||
|
* enabled: boolean,
|
||||||
|
* type: SET_LOBBY_MODE_ENABLED
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setLobbyModeEnabled(enabled: boolean) {
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
type: SET_LOBBY_MODE_ENABLED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to show the dialog to disable lobby mode.
|
||||||
|
*
|
||||||
|
* @returns {showNotification}
|
||||||
|
*/
|
||||||
|
export function showDisableLobbyModeDialog() {
|
||||||
|
return openDialog(DisableLobbyModeDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to show the dialog to enable lobby mode.
|
||||||
|
*
|
||||||
|
* @returns {showNotification}
|
||||||
|
*/
|
||||||
|
export function showEnableLobbyModeDialog() {
|
||||||
|
return openDialog(EnableLobbyModeDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to toggle lobby mode on or off.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - The desired (new) state of the lobby mode.
|
||||||
|
* @param {string} password - Optional password to be set.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function toggleLobbyMode(enabled: boolean, password?: string) {
|
||||||
|
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||||
|
const { conference } = getState()['features/base/conference'];
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
conference.enableLobby(password);
|
||||||
|
} else {
|
||||||
|
conference.disableLobby();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { toggleLobbyMode } from '../actions';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux Dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to encapsulate the platform common code of the {@code DisableLobbyModeDialog}.
|
||||||
|
*/
|
||||||
|
export default class AbstractDisableLobbyModeDialog<P: Props = Props> extends PureComponent<P> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new component.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: P) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onDisableLobbyMode = this._onDisableLobbyMode.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisableLobbyMode: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user initiates the lobby mode disable flow.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDisableLobbyMode() {
|
||||||
|
this.props.dispatch(toggleLobbyMode(false));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { getFieldValue } from '../../base/react';
|
||||||
|
import { toggleLobbyMode } from '../actions';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux Dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The password value entered into the field.
|
||||||
|
*/
|
||||||
|
password: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to encapsulate the platform common code of the {@code EnableLobbyModeDialog}.
|
||||||
|
*/
|
||||||
|
export default class AbstractEnableLobbyModeDialog<P: Props = Props> extends PureComponent<P, State> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new component.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: P) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
password: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onEnableLobbyMode = this._onEnableLobbyMode.bind(this);
|
||||||
|
this._onChangePassword = this._onChangePassword.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user changes the password.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onChangePassword(event) {
|
||||||
|
this.setState({
|
||||||
|
password: getFieldValue(event)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEnableLobbyMode: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user initiates the lobby mode enable flow.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEnableLobbyMode() {
|
||||||
|
this.props.dispatch(toggleLobbyMode(true, this.state.password));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { isLocalParticipantModerator } from '../../base/participants';
|
||||||
|
import { isToolboxVisible } from '../../toolbox';
|
||||||
|
import { setKnockingParticipantApproval } from '../actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of participants.
|
||||||
|
*/
|
||||||
|
_participants: Array<Object>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the toolbox is visible, so we need to adjust the position.
|
||||||
|
*/
|
||||||
|
_toolboxVisible: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the list should be rendered.
|
||||||
|
*/
|
||||||
|
_visible: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux Dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to encapsulate the platform common code of the {@code KnockingParticipantList}.
|
||||||
|
*/
|
||||||
|
export default class AbstractKnockingParticipantList extends PureComponent<Props> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new component.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRespondToParticipant: (string, boolean) => Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that constructs a callback for the response handler button.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the knocking participant.
|
||||||
|
* @param {boolean} approve - The response for the knocking.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
_onRespondToParticipant(id, approve) {
|
||||||
|
return () => {
|
||||||
|
this.props.dispatch(setKnockingParticipantApproval(id, approve));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux state to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function mapStateToProps(state: Object): $Shape<Props> {
|
||||||
|
const _participants = state['features/lobby'].knockingParticipants;
|
||||||
|
|
||||||
|
return {
|
||||||
|
_participants,
|
||||||
|
_toolboxVisible: isToolboxVisible(state),
|
||||||
|
_visible: isLocalParticipantModerator(state) && Boolean(_participants?.length)
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,328 @@
|
||||||
|
// @flow
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { getConferenceName } from '../../base/conference';
|
||||||
|
import { getLocalParticipant } from '../../base/participants';
|
||||||
|
import { getFieldValue } from '../../base/react';
|
||||||
|
import { updateSettings } from '../../base/settings';
|
||||||
|
import { cancelKnocking, startKnocking } from '../actions';
|
||||||
|
|
||||||
|
export const SCREEN_STATES = {
|
||||||
|
EDIT: 1,
|
||||||
|
PASSWORD: 2,
|
||||||
|
VIEW: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if knocking is already happening, so we're waiting for a response.
|
||||||
|
*/
|
||||||
|
_knocking: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the meeting we're about to join.
|
||||||
|
*/
|
||||||
|
_meetingName: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The email of the participant about to knock/join.
|
||||||
|
*/
|
||||||
|
_participantEmail: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the participant about to knock/join. This is the participant ID in the lobby room, at this point.
|
||||||
|
*/
|
||||||
|
_participantId: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the participant about to knock/join.
|
||||||
|
*/
|
||||||
|
_participantName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display name value entered into the field.
|
||||||
|
*/
|
||||||
|
displayName: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The email value entered into the field.
|
||||||
|
*/
|
||||||
|
email: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The password value entered into the field.
|
||||||
|
*/
|
||||||
|
password: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the screen. One of {@code SCREEN_STATES[*]}
|
||||||
|
*/
|
||||||
|
screenState: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to encapsulate the platform common code of the {@code LobbyScreen}.
|
||||||
|
*/
|
||||||
|
export default class AbstractLobbyScreen extends PureComponent<Props, State> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new component.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
displayName: props._participantName || '',
|
||||||
|
email: props._participantEmail || '',
|
||||||
|
password: '',
|
||||||
|
screenState: props._participantName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onAskToJoin = this._onAskToJoin.bind(this);
|
||||||
|
this._onCancel = this._onCancel.bind(this);
|
||||||
|
this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
|
||||||
|
this._onChangeEmail = this._onChangeEmail.bind(this);
|
||||||
|
this._onChangePassword = this._onChangePassword.bind(this);
|
||||||
|
this._onEnableEdit = this._onEnableEdit.bind(this);
|
||||||
|
this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
|
||||||
|
this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the screen title.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_getScreenTitleKey() {
|
||||||
|
const withPassword = Boolean(this.state.password);
|
||||||
|
|
||||||
|
return this.props._knocking
|
||||||
|
? withPassword ? 'lobby.joiningWithPasswordTitle' : 'lobby.joiningTitle'
|
||||||
|
: 'lobby.joinTitle';
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAskToJoin: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user submits the joining request.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onAskToJoin() {
|
||||||
|
this.props.dispatch(startKnocking(this.state.password));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCancel: () => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user cancels the dialog.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onCancel() {
|
||||||
|
this.props.dispatch(cancelKnocking());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangeDisplayName: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user changes its display name.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onChangeDisplayName(event) {
|
||||||
|
const displayName = getFieldValue(event);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
displayName
|
||||||
|
}, () => {
|
||||||
|
this.props.dispatch(updateSettings({
|
||||||
|
displayName
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangeEmail: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user changes its email.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onChangeEmail(event) {
|
||||||
|
const email = getFieldValue(event);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
email
|
||||||
|
}, () => {
|
||||||
|
this.props.dispatch(updateSettings({
|
||||||
|
email
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the user changes the password.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onChangePassword(event) {
|
||||||
|
this.setState({
|
||||||
|
password: getFieldValue(event)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEnableEdit: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked for the edit button.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEnableEdit() {
|
||||||
|
this.setState({
|
||||||
|
screenState: SCREEN_STATES.EDIT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSwitchToKnockMode: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked for the enter (go back to) knocking mode button.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSwitchToKnockMode() {
|
||||||
|
this.setState({
|
||||||
|
screenState: this.state.displayName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSwitchToPasswordMode: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked for the enter password button.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSwitchToPasswordMode() {
|
||||||
|
this.setState({
|
||||||
|
screenState: SCREEN_STATES.PASSWORD
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the content of the dialog.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderContent() {
|
||||||
|
const { _knocking } = this.props;
|
||||||
|
const { password, screenState } = this.state;
|
||||||
|
const withPassword = Boolean(password);
|
||||||
|
|
||||||
|
if (_knocking) {
|
||||||
|
return this._renderJoining(withPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ screenState === SCREEN_STATES.VIEW && this._renderParticipantInfo() }
|
||||||
|
{ screenState === SCREEN_STATES.EDIT && this._renderParticipantForm() }
|
||||||
|
{ screenState === SCREEN_STATES.PASSWORD && this._renderPasswordForm() }
|
||||||
|
|
||||||
|
{ (screenState === SCREEN_STATES.VIEW || screenState === SCREEN_STATES.EDIT)
|
||||||
|
&& this._renderStandardButtons() }
|
||||||
|
{ screenState === SCREEN_STATES.PASSWORD && this._renderPasswordJoinButtons() }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the joining (waiting) fragment of the screen.
|
||||||
|
*
|
||||||
|
* @param {boolean} withPassword - True if we're joining with a password. False otherwise.
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderJoining: boolean => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant form to let the knocking participant enter its details.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderParticipantForm: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant info fragment when we have all the required details of the user.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderParticipantInfo: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderPasswordForm: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password join button (set).
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderPasswordJoinButtons: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the standard button set.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderStandardButtons: () => React$Element<*>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux state to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapStateToProps(state: Object): $Shape<Props> {
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
const participantId = localParticipant?.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
_knocking: state['features/lobby'].knocking,
|
||||||
|
_meetingName: getConferenceName(state),
|
||||||
|
_participantEmail: localParticipant.email,
|
||||||
|
_participantId: participantId,
|
||||||
|
_participantName: localParticipant.name
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import { IconMeetingUnlocked, IconMeetingLocked } from '../../base/icons';
|
||||||
|
import { isLocalParticipantModerator } from '../../base/participants';
|
||||||
|
import { connect } from '../../base/redux';
|
||||||
|
import AbstractButton, { type Props as AbstractProps } from '../../base/toolbox/components/AbstractButton';
|
||||||
|
import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../actions';
|
||||||
|
|
||||||
|
type Props = AbstractProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux Dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the lobby mode is currently enabled for this conference.
|
||||||
|
*/
|
||||||
|
lobbyEnabled: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render the lobby mode initiator button.
|
||||||
|
*/
|
||||||
|
class LobbyModeButton extends AbstractButton<Props, any> {
|
||||||
|
accessibilityLabel = 'toolbar.accessibilityLabel.lobbyButton';
|
||||||
|
icon = IconMeetingUnlocked;
|
||||||
|
label = 'toolbar.lobbyButtonEnable';
|
||||||
|
toggledLabel = 'toolbar.lobbyButtonDisable'
|
||||||
|
toggledIcon = IconMeetingLocked;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the click event of the button.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_handleClick() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
if (this._isToggled()) {
|
||||||
|
dispatch(showDisableLobbyModeDialog());
|
||||||
|
} else {
|
||||||
|
dispatch(showEnableLobbyModeDialog());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to define the button state.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_isToggled() {
|
||||||
|
return this.props.lobbyEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux store to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @param {Props} ownProps - The own props of the component.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapStateToProps(state: Object): $Shape<Props> {
|
||||||
|
const { conference } = state['features/base/conference'];
|
||||||
|
const { lobbyEnabled } = state['features/lobby'];
|
||||||
|
const lobbySupported = conference && conference.isLobbySupported();
|
||||||
|
|
||||||
|
return {
|
||||||
|
lobbyEnabled,
|
||||||
|
visible: lobbySupported && isLocalParticipantModerator(state)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(LobbyModeButton));
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export * from './native';
|
||||||
|
|
||||||
|
export { default as LobbyModeButton } from './LobbyModeButton';
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export * from './web';
|
||||||
|
|
||||||
|
export { default as LobbyModeButton } from './LobbyModeButton';
|
|
@ -0,0 +1,30 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a dialog that lets the user disable the lobby mode.
|
||||||
|
*/
|
||||||
|
class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
contentKey = 'lobby.disableDialogContent'
|
||||||
|
onSubmit = { this._onDisableLobbyMode } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisableLobbyMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect()(DisableLobbyModeDialog));
|
|
@ -0,0 +1,77 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, TextInput, View } from 'react-native';
|
||||||
|
|
||||||
|
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||||
|
import { CustomSubmitDialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import { StyleType } from '../../../base/styles';
|
||||||
|
import AbstractEnableLobbyModeDialog, { type Props as AbstractProps } from '../AbstractEnableLobbyModeDialog';
|
||||||
|
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
type Props = AbstractProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color schemed common style of the dialog feature.
|
||||||
|
*/
|
||||||
|
_dialogStyles: StyleType
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a dialog that lets the user enable the lobby mode.
|
||||||
|
*/
|
||||||
|
class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog<Props> {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _dialogStyles, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomSubmitDialog
|
||||||
|
okKey = 'lobby.enableDialogSubmit'
|
||||||
|
onSubmit = { this._onEnableLobbyMode }
|
||||||
|
titleKey = 'lobby.dialogTitle'>
|
||||||
|
<View style = { styles.formWrapper }>
|
||||||
|
<Text>
|
||||||
|
{ t('lobby.enableDialogText') }
|
||||||
|
</Text>
|
||||||
|
<View style = { styles.fieldRow }>
|
||||||
|
<Text>
|
||||||
|
{ t('lobby.enableDialogPasswordField') }
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
autoCapitalize = 'none'
|
||||||
|
autoCompleteType = 'off'
|
||||||
|
onChangeText = { this._onChangePassword }
|
||||||
|
secureTextEntry = { true }
|
||||||
|
style = { _dialogStyles.field } />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CustomSubmitDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
_onEnableLobbyMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux state to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state: Object): Object {
|
||||||
|
return {
|
||||||
|
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(EnableLobbyModeDialog));
|
|
@ -0,0 +1,78 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
|
||||||
|
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a list for the actively knocking participants.
|
||||||
|
*/
|
||||||
|
class KnockingParticipantList extends AbstractKnockingParticipantList {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _participants, t } = this.props;
|
||||||
|
|
||||||
|
// On mobile we only show a portion of the list for screen real estate reasons
|
||||||
|
const participants = _participants.slice(0, 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style = { styles.knockingParticipantList }>
|
||||||
|
{ participants.map(p => (
|
||||||
|
<View
|
||||||
|
key = { p.id }
|
||||||
|
style = { styles.knockingParticipantListEntry }>
|
||||||
|
<Avatar
|
||||||
|
displayName = { p.name }
|
||||||
|
size = { 48 }
|
||||||
|
url = { p.loadableAvatarUrl } />
|
||||||
|
<View style = { styles.knockingParticipantListDetails }>
|
||||||
|
<Text style = { styles.knockingParticipantListText }>
|
||||||
|
{ p.name }
|
||||||
|
</Text>
|
||||||
|
{ p.email && (
|
||||||
|
<Text style = { styles.knockingParticipantListText }>
|
||||||
|
{ p.email }
|
||||||
|
</Text>
|
||||||
|
) }
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress = { this._onRespondToParticipant(p.id, true) }
|
||||||
|
style = { [
|
||||||
|
styles.knockingParticipantListButton,
|
||||||
|
styles.knockingParticipantListPrimaryButton
|
||||||
|
] }>
|
||||||
|
<Text style = { styles.knockingParticipantListText }>
|
||||||
|
{ t('lobby.allow') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress = { this._onRespondToParticipant(p.id, false) }
|
||||||
|
style = { [
|
||||||
|
styles.knockingParticipantListButton,
|
||||||
|
styles.knockingParticipantListSecondaryButton
|
||||||
|
] }>
|
||||||
|
<Text style = { styles.knockingParticipantListText }>
|
||||||
|
{ t('lobby.reject') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)) }
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRespondToParticipant: (string, boolean) => Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(mapStateToProps)(KnockingParticipantList));
|
|
@ -0,0 +1,234 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View, TouchableOpacity, TextInput } from 'react-native';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
|
import { CustomDialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Icon, IconEdit } from '../../../base/icons';
|
||||||
|
import { LoadingIndicator } from '../../../base/react';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
|
||||||
|
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a waiting screen that represents the participant being in the lobby.
|
||||||
|
*/
|
||||||
|
class LobbyScreen extends AbstractLobbyScreen {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _meetingName, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomDialog
|
||||||
|
onCancel = { this._onCancel }
|
||||||
|
style = { styles.contentWrapper }>
|
||||||
|
<Text style = { styles.dialogTitle }>
|
||||||
|
{ t(this._getScreenTitleKey()) }
|
||||||
|
</Text>
|
||||||
|
<Text style = { styles.secondaryText }>
|
||||||
|
{ _meetingName }
|
||||||
|
</Text>
|
||||||
|
{ this._renderContent() }
|
||||||
|
</CustomDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getScreenTitleKey: () => string;
|
||||||
|
|
||||||
|
_onAskToJoin: () => void;
|
||||||
|
|
||||||
|
_onCancel: () => boolean;
|
||||||
|
|
||||||
|
_onChangeDisplayName: Object => void;
|
||||||
|
|
||||||
|
_onChangeEmail: Object => void;
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
_onEnableEdit: () => void;
|
||||||
|
|
||||||
|
_onSwitchToKnockMode: () => void;
|
||||||
|
|
||||||
|
_onSwitchToPasswordMode: () => void;
|
||||||
|
|
||||||
|
_renderContent: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the joining (waiting) fragment of the screen.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderJoining() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LoadingIndicator
|
||||||
|
color = 'black'
|
||||||
|
style = { styles.loadingIndicator } />
|
||||||
|
<Text style = { styles.joiningMessage }>
|
||||||
|
{ this.props.t('lobby.joiningMessage') }
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant form to let the knocking participant enter its details.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderParticipantForm() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { displayName, email } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.formWrapper }>
|
||||||
|
<Text style = { styles.fieldLabel }>
|
||||||
|
{ t('lobby.nameField') }
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
onChangeText = { this._onChangeDisplayName }
|
||||||
|
style = { styles.field }
|
||||||
|
value = { displayName } />
|
||||||
|
<Text style = { styles.fieldLabel }>
|
||||||
|
{ t('lobby.emailField') }
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
onChangeText = { this._onChangeEmail }
|
||||||
|
style = { styles.field }
|
||||||
|
value = { email } />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant info fragment when we have all the required details of the user.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderParticipantInfo() {
|
||||||
|
const { displayName, email } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.participantBox }>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress = { this._onEnableEdit }
|
||||||
|
style = { styles.editButton }>
|
||||||
|
<Icon
|
||||||
|
src = { IconEdit }
|
||||||
|
style = { styles.editIcon } />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Avatar
|
||||||
|
participantId = { this.props._participantId }
|
||||||
|
size = { 64 }
|
||||||
|
style = { styles.avatar } />
|
||||||
|
<Text style = { styles.displayNameText }>
|
||||||
|
{ displayName }
|
||||||
|
</Text>
|
||||||
|
{ Boolean(email) && <Text style = { styles.secondaryText }>
|
||||||
|
{ email }
|
||||||
|
</Text> }
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderPasswordForm() {
|
||||||
|
return (
|
||||||
|
<View style = { styles.formWrapper }>
|
||||||
|
<Text style = { styles.fieldLabel }>
|
||||||
|
{ this.props.t('lobby.passwordField') }
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
autoCapitalize = 'none'
|
||||||
|
autoCompleteType = 'off'
|
||||||
|
onChangeText = { this._onChangePassword }
|
||||||
|
secureTextEntry = { true }
|
||||||
|
style = { styles.field }
|
||||||
|
value = { this.state.password } />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password join button (set).
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderPasswordJoinButtons() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled = { !this.state.password }
|
||||||
|
onPress = { this._onAskToJoin }
|
||||||
|
style = { [
|
||||||
|
styles.button,
|
||||||
|
styles.primaryButton
|
||||||
|
] }>
|
||||||
|
<Text style = { styles.primaryButtonText }>
|
||||||
|
{ t('lobby.passwordJoinButton') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress = { this._onSwitchToKnockMode }
|
||||||
|
style = { [
|
||||||
|
styles.button,
|
||||||
|
styles.secondaryButton
|
||||||
|
] }>
|
||||||
|
<Text>
|
||||||
|
{ t('lobby.backToKnockModeButton') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the standard button set.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderStandardButtons() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled = { !this.state.displayName }
|
||||||
|
onPress = { this._onAskToJoin }
|
||||||
|
style = { [
|
||||||
|
styles.button,
|
||||||
|
styles.primaryButton
|
||||||
|
] }>
|
||||||
|
<Text style = { styles.primaryButtonText }>
|
||||||
|
{ t('lobby.knockButton') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress = { this._onSwitchToPasswordMode }
|
||||||
|
style = { [
|
||||||
|
styles.button,
|
||||||
|
styles.secondaryButton
|
||||||
|
] }>
|
||||||
|
<Text>
|
||||||
|
{ t('lobby.enterPasswordButton') }
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(LobbyScreen));
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
|
||||||
|
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
|
||||||
|
export { default as KnockingParticipantList } from './KnockingParticipantList';
|
||||||
|
export { default as LobbyScreen } from './LobbyScreen';
|
|
@ -0,0 +1,139 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
const SECONDARY_COLOR = '#B8C7E0';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
avatar: {
|
||||||
|
borderColor: 'red'
|
||||||
|
},
|
||||||
|
|
||||||
|
button: {
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginVertical: 8,
|
||||||
|
paddingVertical: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
contentWrapper: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: 32
|
||||||
|
},
|
||||||
|
|
||||||
|
dialogTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
displayNameText: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginVertical: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
editButton: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
paddingHorizontal: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
editIcon: {
|
||||||
|
color: 'black',
|
||||||
|
fontSize: 16
|
||||||
|
},
|
||||||
|
|
||||||
|
field: {
|
||||||
|
borderColor: SECONDARY_COLOR,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
marginVertical: 8,
|
||||||
|
padding: 8
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldRow: {
|
||||||
|
paddingTop: 16
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldLabel: {
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
formWrapper: {
|
||||||
|
alignItems: 'stretch',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
paddingVertical: 16
|
||||||
|
},
|
||||||
|
|
||||||
|
joiningMessage: {
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
loadingIndicator: {
|
||||||
|
marginVertical: 36
|
||||||
|
},
|
||||||
|
|
||||||
|
participantBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
borderColor: SECONDARY_COLOR,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
marginVertical: 18,
|
||||||
|
paddingVertical: 12
|
||||||
|
},
|
||||||
|
|
||||||
|
primaryButton: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
backgroundColor: 'rgb(3, 118, 218)'
|
||||||
|
},
|
||||||
|
|
||||||
|
primaryButtonText: {
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
|
||||||
|
secondaryButton: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
},
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
color: 'rgba(0, 0, 0, .7)'
|
||||||
|
},
|
||||||
|
|
||||||
|
// KnockingParticipantList
|
||||||
|
|
||||||
|
knockingParticipantList: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
backgroundColor: 'rgba(22, 38, 55, 0.8)',
|
||||||
|
flexDirection: 'column'
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListButton: {
|
||||||
|
borderRadius: 4,
|
||||||
|
marginHorizontal: 3,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 5
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListDetails: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListEntry: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListPrimaryButton: {
|
||||||
|
backgroundColor: 'rgb(3, 118, 218)'
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListSecondaryButton: {
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
},
|
||||||
|
|
||||||
|
knockingParticipantListText: {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Dialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a dialog that lets the user disable the lobby mode.
|
||||||
|
*/
|
||||||
|
class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className = 'lobby-screen'
|
||||||
|
okKey = 'lobby.disableDialogSubmit'
|
||||||
|
onSubmit = { this._onDisableLobbyMode }
|
||||||
|
titleKey = 'lobby.dialogTitle'>
|
||||||
|
{ t('lobby.disableDialogContent') }
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisableLobbyMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect()(DisableLobbyModeDialog));
|
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Dialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractEnableLobbyModeDialog from '../AbstractEnableLobbyModeDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a dialog that lets the user enable the lobby mode.
|
||||||
|
*/
|
||||||
|
class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className = 'lobby-screen'
|
||||||
|
okKey = 'lobby.enableDialogSubmit'
|
||||||
|
onSubmit = { this._onEnableLobbyMode }
|
||||||
|
titleKey = 'lobby.dialogTitle'>
|
||||||
|
<div id = 'lobby-dialog'>
|
||||||
|
<span className = 'description'>
|
||||||
|
{ t('lobby.enableDialogText') }
|
||||||
|
</span>
|
||||||
|
<div className = 'field'>
|
||||||
|
<label htmlFor = 'password'>
|
||||||
|
{ t('lobby.enableDialogPasswordField') }
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange = { this._onChangePassword }
|
||||||
|
type = 'password'
|
||||||
|
value = { this.state.password } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
_onEnableLobbyMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect()(EnableLobbyModeDialog));
|
|
@ -0,0 +1,72 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a list for the actively knocking participants.
|
||||||
|
*/
|
||||||
|
class KnockingParticipantList extends AbstractKnockingParticipantList {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _participants, _toolboxVisible, _visible, t } = this.props;
|
||||||
|
|
||||||
|
if (!_visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className = { _toolboxVisible ? 'toolbox-visible' : '' }
|
||||||
|
id = 'knocking-participant-list'>
|
||||||
|
<span className = 'title'>
|
||||||
|
Knocking participant list
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
{ _participants.map(p => (
|
||||||
|
<li key = { p.id }>
|
||||||
|
<Avatar
|
||||||
|
displayName = { p.name }
|
||||||
|
size = { 48 }
|
||||||
|
url = { p.loadableAvatarUrl } />
|
||||||
|
<div className = 'details'>
|
||||||
|
<span>
|
||||||
|
{ p.name }
|
||||||
|
</span>
|
||||||
|
{ p.email && (
|
||||||
|
<span>
|
||||||
|
{ p.email }
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className = 'primary'
|
||||||
|
onClick = { this._onRespondToParticipant(p.id, true) }
|
||||||
|
type = 'button'>
|
||||||
|
{ t('lobby.allow') }
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className = 'borderLess'
|
||||||
|
onClick = { this._onRespondToParticipant(p.id, false) }
|
||||||
|
type = 'button'>
|
||||||
|
{ t('lobby.reject') }
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)) }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRespondToParticipant: (string, boolean) => Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(mapStateToProps)(KnockingParticipantList));
|
|
@ -0,0 +1,219 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
|
import { Dialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Icon, IconEdit } from '../../../base/icons';
|
||||||
|
import { LoadingIndicator } from '../../../base/react';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a waiting screen that represents the participant being in the lobby.
|
||||||
|
*/
|
||||||
|
class LobbyScreen extends AbstractLobbyScreen {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _meetingName, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
disableBlanketClickDismiss = { false }
|
||||||
|
disableEnter = { true }
|
||||||
|
hideCancelButton = { true }
|
||||||
|
isModal = { false }
|
||||||
|
onCancel = { this._onCancel }
|
||||||
|
submitDisabled = { true }
|
||||||
|
width = 'small'>
|
||||||
|
<div id = 'lobby-screen'>
|
||||||
|
<span className = 'title'>
|
||||||
|
{ t(this._getScreenTitleKey()) }
|
||||||
|
</span>
|
||||||
|
<span className = 'roomName'>
|
||||||
|
{ _meetingName }
|
||||||
|
</span>
|
||||||
|
{ this._renderContent() }
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getScreenTitleKey: () => string;
|
||||||
|
|
||||||
|
_onAskToJoin: () => boolean;
|
||||||
|
|
||||||
|
_onCancel: () => boolean;
|
||||||
|
|
||||||
|
_onChangeDisplayName: Object => void;
|
||||||
|
|
||||||
|
_onChangeEmail: Object => void;
|
||||||
|
|
||||||
|
_onChangePassword: Object => void;
|
||||||
|
|
||||||
|
_onEnableEdit: () => void;
|
||||||
|
|
||||||
|
_onSubmit: () => boolean;
|
||||||
|
|
||||||
|
_onSwitchToKnockMode: () => void;
|
||||||
|
|
||||||
|
_onSwitchToPasswordMode: () => void;
|
||||||
|
|
||||||
|
_renderContent: () => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the joining (waiting) fragment of the screen.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderJoining(withPassword) {
|
||||||
|
return (
|
||||||
|
<div className = 'joiningContainer'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
<span>
|
||||||
|
{ this.props.t(`lobby.${withPassword ? 'joinWithPasswordMessage' : 'joiningMessage'}`) }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant form to let the knocking participant enter its details.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderParticipantForm() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { displayName, email } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className = 'form'>
|
||||||
|
<span>
|
||||||
|
{ t('lobby.nameField') }
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
onChange = { this._onChangeDisplayName }
|
||||||
|
type = 'text'
|
||||||
|
value = { displayName } />
|
||||||
|
<span>
|
||||||
|
{ t('lobby.emailField') }
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
onChange = { this._onChangeEmail }
|
||||||
|
type = 'email'
|
||||||
|
value = { email } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the participant info fragment when we have all the required details of the user.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderParticipantInfo() {
|
||||||
|
const { displayName, email } = this.state;
|
||||||
|
const { _participantId } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className = 'participantInfo'>
|
||||||
|
<div className = 'editButton'>
|
||||||
|
<button
|
||||||
|
onClick = { this._onEnableEdit }
|
||||||
|
type = 'button'>
|
||||||
|
<Icon src = { IconEdit } />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Avatar
|
||||||
|
participantId = { _participantId }
|
||||||
|
size = { 64 } />
|
||||||
|
<span className = 'displayName'>
|
||||||
|
{ displayName }
|
||||||
|
</span>
|
||||||
|
<span className = 'email'>
|
||||||
|
{ email }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderPasswordForm() {
|
||||||
|
return (
|
||||||
|
<div className = 'form'>
|
||||||
|
<span>
|
||||||
|
{ this.props.t('lobby.passwordField') }
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
onChange = { this._onChangePassword }
|
||||||
|
type = 'password'
|
||||||
|
value = { this.state.password } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the password join button (set).
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderPasswordJoinButtons() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className = 'primary'
|
||||||
|
disabled = { !this.state.password }
|
||||||
|
onClick = { this._onAskToJoin }
|
||||||
|
type = 'submit'>
|
||||||
|
{ t('lobby.passwordJoinButton') }
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className = 'borderLess'
|
||||||
|
onClick = { this._onSwitchToKnockMode }
|
||||||
|
type = 'button'>
|
||||||
|
{ t('lobby.backToKnockModeButton') }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the standard button set.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
_renderStandardButtons() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className = 'primary'
|
||||||
|
disabled = { !this.state.displayName }
|
||||||
|
onClick = { this._onAskToJoin }
|
||||||
|
type = 'submit'>
|
||||||
|
{ t('lobby.knockButton') }
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className = 'borderLess'
|
||||||
|
onClick = { this._onSwitchToPasswordMode }
|
||||||
|
type = 'button'>
|
||||||
|
{ t('lobby.enterPasswordButton') }
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(LobbyScreen));
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
|
||||||
|
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
|
||||||
|
export { default as KnockingParticipantList } from './KnockingParticipantList';
|
||||||
|
export { default as LobbyScreen } from './LobbyScreen';
|
|
@ -0,0 +1,39 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a displayable name for the knocking participant.
|
||||||
|
*
|
||||||
|
* @param {string} name - The received name.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getKnockingParticipantDisplayName(name: string) {
|
||||||
|
if (name) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof interfaceConfig === 'object'
|
||||||
|
? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
|
||||||
|
: 'Fellow Jitster';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approves (lets in) or rejects a knocking participant.
|
||||||
|
*
|
||||||
|
* @param {Function} getState - Function to get the Redux state.
|
||||||
|
* @param {string} id - The id of the knocking participant.
|
||||||
|
* @param {boolean} approved - True if the participant is approved, false otherwise.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
|
||||||
|
const { conference } = getState()['features/base/conference'];
|
||||||
|
|
||||||
|
if (conference) {
|
||||||
|
if (approved) {
|
||||||
|
conference.lobbyApproveAccess(id);
|
||||||
|
} else {
|
||||||
|
conference.lobbyDenyAccess(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import './middleware';
|
||||||
|
import './reducer';
|
||||||
|
|
||||||
|
export * from './components';
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { getLogger } from '../base/logging/functions';
|
||||||
|
|
||||||
|
export default getLogger('features/lobby');
|
|
@ -0,0 +1,137 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
|
||||||
|
import { hideDialog } from '../base/dialog';
|
||||||
|
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||||
|
import { getFirstLoadableAvatarUrl } from '../base/participants';
|
||||||
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
|
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
|
||||||
|
|
||||||
|
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
|
||||||
|
import {
|
||||||
|
knockingParticipantLeft,
|
||||||
|
openLobbyScreen,
|
||||||
|
participantIsKnockingOrUpdated,
|
||||||
|
setLobbyModeEnabled
|
||||||
|
} from './actions';
|
||||||
|
import { LobbyScreen } from './components';
|
||||||
|
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONFERENCE_FAILED:
|
||||||
|
return _conferenceFailed(store, next, action);
|
||||||
|
case CONFERENCE_JOINED:
|
||||||
|
return _conferenceJoined(store, next, action);
|
||||||
|
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
|
||||||
|
// We need the full update result to be in the store already
|
||||||
|
const result = next(action);
|
||||||
|
|
||||||
|
_findLoadableAvatarForKnockingParticipant(store, action.participant);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a change handler for state['features/base/conference'].conference to
|
||||||
|
* set the event listeners needed for the lobby feature to operate.
|
||||||
|
*/
|
||||||
|
StateListenerRegistry.register(
|
||||||
|
state => state['features/base/conference'].conference,
|
||||||
|
(conference, { dispatch }, previousConference) => {
|
||||||
|
if (conference && !previousConference) {
|
||||||
|
conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
|
||||||
|
dispatch(setLobbyModeEnabled(enabled));
|
||||||
|
});
|
||||||
|
|
||||||
|
conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
|
||||||
|
dispatch(participantIsKnockingOrUpdated({
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {
|
||||||
|
dispatch(participantIsKnockingOrUpdated({
|
||||||
|
...participant,
|
||||||
|
id
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
|
||||||
|
dispatch(knockingParticipantLeft(id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to handle the conference failed event and navigate the user to the lobby screen
|
||||||
|
* based on the failure reason.
|
||||||
|
*
|
||||||
|
* @param {Object} store - The Redux store.
|
||||||
|
* @param {Function} next - The Redux next function.
|
||||||
|
* @param {Object} action - The Redux action.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function _conferenceFailed({ dispatch }, next, action) {
|
||||||
|
const { error } = action;
|
||||||
|
|
||||||
|
if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
|
||||||
|
if (typeof error.recoverable === 'undefined') {
|
||||||
|
error.recoverable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(openLobbyScreen());
|
||||||
|
} else {
|
||||||
|
dispatch(hideDialog(LobbyScreen));
|
||||||
|
|
||||||
|
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||||
|
dispatch(showNotification({
|
||||||
|
appearance: NOTIFICATION_TYPE.ERROR,
|
||||||
|
hideErrorSupportLink: true,
|
||||||
|
titleKey: 'lobby.joinRejectedMessage'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cleanup of lobby state when a conference is joined.
|
||||||
|
*
|
||||||
|
* @param {Object} store - The Redux store.
|
||||||
|
* @param {Function} next - The Redux next function.
|
||||||
|
* @param {Object} action - The Redux action.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function _conferenceJoined({ dispatch }, next, action) {
|
||||||
|
dispatch(hideDialog(LobbyScreen));
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the loadable avatar URL and updates the participant accordingly.
|
||||||
|
*
|
||||||
|
* @param {Object} store - The Redux store.
|
||||||
|
* @param {Object} participant - The knocking participant.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id }) {
|
||||||
|
const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
|
||||||
|
|
||||||
|
if (updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
||||||
|
getFirstLoadableAvatarUrl(updatedParticipant).then(loadableAvatarUrl => {
|
||||||
|
if (loadableAvatarUrl) {
|
||||||
|
dispatch(participantIsKnockingOrUpdated({
|
||||||
|
loadableAvatarUrl,
|
||||||
|
id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT } from '../base/conference';
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
|
||||||
|
KNOCKING_PARTICIPANT_LEFT,
|
||||||
|
SET_KNOCKING_STATE,
|
||||||
|
SET_LOBBY_MODE_ENABLED
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
knocking: false,
|
||||||
|
knockingParticipants: [],
|
||||||
|
lobbyEnabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces redux actions which affect the display of notifications.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The current redux state.
|
||||||
|
* @param {Object} action - The redux action to reduce.
|
||||||
|
* @returns {Object} The next redux state which is the result of reducing the
|
||||||
|
* specified {@code action}.
|
||||||
|
*/
|
||||||
|
ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONFERENCE_FAILED:
|
||||||
|
case CONFERENCE_JOINED:
|
||||||
|
case CONFERENCE_LEFT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
knocking: false
|
||||||
|
};
|
||||||
|
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
|
||||||
|
return _knockingParticipantArrivedOrUpdated(action.participant, state);
|
||||||
|
case KNOCKING_PARTICIPANT_LEFT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
knockingParticipants: state.knockingParticipants.filter(p => p.id !== action.id)
|
||||||
|
};
|
||||||
|
case SET_KNOCKING_STATE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
knocking: action.knocking
|
||||||
|
};
|
||||||
|
case SET_LOBBY_MODE_ENABLED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
lobbyEnabled: action.enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores or updates a knocking participant.
|
||||||
|
*
|
||||||
|
* @param {Object} participant - The arrived or updated knocking participant.
|
||||||
|
* @param {Object} state - The current Redux state of the feature.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function _knockingParticipantArrivedOrUpdated(participant, state) {
|
||||||
|
let existingParticipant = state.knockingParticipants.find(p => p.id === participant.id);
|
||||||
|
|
||||||
|
existingParticipant = {
|
||||||
|
...existingParticipant,
|
||||||
|
...participant
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
knockingParticipants: [
|
||||||
|
...state.knockingParticipants.filter(p => p.id !== participant.id),
|
||||||
|
existingParticipant
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,11 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||||
import { StateListenerRegistry } from '../base/redux';
|
import { StateListenerRegistry } from '../base/redux';
|
||||||
|
|
||||||
import { setFatalError } from './actions';
|
import { setFatalError } from './actions';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of errors that are not fatal (or handled differently) so then the overlays won't kick in.
|
||||||
|
*/
|
||||||
|
const NON_FATAR_ERRORS = [
|
||||||
|
JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
|
||||||
|
JitsiConferenceErrors.CONFERENCE_DESTROYED,
|
||||||
|
JitsiConferenceErrors.CONNECTION_ERROR
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State listener which emits the {@code fatalErrorOccurred} action which works
|
* State listener which emits the {@code fatalErrorOccurred} action which works
|
||||||
* as a catch all for critical errors which have not been claimed by any other
|
* as a catch all for critical errors which have not been claimed by any other
|
||||||
|
@ -21,6 +31,7 @@ StateListenerRegistry.register(
|
||||||
},
|
},
|
||||||
/* listener */ (error, { dispatch }) => {
|
/* listener */ (error, { dispatch }) => {
|
||||||
error
|
error
|
||||||
|
&& NON_FATAR_ERRORS.indexOf(error.name) === -1
|
||||||
&& typeof error.recoverable === 'undefined'
|
&& typeof error.recoverable === 'undefined'
|
||||||
&& dispatch(setFatalError(error));
|
&& dispatch(setFatalError(error));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { connect } from '../../../base/redux';
|
||||||
import { StyleType } from '../../../base/styles';
|
import { StyleType } from '../../../base/styles';
|
||||||
import { SharedDocumentButton } from '../../../etherpad';
|
import { SharedDocumentButton } from '../../../etherpad';
|
||||||
import { InviteButton } from '../../../invite';
|
import { InviteButton } from '../../../invite';
|
||||||
|
import { LobbyModeButton } from '../../../lobby';
|
||||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||||
import { RoomLockButton } from '../../../room-lock';
|
import { RoomLockButton } from '../../../room-lock';
|
||||||
|
@ -128,6 +129,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
<InviteButton { ...buttonProps } />
|
<InviteButton { ...buttonProps } />
|
||||||
<AudioOnlyButton { ...buttonProps } />
|
<AudioOnlyButton { ...buttonProps } />
|
||||||
<RaiseHandButton { ...buttonProps } />
|
<RaiseHandButton { ...buttonProps } />
|
||||||
|
<LobbyModeButton { ...buttonProps } />
|
||||||
<MoreOptionsButton { ...moreOptionsButtonProps } />
|
<MoreOptionsButton { ...moreOptionsButtonProps } />
|
||||||
<Collapsible collapsed = { !showMore }>
|
<Collapsible collapsed = { !showMore }>
|
||||||
<ToggleCameraButton { ...buttonProps } />
|
<ToggleCameraButton { ...buttonProps } />
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { SharedDocumentButton } from '../../../etherpad';
|
||||||
import { openFeedbackDialog } from '../../../feedback';
|
import { openFeedbackDialog } from '../../../feedback';
|
||||||
import { beginAddPeople } from '../../../invite';
|
import { beginAddPeople } from '../../../invite';
|
||||||
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
|
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
|
||||||
|
import { LobbyModeButton } from '../../../lobby';
|
||||||
import {
|
import {
|
||||||
LocalRecordingButton,
|
LocalRecordingButton,
|
||||||
LocalRecordingInfoDialog
|
LocalRecordingInfoDialog
|
||||||
|
@ -1188,6 +1189,9 @@ class Toolbox extends Component<Props, State> {
|
||||||
if (this._shouldShowButton('closedcaptions')) {
|
if (this._shouldShowButton('closedcaptions')) {
|
||||||
buttonsLeft.push('closedcaptions');
|
buttonsLeft.push('closedcaptions');
|
||||||
}
|
}
|
||||||
|
if (this._shouldShowButton('lobby')) {
|
||||||
|
buttonsRight.push('lobby');
|
||||||
|
}
|
||||||
if (overflowHasItems) {
|
if (overflowHasItems) {
|
||||||
buttonsRight.push('overflowmenu');
|
buttonsRight.push('overflowmenu');
|
||||||
}
|
}
|
||||||
|
@ -1271,6 +1275,8 @@ class Toolbox extends Component<Props, State> {
|
||||||
{ this._renderVideoButton() }
|
{ this._renderVideoButton() }
|
||||||
</div>
|
</div>
|
||||||
<div className = 'button-group-right'>
|
<div className = 'button-group-right'>
|
||||||
|
{ (buttonsRight.indexOf('lobby') !== -1)
|
||||||
|
&& <LobbyModeButton /> }
|
||||||
{ buttonsRight.indexOf('localrecording') !== -1
|
{ buttonsRight.indexOf('localrecording') !== -1
|
||||||
&& <LocalRecordingButton
|
&& <LocalRecordingButton
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
Loading…
Reference in New Issue