This commit is contained in:
39
react/features/lobby/actionTypes.ts
Normal file
39
react/features/lobby/actionTypes.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Action type to set the lobby visibility.
|
||||
*/
|
||||
export const SET_LOBBY_VISIBILITY = 'TOGGLE_LOBBY_VISIBILITY';
|
||||
|
||||
/**
|
||||
* Action type to set the password join failed status.
|
||||
*/
|
||||
export const SET_PASSWORD_JOIN_FAILED = 'SET_PASSWORD_JOIN_FAILED';
|
||||
|
||||
/**
|
||||
* Action type to set a lobby chat participant's state to chatting
|
||||
*/
|
||||
export const SET_LOBBY_PARTICIPANT_CHAT_STATE = 'SET_LOBBY_PARTICIPANT_CHAT_STATE';
|
||||
|
||||
/**
|
||||
* Action type to remove chattingWithModerator field
|
||||
*/
|
||||
export const REMOVE_LOBBY_CHAT_WITH_MODERATOR = 'REMOVE_LOBBY_CHAT_WITH_MODERATOR';
|
||||
429
react/features/lobby/actions.any.ts
Normal file
429
react/features/lobby/actions.any.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { conferenceWillJoin, setPassword } from '../base/conference/actions';
|
||||
import { getCurrentConference, sendLocalParticipant } from '../base/conference/functions';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { onLobbyChatInitialized, removeLobbyChatParticipant, sendMessage } from '../chat/actions.any';
|
||||
import { LOBBY_CHAT_MESSAGE } from '../chat/constants';
|
||||
import { handleLobbyMessageReceived } from '../chat/middleware';
|
||||
import { hideNotification, showNotification } from '../notifications/actions';
|
||||
import { LOBBY_NOTIFICATION_ID } from '../notifications/constants';
|
||||
import { joinConference } from '../prejoin/actions';
|
||||
|
||||
import {
|
||||
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
|
||||
KNOCKING_PARTICIPANT_LEFT,
|
||||
REMOVE_LOBBY_CHAT_WITH_MODERATOR,
|
||||
SET_KNOCKING_STATE,
|
||||
SET_LOBBY_MODE_ENABLED,
|
||||
SET_LOBBY_PARTICIPANT_CHAT_STATE,
|
||||
SET_LOBBY_VISIBILITY,
|
||||
SET_PASSWORD_JOIN_FAILED
|
||||
} from './actionTypes';
|
||||
import { LOBBY_CHAT_INITIALIZED, MODERATOR_IN_CHAT_WITH_LEFT } from './constants';
|
||||
import { getKnockingParticipants, getLobbyConfig, getLobbyEnabled, isEnablingLobbyAllowed } from './functions';
|
||||
import logger from './logger';
|
||||
import { IKnockingParticipant } from './types';
|
||||
|
||||
/**
|
||||
* Tries to join with a preset password.
|
||||
*
|
||||
* @param {string} password - The password to join with.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinWithPassword(password: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
dispatch(setPassword(conference, conference?.join, password));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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: IKnockingParticipant | Object) {
|
||||
return {
|
||||
participant,
|
||||
type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a knocking participant and dismisses the notification.
|
||||
*
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @param {boolean} approved - True if the participant is approved, false otherwise.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function answerKnockingParticipant(id: string, approved: boolean) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(setKnockingParticipantApproval(id, approved));
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference) {
|
||||
if (approved) {
|
||||
conference.lobbyApproveAccess(id);
|
||||
} else {
|
||||
conference.lobbyDenyAccess(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to admit multiple participants in the conference.
|
||||
*
|
||||
* @param {Array<Object>} participants - A list of knocking participants.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function admitMultiple(participants: Array<IKnockingParticipant>) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
conference?.lobbyApproveAccess(participants.map(p => p.id));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves the request of a knocking participant to join the meeting.
|
||||
*
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function approveKnockingParticipant(id: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
conference?.lobbyApproveAccess(id);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies the request of a knocking participant to join the meeting.
|
||||
*
|
||||
* @param {string} id - The id of the knocking participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function rejectKnockingParticipant(id: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
conference?.lobbyDenyAccess(id);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 be dispatched when we failed to join with a password.
|
||||
*
|
||||
* @param {boolean} failed - True of recent password join failed.
|
||||
* @returns {{
|
||||
* failed: boolean,
|
||||
* type: SET_PASSWORD_JOIN_FAILED
|
||||
* }}
|
||||
*/
|
||||
export function setPasswordJoinFailed(failed: boolean) {
|
||||
return {
|
||||
failed,
|
||||
type: SET_PASSWORD_JOIN_FAILED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts knocking and waiting for approval.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function startKnocking() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { membersOnly } = state['features/base/conference'];
|
||||
|
||||
logger.info(`Lobby starting knocking (membersOnly = ${membersOnly})`);
|
||||
|
||||
if (!membersOnly) {
|
||||
// let's hide the notification (the case with denied access and retrying)
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
|
||||
// no membersOnly, this means we got lobby screen shown as someone
|
||||
// tried to join a conference that has lobby enabled without setting display name
|
||||
// join conference should trigger the lobby/member_only path after setting the display name
|
||||
// this is possible only for web, where we can join without a prejoin screen
|
||||
dispatch(joinConference());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
dispatch(conferenceWillJoin(membersOnly));
|
||||
|
||||
// We need to update the conference object with the current display name, if approved
|
||||
// we want to send that display name, it was not updated in case when pre-join is disabled
|
||||
sendLocalParticipant(state, membersOnly);
|
||||
|
||||
membersOnly?.joinLobby(localParticipant?.name, localParticipant?.email);
|
||||
dispatch(setLobbyMessageListener());
|
||||
dispatch(setKnockingState(true));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle lobby mode on or off.
|
||||
*
|
||||
* @param {boolean} enabled - The desired (new) state of the lobby mode.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleLobbyMode(enabled: boolean) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (enabled) {
|
||||
if (isEnablingLobbyAllowed(getState())) {
|
||||
conference?.enableLobby();
|
||||
} else {
|
||||
logger.info('Ignoring enable lobby request because there are visitors in the call already.');
|
||||
}
|
||||
} else {
|
||||
conference?.disableLobby();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to open the lobby screen.
|
||||
*
|
||||
* @returns {openDialog}
|
||||
*/
|
||||
export function openLobbyScreen() {
|
||||
return {
|
||||
type: SET_LOBBY_VISIBILITY,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to hide the lobby screen.
|
||||
*
|
||||
* @returns {hideDialog}
|
||||
*/
|
||||
export function hideLobbyScreen() {
|
||||
return {
|
||||
type: SET_LOBBY_VISIBILITY,
|
||||
visible: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to handle chat initialized in the lobby room.
|
||||
*
|
||||
* @param {Object} payload - The payload received,
|
||||
* contains the information about the two participants
|
||||
* that will chat with each other in the lobby room.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function handleLobbyChatInitialized(payload: { attendee: IParticipant; moderator: IParticipant; }) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
const id = conference?.myLobbyUserId();
|
||||
|
||||
dispatch({
|
||||
type: SET_LOBBY_PARTICIPANT_CHAT_STATE,
|
||||
participant: payload.attendee,
|
||||
moderator: payload.moderator
|
||||
});
|
||||
|
||||
dispatch(onLobbyChatInitialized(payload));
|
||||
|
||||
const attendeeIsKnocking = getKnockingParticipants(state).some(p => p.id === payload.attendee.id);
|
||||
|
||||
if (attendeeIsKnocking && conference?.getRole() === 'moderator' && payload.moderator.id !== id) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'lobby.lobbyChatStartedNotification',
|
||||
titleArguments: {
|
||||
moderator: payload.moderator.name ?? '',
|
||||
attendee: payload.attendee.name ?? ''
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to send message to the moderator.
|
||||
*
|
||||
* @param {string} message - The message to be sent.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function onSendMessage(message: string) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(sendMessage(message));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to send lobby message to every participant. Only allowed for moderators.
|
||||
*
|
||||
* @param {Object} message - The message to be sent.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function sendLobbyChatMessage(message: Object) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
conference?.sendLobbyMessage(message);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lobby listeners if lobby has been enabled.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function maybeSetLobbyChatMessageListener() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const lobbyEnabled = getLobbyEnabled(state);
|
||||
|
||||
if (lobbyEnabled) {
|
||||
dispatch(setLobbyMessageListener());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to handle the event when a moderator leaves during lobby chat.
|
||||
*
|
||||
* @param {string} participantId - The participant id of the moderator who left.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function updateLobbyParticipantOnLeave(participantId: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { knocking, knockingParticipants } = state['features/lobby'];
|
||||
const { lobbyMessageRecipient } = state['features/chat'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (knocking && lobbyMessageRecipient && lobbyMessageRecipient.id === participantId) {
|
||||
return dispatch(removeLobbyChatParticipant(true));
|
||||
}
|
||||
|
||||
if (!knocking) {
|
||||
// inform knocking participant when their moderator leaves
|
||||
const participantToNotify = knockingParticipants.find(p => p.chattingWithModerator === participantId);
|
||||
|
||||
if (participantToNotify) {
|
||||
conference?.sendLobbyMessage({
|
||||
type: MODERATOR_IN_CHAT_WITH_LEFT,
|
||||
moderatorId: participantToNotify.chattingWithModerator
|
||||
}, participantToNotify.id);
|
||||
}
|
||||
dispatch({
|
||||
type: REMOVE_LOBBY_CHAT_WITH_MODERATOR,
|
||||
moderatorId: participantId
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all messages received in the lobby room.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setLobbyMessageListener() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { enableChat = true } = getLobbyConfig(state);
|
||||
|
||||
if (!enableChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
conference?.addLobbyMessageListener((message: any, participantId: string) => {
|
||||
if (message.type === LOBBY_CHAT_MESSAGE) {
|
||||
return dispatch(handleLobbyMessageReceived(message.message, participantId));
|
||||
}
|
||||
if (message.type === LOBBY_CHAT_INITIALIZED) {
|
||||
return dispatch(handleLobbyChatInitialized(message));
|
||||
}
|
||||
if (message.type === MODERATOR_IN_CHAT_WITH_LEFT) {
|
||||
return dispatch(updateLobbyParticipantOnLeave(message.moderatorId));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
23
react/features/lobby/actions.native.ts
Normal file
23
react/features/lobby/actions.native.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../app/actions.native';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
import { hideLobbyScreen, setKnockingState } from './actions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Cancels the ongoing knocking and abandons the join flow.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function cancelKnocking() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
batch(() => {
|
||||
dispatch(setKnockingState(false));
|
||||
dispatch(hideLobbyScreen());
|
||||
dispatch(appNavigate(undefined));
|
||||
});
|
||||
};
|
||||
}
|
||||
18
react/features/lobby/actions.web.ts
Normal file
18
react/features/lobby/actions.web.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { maybeRedirectToWelcomePage } from '../app/actions.web';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Cancels the ongoing knocking and abandons the join flow.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function cancelKnocking() {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
// when we are redirecting the library should handle any
|
||||
// unload and clean of the connection.
|
||||
APP.API.notifyReadyToClose();
|
||||
dispatch(maybeRedirectToWelcomePage());
|
||||
};
|
||||
}
|
||||
467
react/features/lobby/components/AbstractLobbyScreen.tsx
Normal file
467
react/features/lobby/components/AbstractLobbyScreen.tsx
Normal file
@@ -0,0 +1,467 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { conferenceWillJoin } from '../../base/conference/actions';
|
||||
import { getConferenceName } from '../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { getSecurityUiConfig } from '../../base/config/functions.any';
|
||||
import { INVITE_ENABLED } from '../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../base/flags/functions';
|
||||
import { getLocalParticipant } from '../../base/participants/functions';
|
||||
import { getFieldValue } from '../../base/react/functions';
|
||||
import { updateSettings } from '../../base/settings/actions';
|
||||
import { IMessage } from '../../chat/types';
|
||||
import { isDeviceStatusVisible } from '../../prejoin/functions';
|
||||
import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions';
|
||||
|
||||
export const SCREEN_STATES = {
|
||||
EDIT: 1,
|
||||
PASSWORD: 2,
|
||||
VIEW: 3
|
||||
};
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* Indicates whether the device status should be visible.
|
||||
*/
|
||||
_deviceStatusVisible: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the message that display name is required is shown.
|
||||
*/
|
||||
_isDisplayNameRequiredActive: boolean;
|
||||
|
||||
/**
|
||||
* True if moderator initiated a chat session with the participant.
|
||||
*/
|
||||
_isLobbyChatActive: boolean;
|
||||
|
||||
/**
|
||||
* True if knocking is already happening, so we're waiting for a response.
|
||||
*/
|
||||
_knocking: boolean;
|
||||
|
||||
/**
|
||||
* Lobby messages between moderator and the participant.
|
||||
*/
|
||||
_lobbyChatMessages: IMessage[];
|
||||
|
||||
/**
|
||||
* Name of the lobby chat recipient.
|
||||
*/
|
||||
_lobbyMessageRecipient?: string;
|
||||
|
||||
/**
|
||||
* The name of the meeting we're about to join.
|
||||
*/
|
||||
_meetingName: string;
|
||||
|
||||
/**
|
||||
* The members only conference if any,.
|
||||
*/
|
||||
_membersOnlyConference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* True if a recent attempt to join with password failed.
|
||||
*/
|
||||
_passwordJoinFailed: boolean;
|
||||
|
||||
/**
|
||||
* True if the password field should be available for lobby participants.
|
||||
*/
|
||||
_renderPassword: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Indicates whether the copy url button should be shown.
|
||||
*/
|
||||
showCopyUrlButton: boolean;
|
||||
|
||||
/**
|
||||
* Function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The display name value entered into the field.
|
||||
*/
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* The email value entered into the field.
|
||||
*/
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* True if lobby chat widget is open.
|
||||
*/
|
||||
isChatOpen: boolean;
|
||||
|
||||
/**
|
||||
* The password value entered into the field.
|
||||
*/
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* True if a recent attempt to join with password failed.
|
||||
*/
|
||||
passwordJoinFailed: boolean;
|
||||
|
||||
/**
|
||||
* 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<P extends IProps = IProps> extends PureComponent<P, IState> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
displayName: props._participantName || '',
|
||||
email: props._participantEmail || '',
|
||||
isChatOpen: true,
|
||||
password: '',
|
||||
passwordJoinFailed: false,
|
||||
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._onJoinWithPassword = this._onJoinWithPassword.bind(this);
|
||||
this._onSendMessage = this._onSendMessage.bind(this);
|
||||
this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
|
||||
this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
|
||||
this._onToggleChat = this._onToggleChat.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent.getDerivedStateFromProps}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: IProps, state: IState) {
|
||||
if (props._passwordJoinFailed && !state.passwordJoinFailed) {
|
||||
return {
|
||||
password: '',
|
||||
passwordJoinFailed: true
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen title.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getScreenTitleKey() {
|
||||
const { screenState } = this.state;
|
||||
const passwordPrompt = screenState === SCREEN_STATES.PASSWORD;
|
||||
|
||||
return !passwordPrompt && this.props._knocking
|
||||
? this.props._isLobbyChatActive ? 'lobby.lobbyChatStartedTitle' : 'lobby.joiningTitle'
|
||||
: passwordPrompt ? 'lobby.enterPasswordTitle' : 'lobby.joinTitle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user submits the joining request.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAskToJoin() {
|
||||
this.setState({
|
||||
password: ''
|
||||
});
|
||||
|
||||
this.props.dispatch(startKnocking());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user cancels the dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.dispatch(cancelKnocking());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user changes its display name.
|
||||
*
|
||||
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeDisplayName(event: { target: { value: string; }; } | string) {
|
||||
const displayName = getFieldValue(event);
|
||||
|
||||
this.setState({
|
||||
displayName
|
||||
}, () => {
|
||||
this.props.dispatch(updateSettings({
|
||||
displayName
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user changes its email.
|
||||
*
|
||||
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeEmail(event: { target: { value: string; }; } | string) {
|
||||
const email = getFieldValue(event);
|
||||
|
||||
this.setState({
|
||||
email
|
||||
}, () => {
|
||||
this.props.dispatch(updateSettings({
|
||||
email
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user changes the password.
|
||||
*
|
||||
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangePassword(event: { target: { value: string; }; } | string) {
|
||||
this.setState({
|
||||
password: getFieldValue(event)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked for the edit button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onEnableEdit() {
|
||||
this.setState({
|
||||
screenState: SCREEN_STATES.EDIT
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user tries to join using a preset password.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onJoinWithPassword() {
|
||||
this.setState({
|
||||
passwordJoinFailed: false
|
||||
});
|
||||
this.props.dispatch(joinWithPassword(this.state.password));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked for sending lobby chat messages.
|
||||
*
|
||||
* @param {string} message - Message to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSendMessage(message: string) {
|
||||
this.props.dispatch(onSendMessage(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked for the enter (go back to) knocking mode button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSwitchToKnockMode() {
|
||||
this.setState({
|
||||
password: '',
|
||||
screenState: this.state.displayName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
|
||||
});
|
||||
this.props.dispatch(setPasswordJoinFailed(false));
|
||||
|
||||
// let's return to the correct state after password failed
|
||||
this.props.dispatch(conferenceWillJoin(this.props._membersOnlyConference));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked for the enter password button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSwitchToPasswordMode() {
|
||||
this.setState({
|
||||
screenState: SCREEN_STATES.PASSWORD
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked for toggling lobby chat visibility.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleChat() {
|
||||
this.setState(_prevState => {
|
||||
return {
|
||||
isChatOpen: !_prevState.isChatOpen
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of the dialog.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderContent() {
|
||||
const { _knocking } = this.props;
|
||||
const { screenState } = this.state;
|
||||
|
||||
if (screenState !== SCREEN_STATES.PASSWORD && _knocking) {
|
||||
return this._renderJoining();
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderJoining() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant form to let the knocking participant enter its details.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderParticipantForm() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant info fragment when we have all the required details of the user.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderParticipantInfo() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderPasswordForm() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password join button (set).
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderPasswordJoinButtons() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the standard (pre-knocking) button set.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderStandardButtons() {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const participantId = localParticipant?.id;
|
||||
const inviteEnabledFlag = getFeatureFlag(state, INVITE_ENABLED, true);
|
||||
const { disableInviteFunctions } = state['features/base/config'];
|
||||
const { isDisplayNameRequiredError, knocking, passwordJoinFailed } = state['features/lobby'];
|
||||
const { iAmSipGateway } = state['features/base/config'];
|
||||
const { disableLobbyPassword } = getSecurityUiConfig(state);
|
||||
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
|
||||
const deviceStatusVisible = isDeviceStatusVisible(state);
|
||||
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
|
||||
const { isLobbyChatActive, lobbyMessageRecipient, messages } = state['features/chat'];
|
||||
|
||||
return {
|
||||
_deviceStatusVisible: deviceStatusVisible,
|
||||
_isDisplayNameRequiredActive: Boolean(isDisplayNameRequiredError),
|
||||
_knocking: knocking,
|
||||
_lobbyChatMessages: messages,
|
||||
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_meetingName: getConferenceName(state),
|
||||
_membersOnlyConference: membersOnly,
|
||||
_participantEmail: localParticipant?.email,
|
||||
_participantId: participantId,
|
||||
_participantName: localParticipant?.name,
|
||||
_passwordJoinFailed: passwordJoinFailed,
|
||||
_renderPassword: !iAmSipGateway && !disableLobbyPassword && !lobbyWaitingForHost,
|
||||
showCopyUrlButton
|
||||
};
|
||||
}
|
||||
46
react/features/lobby/components/native/LobbyChatScreen.tsx
Normal file
46
react/features/lobby/components/native/LobbyChatScreen.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import ChatInputBar from '../../../chat/components/native/ChatInputBar';
|
||||
import MessageContainer from '../../../chat/components/native/MessageContainer';
|
||||
import AbstractLobbyScreen, {
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as abstractMapStateToProps
|
||||
} from '../AbstractLobbyScreen';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Implements a chat screen that appears when communication is started
|
||||
* between the moderator and the participant being in the lobby.
|
||||
*/
|
||||
class LobbyChatScreen extends
|
||||
AbstractLobbyScreen<AbstractProps> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _lobbyChatMessages } = this.props;
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
hasBottomTextInput = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { styles.lobbyChatWrapper }>
|
||||
{/* @ts-ignore */}
|
||||
<MessageContainer messages = { _lobbyChatMessages } />
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
_onSendMessage: () => void;
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(LobbyChatScreen));
|
||||
271
react/features/lobby/components/native/LobbyScreen.tsx
Normal file
271
react/features/lobby/components/native/LobbyScreen.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React from 'react';
|
||||
import { Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
|
||||
import LargeVideo from '../../../large-video/components/LargeVideo.native';
|
||||
import { navigate }
|
||||
from '../../../mobile/navigation/components/lobby/LobbyNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { preJoinStyles } from '../../../prejoin/components/native/styles';
|
||||
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
|
||||
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
|
||||
import AbstractLobbyScreen, {
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as abstractMapStateToProps } from '../AbstractLobbyScreen';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* The current aspect ratio of the screen.
|
||||
*/
|
||||
_aspectRatio: Symbol;
|
||||
|
||||
/**
|
||||
* The room name.
|
||||
*/
|
||||
_roomName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a waiting screen that represents the participant being in the lobby.
|
||||
*/
|
||||
class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _aspectRatio, _roomName } = this.props;
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapper;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
} else {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapperWide;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainerWide;
|
||||
contentContainerStyles = preJoinStyles.contentContainerWide;
|
||||
}
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
<BrandingImageBackground />
|
||||
<View style = { largeVideoContainerStyles as ViewStyle }>
|
||||
<View style = { preJoinStyles.displayRoomNameBackdrop as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { preJoinStyles.preJoinRoomName }>
|
||||
{ _roomName }
|
||||
</Text>
|
||||
</View>
|
||||
<LargeVideo />
|
||||
</View>
|
||||
<View style = { contentContainerStyles as ViewStyle }>
|
||||
{ this._renderToolbarButtons() }
|
||||
{ this._renderContent() }
|
||||
</View>
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the lobby chat screen.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onNavigateToLobbyChat() {
|
||||
navigate(screen.lobby.chat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the joining (waiting) fragment of the screen.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderJoining() {
|
||||
return (
|
||||
<View style = { styles.lobbyWaitingFragmentContainer }>
|
||||
<Text style = { styles.lobbyTitle }>
|
||||
{ this.props.t('lobby.joiningTitle') }
|
||||
</Text>
|
||||
<LoadingIndicator
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
style = { styles.loadingIndicator } />
|
||||
<Text style = { styles.joiningMessage as TextStyle }>
|
||||
{ this.props.t('lobby.joiningMessage') }
|
||||
</Text>
|
||||
{ this._renderStandardButtons() }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant form to let the knocking participant enter its details.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderParticipantForm() {
|
||||
const { t } = this.props;
|
||||
const { displayName } = this.state;
|
||||
|
||||
return (
|
||||
<Input
|
||||
customStyles = {{ input: preJoinStyles.customInput }}
|
||||
onChange = { this._onChangeDisplayName }
|
||||
placeholder = { t('lobby.nameField') }
|
||||
value = { displayName } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant info fragment when we have all the required details of the user.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderParticipantInfo() {
|
||||
return this._renderParticipantForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderPasswordForm() {
|
||||
const { _passwordJoinFailed, t } = this.props;
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoCapitalize = 'none'
|
||||
customStyles = {{ input: styles.customInput }}
|
||||
error = { _passwordJoinFailed }
|
||||
onChange = { this._onChangePassword }
|
||||
placeholder = { t('lobby.enterPasswordButton') }
|
||||
secureTextEntry = { true }
|
||||
value = { this.state.password } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password join button (set).
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderPasswordJoinButtons() {
|
||||
return (
|
||||
<View style = { styles.passwordJoinButtons }>
|
||||
<Button
|
||||
accessibilityLabel = 'lobby.passwordJoinButton'
|
||||
disabled = { !this.state.password }
|
||||
labelKey = { 'lobby.passwordJoinButton' }
|
||||
onClick = { this._onJoinWithPassword }
|
||||
style = { preJoinStyles.joinButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'lobby.backToKnockModeButton'
|
||||
labelKey = 'lobby.backToKnockModeButton'
|
||||
onClick = { this._onSwitchToKnockMode }
|
||||
style = { preJoinStyles.joinButton }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the toolbar buttons menu.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderToolbarButtons() {
|
||||
return (
|
||||
<View style = { preJoinStyles.toolboxContainer as ViewStyle }>
|
||||
<AudioMuteButton
|
||||
styles = { preJoinStyles.buttonStylesBorderless } />
|
||||
<VideoMuteButton
|
||||
styles = { preJoinStyles.buttonStylesBorderless } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the standard button set.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderStandardButtons() {
|
||||
const { _knocking, _renderPassword, _isLobbyChatActive } = this.props;
|
||||
const { displayName } = this.state;
|
||||
|
||||
return (
|
||||
<View style = { styles.formWrapper as ViewStyle }>
|
||||
{
|
||||
_knocking && _isLobbyChatActive
|
||||
&& <Button
|
||||
accessibilityLabel = 'toolbar.openChat'
|
||||
labelKey = 'toolbar.openChat'
|
||||
onClick = { this._onNavigateToLobbyChat }
|
||||
style = { preJoinStyles.joinButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
}
|
||||
{
|
||||
_knocking
|
||||
|| <Button
|
||||
accessibilityLabel = 'lobby.knockButton'
|
||||
disabled = { !displayName }
|
||||
labelKey = 'lobby.knockButton'
|
||||
onClick = { this._onAskToJoin }
|
||||
style = { preJoinStyles.joinButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
}
|
||||
{
|
||||
_renderPassword
|
||||
&& <Button
|
||||
accessibilityLabel = 'lobby.enterPasswordButton'
|
||||
labelKey = 'lobby.enterPasswordButton'
|
||||
onClick = { this._onSwitchToPasswordMode }
|
||||
style = { preJoinStyles.joinButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {{
|
||||
* _aspectRatio: Symbol
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_roomName: getConferenceName(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(LobbyScreen));
|
||||
98
react/features/lobby/components/native/styles.ts
Normal file
98
react/features/lobby/components/native/styles.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export default {
|
||||
|
||||
lobbyChatWrapper: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
passwordJoinButtons: {
|
||||
top: 40
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
height: 388,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
formWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
customInput: {
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
top: BaseTheme.spacing[6],
|
||||
width: 352
|
||||
},
|
||||
|
||||
joiningMessage: {
|
||||
color: BaseTheme.palette.text01,
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
loadingIndicator: {
|
||||
marginBottom: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
// KnockingParticipantList
|
||||
|
||||
knockingParticipantList: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
},
|
||||
|
||||
|
||||
knockingParticipantListDetails: {
|
||||
flex: 1,
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
knockingParticipantListEntry: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
knockingParticipantListText: {
|
||||
color: 'white'
|
||||
},
|
||||
|
||||
lobbyButtonAdmit: {
|
||||
position: 'absolute',
|
||||
right: 184,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonChat: {
|
||||
position: 'absolute',
|
||||
right: 104,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonReject: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyTitle: {
|
||||
...BaseTheme.typography.heading5,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
lobbyWaitingFragmentContainer: {
|
||||
height: 260
|
||||
}
|
||||
};
|
||||
284
react/features/lobby/components/web/LobbyScreen.tsx
Normal file
284
react/features/lobby/components/web/LobbyScreen.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/web/LoadingIndicator';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import ChatInput from '../../../chat/components/web/ChatInput';
|
||||
import MessageContainer from '../../../chat/components/web/MessageContainer';
|
||||
import AbstractLobbyScreen, {
|
||||
IProps,
|
||||
_mapStateToProps
|
||||
} from '../AbstractLobbyScreen';
|
||||
|
||||
/**
|
||||
* Implements a waiting screen that represents the participant being in the lobby.
|
||||
*/
|
||||
class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
/**
|
||||
* Reference to the React Component for displaying chat messages. Used for
|
||||
* scrolling to the end of the chat messages.
|
||||
*/
|
||||
_messageContainerRef: React.RefObject<MessageContainer>;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code LobbyScreen} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._messageContainerRef = React.createRef<MessageContainer>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this._scrollMessageContainerToBottom(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props._lobbyChatMessages !== prevProps._lobbyChatMessages) {
|
||||
this._scrollMessageContainerToBottom(true);
|
||||
} else if (this.props._isLobbyChatActive && !prevProps._isLobbyChatActive) {
|
||||
this._scrollMessageContainerToBottom(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _deviceStatusVisible, showCopyUrlButton, t } = this.props;
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
className = 'lobby-screen'
|
||||
showCopyUrlButton = { showCopyUrlButton }
|
||||
showDeviceStatus = { _deviceStatusVisible }
|
||||
title = { t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }>
|
||||
{ this._renderContent() }
|
||||
</PreMeetingScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the joining (waiting) fragment of the screen.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderJoining() {
|
||||
const { _isLobbyChatActive } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'lobby-screen-content'>
|
||||
{_isLobbyChatActive
|
||||
? this._renderLobbyChat()
|
||||
: (
|
||||
<>
|
||||
<div className = 'spinner'>
|
||||
<LoadingIndicator size = 'large' />
|
||||
</div>
|
||||
<span className = 'joining-message'>
|
||||
{ this.props.t('lobby.joiningMessage') }
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ this._renderStandardButtons() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the widget to chat with the moderator before allowed in.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderLobbyChat() {
|
||||
const { _lobbyChatMessages, t } = this.props;
|
||||
const { isChatOpen } = this.state;
|
||||
|
||||
return (
|
||||
<div className = { `lobby-chat-container ${isChatOpen ? 'hidden' : ''}` }>
|
||||
<div className = 'lobby-chat-header'>
|
||||
<h1 className = 'title'>
|
||||
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
|
||||
</h1>
|
||||
<Icon
|
||||
ariaLabel = { t('toolbar.closeChat') }
|
||||
onClick = { this._onToggleChat }
|
||||
role = 'button'
|
||||
src = { IconCloseLarge } />
|
||||
</div>
|
||||
<MessageContainer
|
||||
messages = { _lobbyChatMessages }
|
||||
ref = { this._messageContainerRef } />
|
||||
<ChatInput onSend = { this._onSendMessage } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant form to let the knocking participant enter its details.
|
||||
*
|
||||
* NOTE: We don't use edit action on web since the prejoin functionality got merged.
|
||||
* Mobile won't use it either once prejoin gets implemented there too.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderParticipantForm() {
|
||||
return this._renderParticipantInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the participant info fragment when we have all the required details of the user.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderParticipantInfo() {
|
||||
const { displayName } = this.state;
|
||||
const { _isDisplayNameRequiredActive, t } = this.props;
|
||||
const showError = _isDisplayNameRequiredActive && !displayName;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = 'lobby-prejoin-input'
|
||||
error = { showError }
|
||||
id = 'lobby-name-field'
|
||||
onChange = { this._onChangeDisplayName }
|
||||
placeholder = { t('lobby.nameField') }
|
||||
testId = 'lobby.nameField'
|
||||
value = { displayName } />
|
||||
|
||||
{ showError && <div
|
||||
className = 'lobby-prejoin-error'
|
||||
data-testid = 'lobby.errorMessage'>{t('prejoin.errorMissingName')}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password form to let the participant join by using a password instead of knocking.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderPasswordForm() {
|
||||
const { _passwordJoinFailed, t } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className = { `lobby-prejoin-input ${_passwordJoinFailed ? 'error' : ''}` }
|
||||
id = 'lobby-password-input'
|
||||
onChange = { this._onChangePassword }
|
||||
placeholder = { t('lobby.enterPasswordButton') }
|
||||
testId = 'lobby.password'
|
||||
type = 'password'
|
||||
value = { this.state.password } />
|
||||
|
||||
{_passwordJoinFailed && <div
|
||||
className = 'lobby-prejoin-error'
|
||||
data-testid = 'lobby.errorMessage'>{t('lobby.invalidPassword')}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the password join button (set).
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderPasswordJoinButtons() {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className = 'lobby-button-margin'
|
||||
fullWidth = { true }
|
||||
labelKey = 'prejoin.joinMeeting'
|
||||
onClick = { this._onJoinWithPassword }
|
||||
testId = 'lobby.passwordJoinButton'
|
||||
type = 'primary' />
|
||||
<Button
|
||||
className = 'lobby-button-margin'
|
||||
fullWidth = { true }
|
||||
labelKey = 'lobby.backToKnockModeButton'
|
||||
onClick = { this._onSwitchToKnockMode }
|
||||
testId = 'lobby.backToKnockModeButton'
|
||||
type = 'secondary' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the standard button set.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderStandardButtons() {
|
||||
const { _knocking, _isLobbyChatActive, _renderPassword } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{_knocking || <Button
|
||||
className = 'lobby-button-margin'
|
||||
disabled = { !this.state.displayName }
|
||||
fullWidth = { true }
|
||||
labelKey = 'lobby.knockButton'
|
||||
onClick = { this._onAskToJoin }
|
||||
testId = 'lobby.knockButton'
|
||||
type = 'primary' />
|
||||
}
|
||||
{(_knocking && _isLobbyChatActive) && <Button
|
||||
className = 'lobby-button-margin open-chat-button'
|
||||
fullWidth = { true }
|
||||
labelKey = 'toolbar.openChat'
|
||||
onClick = { this._onToggleChat }
|
||||
testId = 'toolbar.openChat'
|
||||
type = 'primary' />
|
||||
}
|
||||
{_renderPassword && <Button
|
||||
className = 'lobby-button-margin'
|
||||
fullWidth = { true }
|
||||
labelKey = 'lobby.enterPasswordButton'
|
||||
onClick = { this._onSwitchToPasswordMode }
|
||||
testId = 'lobby.enterPasswordButton'
|
||||
type = 'secondary' />
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the chat messages so the latest message is visible.
|
||||
*
|
||||
* @param {boolean} withAnimation - Whether or not to show a scrolling
|
||||
* animation.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_scrollMessageContainerToBottom(withAnimation: boolean) {
|
||||
if (this._messageContainerRef.current) {
|
||||
this._messageContainerRef.current.scrollToElement(withAnimation, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(LobbyScreen));
|
||||
122
react/features/lobby/components/web/LobbySection.tsx
Normal file
122
react/features/lobby/components/web/LobbySection.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Switch from '../../../base/ui/components/web/Switch';
|
||||
import { toggleLobbyMode } from '../../actions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* True if lobby is currently enabled in the conference.
|
||||
*/
|
||||
_lobbyEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* True if the lobby switch is toggled on.
|
||||
*/
|
||||
lobbyEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a security feature section to control lobby mode.
|
||||
*/
|
||||
class LobbySection extends PureComponent<IProps, IState> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
lobbyEnabled: props._lobbyEnabled
|
||||
};
|
||||
|
||||
this._onToggleLobby = this._onToggleLobby.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#getDerivedStateFromProps()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: IProps, state: IState) {
|
||||
if (props._lobbyEnabled !== state.lobbyEnabled) {
|
||||
|
||||
return {
|
||||
lobbyEnabled: props._lobbyEnabled
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div id = 'lobby-section'>
|
||||
<p
|
||||
className = 'description'
|
||||
role = 'banner'>
|
||||
{ t('lobby.enableDialogText') }
|
||||
</p>
|
||||
<div className = 'control-row'>
|
||||
<label htmlFor = 'lobby-section-switch'>
|
||||
{ t('lobby.toggleLabel') }
|
||||
</label>
|
||||
<Switch
|
||||
checked = { this.state.lobbyEnabled }
|
||||
id = 'lobby-section-switch'
|
||||
onChange = { this._onToggleLobby } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user toggles the lobby feature on or off.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleLobby() {
|
||||
const newValue = !this.state.lobbyEnabled;
|
||||
|
||||
this.setState({
|
||||
lobbyEnabled: newValue
|
||||
});
|
||||
|
||||
this.props.dispatch(toggleLobbyMode(newValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_lobbyEnabled: state['features/lobby'].lobbyEnabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(LobbySection));
|
||||
25
react/features/lobby/constants.ts
Normal file
25
react/features/lobby/constants.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Hide these emails when trying to join a lobby.
|
||||
*/
|
||||
export const HIDDEN_EMAILS = [ 'inbound-sip-jibri@jitsi.net', 'outbound-sip-jibri@jitsi.net' ];
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when a participant joins lobby.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const KNOCKING_PARTICIPANT_SOUND_ID = 'KNOCKING_PARTICIPANT_SOUND';
|
||||
|
||||
/**
|
||||
* Lobby chat initialized message type.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const LOBBY_CHAT_INITIALIZED = 'LOBBY_CHAT_INITIALIZED';
|
||||
|
||||
/**
|
||||
* Event message sent to knocking participant when moderator in chat with leaves.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const MODERATOR_IN_CHAT_WITH_LEFT = 'MODERATOR_IN_CHAT_WITH_LEFT';
|
||||
105
react/features/lobby/functions.ts
Normal file
105
react/features/lobby/functions.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getVisitorsCount } from '../visitors/functions';
|
||||
|
||||
import { IKnockingParticipant } from './types';
|
||||
|
||||
|
||||
/**
|
||||
* Selector to return lobby enable state.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getLobbyEnabled(state: IReduxState) {
|
||||
return state['features/lobby'].lobbyEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return a list of knocking participants.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getKnockingParticipants(state: IReduxState) {
|
||||
return state['features/lobby'].knockingParticipants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return lobby visibility.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getIsLobbyVisible(state: IReduxState) {
|
||||
return state['features/lobby'].lobbyVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return array with knocking participant ids.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getKnockingParticipantsById(state: IReduxState) {
|
||||
return getKnockingParticipants(state).map(participant => participant.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return the lobby config.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getLobbyConfig(state: IReduxState) {
|
||||
return state['features/base/config']?.lobby || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that handles the visibility of the lobby chat message.
|
||||
*
|
||||
* @param {Object} participant - Lobby Participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showLobbyChatButton(
|
||||
participant: IKnockingParticipant
|
||||
) {
|
||||
return function(state: IReduxState) {
|
||||
|
||||
const { enableChat = true } = getLobbyConfig(state);
|
||||
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
const lobbyLocalId = conference?.myLobbyUserId();
|
||||
|
||||
if (!enableChat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLobbyChatActive
|
||||
&& (!participant.chattingWithModerator
|
||||
|| participant.chattingWithModerator === lobbyLocalId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isLobbyChatActive && lobbyMessageRecipient
|
||||
&& participant.id !== lobbyMessageRecipient.id
|
||||
&& (!participant.chattingWithModerator
|
||||
|| participant.chattingWithModerator === lobbyLocalId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if enabling lobby is allowed and false otherwise.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEnablingLobbyAllowed(state: IReduxState) {
|
||||
return getVisitorsCount(state) <= 0;
|
||||
}
|
||||
3
react/features/lobby/logger.ts
Normal file
3
react/features/lobby/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/lobby');
|
||||
436
react/features/lobby/middleware.ts
Normal file
436
react/features/lobby/middleware.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { conferenceWillJoin } from '../base/conference/actions';
|
||||
import {
|
||||
JitsiConferenceErrors,
|
||||
JitsiConferenceEvents
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
getFirstLoadableAvatarUrl,
|
||||
getParticipantDisplayName
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import {
|
||||
playSound,
|
||||
registerSound,
|
||||
unregisterSound
|
||||
} from '../base/sounds/actions';
|
||||
import { isTestModeEnabled } from '../base/testing/functions';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { openChat } from '../chat/actions';
|
||||
import {
|
||||
handleLobbyChatInitialized,
|
||||
removeLobbyChatParticipant
|
||||
} from '../chat/actions.any';
|
||||
import { arePollsDisabled } from '../conference/functions.any';
|
||||
import { hideNotification, showNotification } from '../notifications/actions';
|
||||
import {
|
||||
LOBBY_NOTIFICATION_ID,
|
||||
NOTIFICATION_ICON,
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
NOTIFICATION_TYPE
|
||||
} from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
import { getParticipantsPaneOpen } from '../participants-pane/functions';
|
||||
import { PREJOIN_JOINING_IN_PROGRESS } from '../prejoin/actionTypes';
|
||||
import {
|
||||
isPrejoinEnabledInConfig,
|
||||
isPrejoinPageVisible,
|
||||
shouldAutoKnock
|
||||
} from '../prejoin/functions';
|
||||
|
||||
import {
|
||||
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
|
||||
KNOCKING_PARTICIPANT_LEFT
|
||||
} from './actionTypes';
|
||||
import {
|
||||
approveKnockingParticipant,
|
||||
hideLobbyScreen,
|
||||
knockingParticipantLeft,
|
||||
openLobbyScreen,
|
||||
participantIsKnockingOrUpdated,
|
||||
rejectKnockingParticipant,
|
||||
setLobbyMessageListener,
|
||||
setLobbyModeEnabled,
|
||||
setPasswordJoinFailed,
|
||||
startKnocking
|
||||
} from './actions';
|
||||
import { updateLobbyParticipantOnLeave } from './actions.any';
|
||||
import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
|
||||
import { getKnockingParticipants, showLobbyChatButton } from './functions';
|
||||
import { KNOCKING_PARTICIPANT_FILE } from './sounds';
|
||||
import { IKnockingParticipant } from './types';
|
||||
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE));
|
||||
break;
|
||||
case APP_WILL_UNMOUNT:
|
||||
store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID));
|
||||
break;
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
|
||||
_maybeSendLobbyNotification(participant, data, store);
|
||||
|
||||
break;
|
||||
}
|
||||
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);
|
||||
_handleLobbyNotification(store);
|
||||
|
||||
return result;
|
||||
}
|
||||
case KNOCKING_PARTICIPANT_LEFT: {
|
||||
// We need the full update result to be in the store already
|
||||
const result = next(action);
|
||||
|
||||
_handleLobbyNotification(store);
|
||||
|
||||
return result;
|
||||
}
|
||||
case PREJOIN_JOINING_IN_PROGRESS: {
|
||||
if (action.value) {
|
||||
// let's hide the notification (the case with denied access and retrying) when prejoin is enabled
|
||||
store.dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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, getState }, previousConference) => {
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, (enabled: boolean) => {
|
||||
dispatch(setLobbyModeEnabled(enabled));
|
||||
if (enabled) {
|
||||
dispatch(setLobbyMessageListener());
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id: string, name: string) => {
|
||||
const { soundsParticipantKnocking } = getState()['features/base/settings'];
|
||||
|
||||
batch(() => {
|
||||
dispatch(
|
||||
participantIsKnockingOrUpdated({
|
||||
id,
|
||||
name
|
||||
})
|
||||
);
|
||||
if (soundsParticipantKnocking) {
|
||||
dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
|
||||
}
|
||||
|
||||
const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyKnockingParticipant({
|
||||
id,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
if (isParticipantsPaneVisible || navigator.product === 'ReactNative') {
|
||||
return;
|
||||
}
|
||||
|
||||
_handleLobbyNotification({
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id: string, participant: IKnockingParticipant) => {
|
||||
dispatch(
|
||||
participantIsKnockingOrUpdated({
|
||||
...participant,
|
||||
id
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, (id: string) => {
|
||||
batch(() => {
|
||||
dispatch(knockingParticipantLeft(id));
|
||||
dispatch(removeLobbyChatParticipant());
|
||||
dispatch(updateLobbyParticipantOnLeave(id));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Function to handle the lobby notification.
|
||||
*
|
||||
* @param {Object} store - The Redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleLobbyNotification(store: IStore) {
|
||||
const { dispatch, getState } = store;
|
||||
const knockingParticipants = getKnockingParticipants(getState());
|
||||
|
||||
if (knockingParticipants.length === 0) {
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let notificationTitle;
|
||||
let customActionNameKey;
|
||||
let customActionHandler;
|
||||
let customActionType;
|
||||
let descriptionKey;
|
||||
let icon;
|
||||
|
||||
if (knockingParticipants.length === 1) {
|
||||
const firstParticipant = knockingParticipants[0];
|
||||
const showChat = showLobbyChatButton(firstParticipant)(getState());
|
||||
|
||||
descriptionKey = 'notify.participantWantsToJoin';
|
||||
notificationTitle = firstParticipant.name;
|
||||
icon = NOTIFICATION_ICON.PARTICIPANT;
|
||||
customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
|
||||
customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
|
||||
customActionHandler = [ () => batch(() => {
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
dispatch(approveKnockingParticipant(firstParticipant.id));
|
||||
}),
|
||||
() => batch(() => {
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
dispatch(rejectKnockingParticipant(firstParticipant.id));
|
||||
}) ];
|
||||
|
||||
// This checks if lobby chat button is available
|
||||
// and, if so, it adds it to the customActionNameKey array
|
||||
if (showChat) {
|
||||
customActionNameKey.splice(1, 0, 'lobby.chat');
|
||||
customActionType.splice(1, 0, BUTTON_TYPES.SECONDARY);
|
||||
customActionHandler.splice(1, 0, () => batch(() => {
|
||||
dispatch(handleLobbyChatInitialized(firstParticipant.id));
|
||||
dispatch(openChat({}, arePollsDisabled(getState())));
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
descriptionKey = 'notify.participantsWantToJoin';
|
||||
notificationTitle = i18n.t('notify.waitingParticipants', {
|
||||
waitingParticipants: knockingParticipants.length
|
||||
});
|
||||
icon = NOTIFICATION_ICON.PARTICIPANTS;
|
||||
customActionNameKey = [ 'notify.viewLobby' ];
|
||||
customActionType = [ BUTTON_TYPES.PRIMARY ];
|
||||
customActionHandler = [ () => batch(() => {
|
||||
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
|
||||
dispatch(openParticipantsPane());
|
||||
}) ];
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
title: notificationTitle,
|
||||
descriptionKey,
|
||||
uid: LOBBY_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionType,
|
||||
customActionHandler,
|
||||
icon
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { error } = action;
|
||||
const state = getState();
|
||||
const { lobbyError, membersOnly } = state['features/base/conference'];
|
||||
const nonFirstFailure = Boolean(membersOnly);
|
||||
|
||||
if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
|
||||
if (typeof error.recoverable === 'undefined') {
|
||||
error.recoverable = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
|
||||
|
||||
const result = next(action);
|
||||
|
||||
dispatch(openLobbyScreen());
|
||||
|
||||
// if there was an error about display name and pre-join is not enabled
|
||||
if (shouldAutoKnock(state)
|
||||
|| (lobbyError && !isPrejoinEnabledInConfig(state))
|
||||
|| lobbyWaitingForHost) {
|
||||
dispatch(startKnocking());
|
||||
}
|
||||
|
||||
// In case of wrong password we need to be in the right state if in the meantime someone allows us to join
|
||||
if (nonFirstFailure) {
|
||||
dispatch(conferenceWillJoin(membersOnly));
|
||||
}
|
||||
|
||||
dispatch(setPasswordJoinFailed(nonFirstFailure));
|
||||
|
||||
return result;
|
||||
} else if (error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
|
||||
const [ isLobbyEnabled ] = error.params;
|
||||
|
||||
const result = next(action);
|
||||
|
||||
// if the error is due to required display name because lobby is enabled for the room
|
||||
// if not showing the prejoin page then show lobby UI
|
||||
if (isLobbyEnabled && !isPrejoinPageVisible(state)) {
|
||||
dispatch(openLobbyScreen());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// if both are available pre-join is with priority (the case when pre-join is enabled)
|
||||
// when pre-join is disabled, and we are in lobby with error, we want to end up in lobby UI
|
||||
// instead of hiding it and showing conference UI. Still in lobby the user can retry
|
||||
// after we show the error notification
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
dispatch(hideLobbyScreen());
|
||||
}
|
||||
|
||||
// we want to finish this action before showing the notification
|
||||
// as the conference will be cleared which will clear all notifications, including this one
|
||||
const result = next(action);
|
||||
|
||||
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
appearance: NOTIFICATION_TYPE.ERROR,
|
||||
hideErrorSupportLink: true,
|
||||
titleKey: 'lobby.joinRejectedTitle',
|
||||
uid: LOBBY_NOTIFICATION_ID,
|
||||
descriptionKey: 'lobby.joinRejectedMessage'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }: IStore, next: Function, action: AnyAction) {
|
||||
dispatch(hideLobbyScreen());
|
||||
|
||||
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(store: IStore, { id }: { id: string; }) {
|
||||
const { dispatch, getState } = store;
|
||||
const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
|
||||
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
||||
|
||||
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store).then((result: { isUsingCORS: boolean; src: string; }) => {
|
||||
if (result) {
|
||||
const { isUsingCORS, src } = result;
|
||||
|
||||
dispatch(
|
||||
participantIsKnockingOrUpdated({
|
||||
loadableAvatarUrl: src,
|
||||
id,
|
||||
isUsingCORS
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the endpoint message that arrived through the conference and
|
||||
* sends a lobby notification, if the message belongs to the feature.
|
||||
*
|
||||
* @param {Object} origin - The origin (initiator) of the message.
|
||||
* @param {Object} message - The actual message.
|
||||
* @param {Object} store - The Redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeSendLobbyNotification(origin: any, message: any, { dispatch, getState }: IStore) {
|
||||
if (!origin?._id || message?.type !== 'lobby-notify') {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationProps: INotificationProps = {
|
||||
descriptionArguments: {
|
||||
originParticipantName: getParticipantDisplayName(getState, origin._id),
|
||||
targetParticipantName: message.name
|
||||
},
|
||||
titleKey: 'lobby.notificationTitle'
|
||||
};
|
||||
|
||||
switch (message.event) {
|
||||
case 'LOBBY-ENABLED':
|
||||
notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
|
||||
break;
|
||||
case 'LOBBY-ACCESS-GRANTED':
|
||||
notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
|
||||
break;
|
||||
case 'LOBBY-ACCESS-DENIED':
|
||||
notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
|
||||
break;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
showNotification(
|
||||
notificationProps,
|
||||
isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM
|
||||
)
|
||||
);
|
||||
}
|
||||
166
react/features/lobby/reducer.ts
Normal file
166
react/features/lobby/reducer.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
SET_PASSWORD
|
||||
} from '../base/conference/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
|
||||
KNOCKING_PARTICIPANT_LEFT,
|
||||
REMOVE_LOBBY_CHAT_WITH_MODERATOR,
|
||||
SET_KNOCKING_STATE,
|
||||
SET_LOBBY_MODE_ENABLED,
|
||||
SET_LOBBY_PARTICIPANT_CHAT_STATE,
|
||||
SET_LOBBY_VISIBILITY,
|
||||
SET_PASSWORD_JOIN_FAILED
|
||||
} from './actionTypes';
|
||||
import { IKnockingParticipant } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isDisplayNameRequiredError: false,
|
||||
knocking: false,
|
||||
knockingParticipants: [],
|
||||
lobbyEnabled: false,
|
||||
lobbyVisible: false,
|
||||
passwordJoinFailed: false
|
||||
};
|
||||
|
||||
export interface ILobbyState {
|
||||
|
||||
/**
|
||||
* A conference error when we tried to join into a room with no display name
|
||||
* when lobby is enabled in the room.
|
||||
*/
|
||||
isDisplayNameRequiredError: boolean;
|
||||
knocking: boolean;
|
||||
knockingParticipants: IKnockingParticipant[];
|
||||
lobbyEnabled: boolean;
|
||||
lobbyVisible: boolean;
|
||||
passwordJoinFailed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ILobbyState>('features/lobby', (state = DEFAULT_STATE, action): ILobbyState => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED: {
|
||||
if (action.error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequiredError: true
|
||||
};
|
||||
} else if (action.error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||
return {
|
||||
...state,
|
||||
knocking: false
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequiredError: false,
|
||||
knocking: false,
|
||||
passwordJoinFailed: 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,
|
||||
passwordJoinFailed: false
|
||||
};
|
||||
case SET_LOBBY_MODE_ENABLED:
|
||||
return {
|
||||
...state,
|
||||
lobbyEnabled: action.enabled
|
||||
};
|
||||
case SET_LOBBY_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
lobbyVisible: action.visible
|
||||
};
|
||||
case SET_PASSWORD:
|
||||
return {
|
||||
...state,
|
||||
passwordJoinFailed: false
|
||||
};
|
||||
case SET_PASSWORD_JOIN_FAILED:
|
||||
return {
|
||||
...state,
|
||||
passwordJoinFailed: action.failed
|
||||
};
|
||||
case SET_LOBBY_PARTICIPANT_CHAT_STATE:
|
||||
return {
|
||||
...state,
|
||||
knockingParticipants: state.knockingParticipants.map(participant => {
|
||||
if (participant.id === action.participant.id) {
|
||||
return {
|
||||
...participant,
|
||||
chattingWithModerator: action.moderator.id
|
||||
};
|
||||
}
|
||||
|
||||
return participant;
|
||||
})
|
||||
};
|
||||
case REMOVE_LOBBY_CHAT_WITH_MODERATOR:
|
||||
return {
|
||||
...state,
|
||||
knockingParticipants: state.knockingParticipants.map(participant => {
|
||||
if (participant.chattingWithModerator === action.moderatorId) {
|
||||
return {
|
||||
...participant,
|
||||
chattingWithModerator: undefined
|
||||
};
|
||||
}
|
||||
|
||||
return participant;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
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: IKnockingParticipant, state: ILobbyState) {
|
||||
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
|
||||
]
|
||||
};
|
||||
}
|
||||
5
react/features/lobby/sounds.ts
Normal file
5
react/features/lobby/sounds.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* The name of the bundled sound file which will be played when a
|
||||
* participant enters lobby.
|
||||
*/
|
||||
export const KNOCKING_PARTICIPANT_FILE = 'knock.mp3';
|
||||
5
react/features/lobby/types.ts
Normal file
5
react/features/lobby/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
|
||||
export interface IKnockingParticipant extends IParticipant {
|
||||
chattingWithModerator?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user