mirror of https://github.com/jitsi/jitsi-meet
The lobby feature adds the possibility to lock a meeting and only allow people in after virtually knocking and going through formal approvalpull/6993/head
parent
338c960215
commit
475a2ae596
@ -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; |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 287 B |
After Width: | Height: | Size: 224 B |
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'; |
||||
|
@ -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 |
||||
] |
||||
}; |
||||
} |
Loading…
Reference in new issue