This commit is contained in:
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));
|
||||
Reference in New Issue
Block a user