init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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
};
}

View 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));

View 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));

View 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
}
};

View 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));

View 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));