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);
|
||||
|
||||
switch (err) {
|
||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||
const [ msg ] = params;
|
||||
|
||||
APP.UI.notifyConnectionFailed(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
||||
// let's show some auth not allowed page
|
||||
|
@ -336,14 +330,6 @@ class ConferenceConnector {
|
|||
APP.UI.notifyGracefulShutdown();
|
||||
break;
|
||||
|
||||
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
|
||||
const [ reason ] = params;
|
||||
|
||||
APP.UI.hideStats();
|
||||
APP.UI.notifyConferenceDestroyed(reason);
|
||||
break;
|
||||
}
|
||||
|
||||
// FIXME FOCUS_DISCONNECTED is a confusing event name.
|
||||
// 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
|
||||
|
|
|
@ -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_overrides';
|
||||
@import 'labels';
|
||||
@import 'lobby';
|
||||
@import 'unsupported-browser/main';
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'deep-linking/main';
|
||||
|
|
|
@ -48,7 +48,7 @@ var interfaceConfig = {
|
|||
*/
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'fodeviceselection', 'hangup', 'lobby', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
||||
|
|
|
@ -675,6 +675,7 @@
|
|||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"kick": "Kick participant",
|
||||
"lobbyButton": "Enable/disable lobby mode",
|
||||
"localRecording": "Toggle local recording controls",
|
||||
"lockRoom": "Toggle meeting password",
|
||||
"moreActions": "Toggle more actions menu",
|
||||
|
@ -722,6 +723,8 @@
|
|||
"hangup": "Leave",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"lobbyButtonDisable": "Disable lobby mode",
|
||||
"lobbyButtonEnable": "Enable lobby mode",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"lowerYourHand": "Lower your hand",
|
||||
|
@ -861,5 +864,29 @@
|
|||
},
|
||||
"helpView": {
|
||||
"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.
|
||||
* @param {string} id user id
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { showNotification } from '../notifications';
|
||||
import { clearNotifications, showNotification } from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import {
|
||||
|
@ -79,6 +79,10 @@ export function appNavigate(uri: ?string) {
|
|||
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));
|
||||
|
||||
let protocol = location.protocol.toLowerCase();
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../../base/lastn'; // Register lastN middleware
|
|||
import { toURLString } from '../../base/util';
|
||||
import '../../follow-me';
|
||||
import { OverlayContainer } from '../../overlay';
|
||||
import '../../lobby'; // Import lobby function
|
||||
import '../../rejoin'; // Enable rejoin analytics
|
||||
import { appNavigate } from '../actions';
|
||||
import { getDefaultURL } from '../functions';
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
|
||||
initialsText: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
color: 'white',
|
||||
fontSize: size * 0.45,
|
||||
fontWeight: '100'
|
||||
};
|
||||
|
|
|
@ -256,7 +256,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
|
|||
* }}
|
||||
* @public
|
||||
*/
|
||||
export function conferenceFailed(conference: Object, error: string) {
|
||||
export function conferenceFailed(conference: Object, error: string, ...params: any) {
|
||||
return {
|
||||
type: CONFERENCE_FAILED,
|
||||
conference,
|
||||
|
@ -265,6 +265,7 @@ export function conferenceFailed(conference: Object, error: string) {
|
|||
// jitsi-meet needs it).
|
||||
error: {
|
||||
name: error,
|
||||
params,
|
||||
recoverable: undefined
|
||||
}
|
||||
};
|
||||
|
|
|
@ -203,7 +203,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
|
|||
* @returns {JitsiConference|undefined}
|
||||
*/
|
||||
export function getCurrentConference(stateful: Function | Object) {
|
||||
const { conference, joining, leaving, passwordRequired }
|
||||
const { conference, joining, leaving, membersOnly, passwordRequired }
|
||||
= toState(stateful)['features/base/conference'];
|
||||
|
||||
// There is a precendence
|
||||
|
@ -211,7 +211,7 @@ export function getCurrentConference(stateful: Function | Object) {
|
|||
return conference === leaving ? undefined : conference;
|
||||
}
|
||||
|
||||
return joining || passwordRequired;
|
||||
return joining || passwordRequired || membersOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
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 { MEDIA_TYPE } from '../media';
|
||||
import {
|
||||
|
@ -140,15 +141,43 @@ StateListenerRegistry.register(
|
|||
* @private
|
||||
* @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 { conference, error } = action;
|
||||
|
||||
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
|
||||
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
|
||||
// conference is handled by /conference.js and appropriate failure handlers
|
||||
// are set there.
|
||||
|
|
|
@ -36,6 +36,7 @@ const DEFAULT_STATE = {
|
|||
leaving: undefined,
|
||||
locked: undefined,
|
||||
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
|
||||
membersOnly: undefined,
|
||||
password: undefined,
|
||||
passwordRequired: undefined,
|
||||
preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
|
||||
|
@ -161,6 +162,7 @@ function _conferenceFailed(state, { conference, error }) {
|
|||
}
|
||||
|
||||
let authRequired;
|
||||
let membersOnly;
|
||||
let passwordRequired;
|
||||
|
||||
switch (error.name) {
|
||||
|
@ -168,6 +170,11 @@ function _conferenceFailed(state, { conference, error }) {
|
|||
authRequired = conference;
|
||||
break;
|
||||
|
||||
case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
|
||||
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR:
|
||||
membersOnly = conference;
|
||||
break;
|
||||
|
||||
case JitsiConferenceErrors.PASSWORD_REQUIRED:
|
||||
passwordRequired = conference;
|
||||
break;
|
||||
|
@ -189,6 +196,7 @@ function _conferenceFailed(state, { conference, error }) {
|
|||
* @type {string}
|
||||
*/
|
||||
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
|
||||
membersOnly,
|
||||
password: undefined,
|
||||
|
||||
/**
|
||||
|
@ -232,6 +240,7 @@ function _conferenceJoined(state, { conference }) {
|
|||
e2eeSupported: conference.isE2EESupported(),
|
||||
|
||||
joining: undefined,
|
||||
membersOnly: undefined,
|
||||
leaving: undefined,
|
||||
|
||||
/**
|
||||
|
|
|
@ -119,7 +119,7 @@ export function connect(id: ?string, password: ?string) {
|
|||
*/
|
||||
function _onConnectionDisconnected(message: string) {
|
||||
unsubscribe();
|
||||
dispatch(_connectionDisconnected(connection, message));
|
||||
dispatch(connectionDisconnected(connection, message));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -195,7 +195,7 @@ export function connect(id: ?string, password: ?string) {
|
|||
* message: string
|
||||
* }}
|
||||
*/
|
||||
function _connectionDisconnected(connection: Object, message: string) {
|
||||
export function connectionDisconnected(connection: Object, message: string) {
|
||||
return {
|
||||
type: CONNECTION_DISCONNECTED,
|
||||
connection,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { configureInitialDevices } from '../devices';
|
|||
import { getBackendSafeRoomName } from '../util';
|
||||
|
||||
export {
|
||||
connectionDisconnected,
|
||||
connectionEstablished,
|
||||
connectionFailed,
|
||||
setLocationURL
|
||||
|
|
|
@ -57,14 +57,13 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
|
|||
<KeyboardAvoidingView
|
||||
behavior = 'height'
|
||||
style = { [
|
||||
styles.overlay,
|
||||
style
|
||||
styles.overlay
|
||||
] }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { [
|
||||
_dialogStyles.dialog,
|
||||
this.props.style
|
||||
style
|
||||
] }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
|
|
|
@ -34,7 +34,7 @@ class BaseSubmitDialog<P: Props, S: *> extends BaseDialog<P, S> {
|
|||
* @returns {string}
|
||||
*/
|
||||
_getSubmitButtonKey() {
|
||||
return 'dialog.Ok';
|
||||
return this.props.okKey || 'dialog.Ok';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,11 @@ import StatelessDialog from './StatelessDialog';
|
|||
*/
|
||||
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
|
||||
* leave the dialog open. No cancel button.
|
||||
|
|
|
@ -33,6 +33,11 @@ type Props = {
|
|||
*/
|
||||
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
|
||||
* by default.
|
||||
|
@ -313,7 +318,7 @@ class StatelessDialog extends Component<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === 'Enter' && !this.props.disableEnter) {
|
||||
event.preventDefault();
|
||||
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 IconE2EE } from './e2ee.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 IconExclamation } from './exclamation.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 IconLiveStreaming } from './public.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 IconMenuDown } from './menu-down.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 './functions';
|
||||
|
||||
export { default as Platform } from './Platform';
|
||||
export * from './Types';
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../../../filmstrip';
|
||||
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { KnockingParticipantList } from '../../../lobby';
|
||||
import { BackButtonRegistry } from '../../../mobile/back-button';
|
||||
import { Captions } from '../../../subtitles';
|
||||
import { isToolboxVisible, setToolboxVisible, Toolbox } from '../../../toolbox';
|
||||
|
@ -320,6 +321,7 @@ class Conference extends AbstractConference<Props, *> {
|
|||
style = { styles.navBarSafeView }>
|
||||
<NavigationBar />
|
||||
{ this._renderNotificationsContainer() }
|
||||
<KnockingParticipantList />
|
||||
</SafeAreaView>
|
||||
|
||||
<TestConnectionInfo />
|
||||
|
@ -414,6 +416,7 @@ function _mapStateToProps(state) {
|
|||
const {
|
||||
conference,
|
||||
joining,
|
||||
membersOnly,
|
||||
leaving
|
||||
} = state['features/base/conference'];
|
||||
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
|
||||
// are leaving one.
|
||||
const connecting_
|
||||
= connecting || (connection && (joining || (!conference && !leaving)));
|
||||
= connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))));
|
||||
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Chat } from '../../../chat';
|
|||
import { Filmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { KnockingParticipantList } from '../../../lobby';
|
||||
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
||||
import {
|
||||
Toolbox,
|
||||
|
@ -198,8 +199,8 @@ class Conference extends AbstractConference<Props, *> {
|
|||
<InviteMore />
|
||||
<div id = 'videospace'>
|
||||
<LargeVideo />
|
||||
{ hideLabels
|
||||
|| <Labels /> }
|
||||
<KnockingParticipantList />
|
||||
{ hideLabels || <Labels /> }
|
||||
<Filmstrip filmstripOnly = { filmstripOnly } />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, prevConference) => {
|
||||
const { authRequired, passwordRequired }
|
||||
const { authRequired, membersOnly, passwordRequired }
|
||||
= getState()['features/base/conference'];
|
||||
|
||||
if (conference !== prevConference) {
|
||||
|
@ -80,6 +80,7 @@ StateListenerRegistry.register(
|
|||
// and explicitly check.
|
||||
if (typeof authRequired === 'undefined'
|
||||
&& typeof passwordRequired === 'undefined'
|
||||
&& typeof membersOnly === 'undefined'
|
||||
&& !isDialogOpen(getState(), FeedbackDialog)) {
|
||||
// Conference changed, left or failed... and there is no
|
||||
// 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
|
||||
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { setFatalError } from './actions';
|
||||
|
||||
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
|
||||
* as a catch all for critical errors which have not been claimed by any other
|
||||
|
@ -21,6 +31,7 @@ StateListenerRegistry.register(
|
|||
},
|
||||
/* listener */ (error, { dispatch }) => {
|
||||
error
|
||||
&& NON_FATAR_ERRORS.indexOf(error.name) === -1
|
||||
&& typeof error.recoverable === 'undefined'
|
||||
&& dispatch(setFatalError(error));
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { connect } from '../../../base/redux';
|
|||
import { StyleType } from '../../../base/styles';
|
||||
import { SharedDocumentButton } from '../../../etherpad';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { LobbyModeButton } from '../../../lobby';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||
import { RoomLockButton } from '../../../room-lock';
|
||||
|
@ -128,6 +129,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
<InviteButton { ...buttonProps } />
|
||||
<AudioOnlyButton { ...buttonProps } />
|
||||
<RaiseHandButton { ...buttonProps } />
|
||||
<LobbyModeButton { ...buttonProps } />
|
||||
<MoreOptionsButton { ...moreOptionsButtonProps } />
|
||||
<Collapsible collapsed = { !showMore }>
|
||||
<ToggleCameraButton { ...buttonProps } />
|
||||
|
|
|
@ -38,6 +38,7 @@ import { SharedDocumentButton } from '../../../etherpad';
|
|||
import { openFeedbackDialog } from '../../../feedback';
|
||||
import { beginAddPeople } from '../../../invite';
|
||||
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
|
||||
import { LobbyModeButton } from '../../../lobby';
|
||||
import {
|
||||
LocalRecordingButton,
|
||||
LocalRecordingInfoDialog
|
||||
|
@ -1188,6 +1189,9 @@ class Toolbox extends Component<Props, State> {
|
|||
if (this._shouldShowButton('closedcaptions')) {
|
||||
buttonsLeft.push('closedcaptions');
|
||||
}
|
||||
if (this._shouldShowButton('lobby')) {
|
||||
buttonsRight.push('lobby');
|
||||
}
|
||||
if (overflowHasItems) {
|
||||
buttonsRight.push('overflowmenu');
|
||||
}
|
||||
|
@ -1271,6 +1275,8 @@ class Toolbox extends Component<Props, State> {
|
|||
{ this._renderVideoButton() }
|
||||
</div>
|
||||
<div className = 'button-group-right'>
|
||||
{ (buttonsRight.indexOf('lobby') !== -1)
|
||||
&& <LobbyModeButton /> }
|
||||
{ buttonsRight.indexOf('localrecording') !== -1
|
||||
&& <LocalRecordingButton
|
||||
onClick = {
|
||||
|
|
Loading…
Reference in New Issue