diff --git a/config.js b/config.js index 49173d89f5..489b3024ed 100644 --- a/config.js +++ b/config.js @@ -723,6 +723,8 @@ var config = { // autoKnock: false, // // Enables the lobby chat. Replaces `enableLobbyChat`. // enableChat: true, + // // Shows the hangup button in the lobby screen. + // showHangUp: true, // }, // Configs for the security related UI elements. diff --git a/lang/main.json b/lang/main.json index 083e8ffb10..62ba251bd6 100644 --- a/lang/main.json +++ b/lang/main.json @@ -280,7 +280,6 @@ "Submit": "Submit", "Understand": "I understand, keep me muted for now", "UnderstandAndUnmute": "I understand, please unmute me", - "WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.", "WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.", "WaitingForHostButton": "Wait for moderator", "WaitingForHostTitle": "Waiting for a moderator…", @@ -748,7 +747,8 @@ "notificationTitle": "Lobby", "passwordJoinButton": "Join", "title": "Lobby", - "toggleLabel": "Enable lobby" + "toggleLabel": "Enable lobby", + "waitForModerator": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait." }, "localRecording": { "clientState": { diff --git a/react/features/authentication/actionTypes.ts b/react/features/authentication/actionTypes.ts index 9f7e404deb..34e46c19b1 100644 --- a/react/features/authentication/actionTypes.ts +++ b/react/features/authentication/actionTypes.ts @@ -46,6 +46,15 @@ export const SET_TOKEN_AUTH_URL_SUCCESS = 'SET_TOKEN_AUTH_URL_SUCCESS'; */ export const STOP_WAIT_FOR_OWNER = 'STOP_WAIT_FOR_OWNER'; +/** + * The type of (redux) action which disables moderator login. + * + * { + * type: DISABLE_MODERATOR_LOGIN + * } + */ +export const DISABLE_MODERATOR_LOGIN = 'DISABLE_MODERATOR_LOGIN'; + /** * The type of (redux) action which informs that the authentication and role * upgrade process has finished either with success or with a specific error. @@ -74,6 +83,15 @@ export const UPGRADE_ROLE_FINISHED = 'UPGRADE_ROLE_FINISHED'; */ export const UPGRADE_ROLE_STARTED = 'UPGRADE_ROLE_STARTED'; +/** + * The type of (redux) action which enables moderator login. + * + * { + * type: ENABLE_MODERATOR_LOGIN + * } + */ +export const ENABLE_MODERATOR_LOGIN = 'ENABLE_MODERATOR_LOGIN'; + /** * The type of (redux) action that sets delayed handler which will check if * the conference has been created and it's now possible to join from anonymous diff --git a/react/features/authentication/actions.any.ts b/react/features/authentication/actions.any.ts index 89cdfee4f7..659a5c9520 100644 --- a/react/features/authentication/actions.any.ts +++ b/react/features/authentication/actions.any.ts @@ -4,12 +4,15 @@ import { IJitsiConference } from '../base/conference/reducer'; import { hideDialog, openDialog } from '../base/dialog/actions'; import { + DISABLE_MODERATOR_LOGIN, + ENABLE_MODERATOR_LOGIN, LOGIN, LOGOUT, SET_TOKEN_AUTH_URL_SUCCESS, STOP_WAIT_FOR_OWNER, UPGRADE_ROLE_FINISHED, - UPGRADE_ROLE_STARTED, WAIT_FOR_OWNER + UPGRADE_ROLE_STARTED, + WAIT_FOR_OWNER } from './actionTypes'; import { LoginDialog, WaitForOwnerDialog } from './components'; import logger from './logger'; @@ -165,6 +168,30 @@ export function logout() { }; } +/** + * Disables moderator login. + * + * @returns {{ + * type: DISABLE_MODERATOR_LOGIN + * }} + */ +export function disableModeratorLogin() { + return { + type: DISABLE_MODERATOR_LOGIN + }; +} + +/** + * Enables moderator login. + * + * @returns {Object} + */ +export function enableModeratorLogin() { + return { + type: ENABLE_MODERATOR_LOGIN + }; +} + /** * Opens {@link WaitForOnwerDialog}. * @@ -175,6 +202,7 @@ export function openWaitForOwnerDialog() { return openDialog(WaitForOwnerDialog); } + /** * Stops waiting for the conference owner. * diff --git a/react/features/authentication/components/native/WaitForOwnerDialog.tsx b/react/features/authentication/components/native/WaitForOwnerDialog.tsx index d81f5151f7..488058c003 100644 --- a/react/features/authentication/components/native/WaitForOwnerDialog.tsx +++ b/react/features/authentication/components/native/WaitForOwnerDialog.tsx @@ -67,7 +67,7 @@ class WaitForOwnerDialog extends Component { diff --git a/react/features/authentication/components/web/WaitForOwnerDialog.tsx b/react/features/authentication/components/web/WaitForOwnerDialog.tsx index 2d9185e3e9..8ee12c790f 100644 --- a/react/features/authentication/components/web/WaitForOwnerDialog.tsx +++ b/react/features/authentication/components/web/WaitForOwnerDialog.tsx @@ -91,7 +91,7 @@ class WaitForOwnerDialog extends PureComponent { onSubmit = { this._onIAmHost } titleKey = { t('dialog.WaitingForHostTitle') }> - { this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('dialog.WaitForHostMsg') } + { this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('lobby.waitForModerator') } ); diff --git a/react/features/authentication/middleware.ts b/react/features/authentication/middleware.ts index cbf35c79e3..888cc8e8e4 100644 --- a/react/features/authentication/middleware.ts +++ b/react/features/authentication/middleware.ts @@ -28,6 +28,8 @@ import { WAIT_FOR_OWNER } from './actionTypes'; import { + disableModeratorLogin, + enableModeratorLogin, hideLoginDialog, openLoginDialog, openTokenAuthUrl, @@ -44,7 +46,7 @@ import logger from './logger'; /** * Middleware that captures connection or conference failed errors and controls - * {@link WaitForOwnerDialog} and {@link LoginDialog}. + * moderator login availability and {@link LoginDialog}. * * FIXME Some of the complexity was introduced by the lack of dialog stacking. * @@ -105,11 +107,21 @@ MiddlewareRegistry.register(store => next => action => { } recoverable = error.recoverable; } - if (recoverable) { - store.dispatch(waitForOwner()); - } else { - store.dispatch(stopWaitForOwner()); + + if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR && lobbyWaitingForHost) { + if (recoverable) { + store.dispatch(enableModeratorLogin()); + } else { + store.dispatch(disableModeratorLogin()); + } + } else if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) { + if (recoverable) { + store.dispatch(waitForOwner()); + } else { + store.dispatch(stopWaitForOwner()); + } } + break; } @@ -126,6 +138,9 @@ MiddlewareRegistry.register(store => next => action => { dispatch(setTokenAuthUrlSuccess(true)); } + if (_isWaitingForModerator(store)) { + store.dispatch(disableModeratorLogin()); + } if (_isWaitingForOwner(store)) { store.dispatch(stopWaitForOwner()); } @@ -134,6 +149,7 @@ MiddlewareRegistry.register(store => next => action => { } case CONFERENCE_LEFT: + store.dispatch(disableModeratorLogin()); store.dispatch(stopWaitForOwner()); break; @@ -236,7 +252,6 @@ function _clearExistingWaitForOwnerTimeout({ getState }: IStore) { waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID); } - /** * Checks if the cyclic "wait for conference owner" task is currently scheduled. * @@ -247,6 +262,16 @@ function _isWaitingForOwner({ getState }: IStore) { return Boolean(getState()['features/authentication'].waitForOwnerTimeoutID); } +/** + * Checks if the cyclic "wait for moderator" task is currently scheduled. + * + * @param {Object} store - The redux store. + * @returns {boolean} + */ +function _isWaitingForModerator({ getState }: IStore) { + return getState()['features/authentication'].showModeratorLogin; +} + /** * Handles login challenge. Opens login dialog or redirects to token auth URL. * diff --git a/react/features/authentication/reducer.ts b/react/features/authentication/reducer.ts index 958344fbea..077fd6aa25 100644 --- a/react/features/authentication/reducer.ts +++ b/react/features/authentication/reducer.ts @@ -4,6 +4,8 @@ import { assign } from '../base/redux/functions'; import { CANCEL_LOGIN, + DISABLE_MODERATOR_LOGIN, + ENABLE_MODERATOR_LOGIN, SET_TOKEN_AUTH_URL_SUCCESS, STOP_WAIT_FOR_OWNER, UPGRADE_ROLE_FINISHED, @@ -14,6 +16,7 @@ import { export interface IAuthenticationState { error?: Object | undefined; progress?: number | undefined; + showModeratorLogin?: boolean; thenableWithCancel?: { cancel: Function; }; @@ -45,6 +48,11 @@ ReducerRegistry.register('features/authentication', progress: undefined, thenableWithCancel: undefined }); + case ENABLE_MODERATOR_LOGIN: + return assign(state, { + showModeratorLogin: true + }); + case SET_TOKEN_AUTH_URL_SUCCESS: return assign(state, { tokenAuthUrlSuccessful: action.value @@ -56,6 +64,12 @@ ReducerRegistry.register('features/authentication', waitForOwnerTimeoutID: undefined }); + case DISABLE_MODERATOR_LOGIN: + return assign(state, { + error: undefined, + showModeratorLogin: false + }); + case UPGRADE_ROLE_FINISHED: { let { thenableWithCancel } = action; diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 4c18c6eeea..7188b0ae4f 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -464,6 +464,7 @@ export interface IConfig { lobby?: { autoKnock?: boolean; enableChat?: boolean; + showHangUp?: boolean; }; localRecording?: { disable?: boolean; diff --git a/react/features/base/premeeting/components/web/PreMeetingScreen.tsx b/react/features/base/premeeting/components/web/PreMeetingScreen.tsx index dd56a18d88..2d945627cb 100644 --- a/react/features/base/premeeting/components/web/PreMeetingScreen.tsx +++ b/react/features/base/premeeting/components/web/PreMeetingScreen.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { IReduxState } from '../../../../app/types'; +import { getLobbyConfig } from '../../../../lobby/functions'; import DeviceStatus from '../../../../prejoin/components/web/preview/DeviceStatus'; import { isRoomNameEnabled } from '../../../../prejoin/functions.web'; import Toolbox from '../../../../toolbox/components/web/Toolbox'; @@ -121,10 +122,9 @@ const useStyles = makeStyles()(theme => { alignItems: 'center', flexShrink: 0, boxSizing: 'border-box', - margin: '0 48px', padding: '24px 0 16px', position: 'relative', - width: '300px', + width: '400px', height: '100%', zIndex: 252, @@ -146,10 +146,21 @@ const useStyles = makeStyles()(theme => { contentControls: { display: 'flex', flexDirection: 'column', - alignItems: 'center', + alignItems: 'stretch', margin: 'auto', width: '100%' }, + paddedContent: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '0 50px', + + '& > *': { + width: '100%', + boxSizing: 'border-box' + } + }, title: { ...theme.typography.heading4, color: `${theme.palette.text01}!important`, @@ -220,34 +231,38 @@ const PreMeetingScreen = ({ {_isPreCallTestEnabled && }
-

- {title} -

- {_roomName && ( - - {isOverflowing ? ( - +
+

+ {title} +

+ {_roomName && ( + + {isOverflowing ? ( + + + {_roomName} + + + ) : ( {_roomName} - - ) : ( - - {_roomName} - - )} - - )} - {children} + )} + + )} + {children} +
{_buttons.length && } - {skipPrejoinButton} - {showUnsafeRoomWarning && } - {showDeviceStatus && } - {showRecordingWarning && } +
+ {skipPrejoinButton} + {showUnsafeRoomWarning && } + {showDeviceStatus && } + {showRecordingWarning && } +
@@ -269,10 +284,16 @@ const PreMeetingScreen = ({ function mapStateToProps(state: IReduxState, ownProps: Partial) { const { hiddenPremeetingButtons } = state['features/base/config']; const { toolbarButtons } = state['features/toolbox']; + const { showHangUp = true } = getLobbyConfig(state); + const { knocking } = state['features/lobby']; const premeetingButtons = (ownProps.thirdParty ? THIRD_PARTY_PREJOIN_BUTTONS : PREMEETING_BUTTONS).filter((b: any) => !(hiddenPremeetingButtons || []).includes(b)); + if (showHangUp && knocking && !premeetingButtons.includes('hangup')) { + premeetingButtons.push('hangup'); + } + const { premeetingBackground } = state['features/dynamic-branding']; return { diff --git a/react/features/lobby/components/AbstractLobbyScreen.tsx b/react/features/lobby/components/AbstractLobbyScreen.tsx index 380b8735d1..2f729ff4c9 100644 --- a/react/features/lobby/components/AbstractLobbyScreen.tsx +++ b/react/features/lobby/components/AbstractLobbyScreen.tsx @@ -13,6 +13,7 @@ import { updateSettings } from '../../base/settings/actions'; import { IMessage } from '../../chat/types'; import { isDeviceStatusVisible } from '../../prejoin/functions'; import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions'; +import { getLobbyConfig } from '../functions'; export const SCREEN_STATES = { EDIT: 1, @@ -27,6 +28,11 @@ export interface IProps { */ _deviceStatusVisible: boolean; + /** + * Whether to show the hangup button. + */ + _hangUp?: boolean; + /** * Indicates whether the message that display name is required is shown. */ @@ -52,6 +58,11 @@ export interface IProps { */ _lobbyMessageRecipient?: string; + /** + * Whether to hide the login button. + */ + _login?: boolean; + /** * The name of the meeting we're about to join. */ @@ -445,8 +456,10 @@ export function _mapStateToProps(state: IReduxState) { const { disableLobbyPassword } = getSecurityUiConfig(state); const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions; const deviceStatusVisible = isDeviceStatusVisible(state); + const { showHangUp = true } = getLobbyConfig(state); const { membersOnly, lobbyWaitingForHost } = state['features/base/conference']; const { isLobbyChatActive, lobbyMessageRecipient, messages } = state['features/chat']; + const { showModeratorLogin } = state['features/authentication']; return { _deviceStatusVisible: deviceStatusVisible, @@ -454,6 +467,8 @@ export function _mapStateToProps(state: IReduxState) { _knocking: knocking, _lobbyChatMessages: messages, _lobbyMessageRecipient: lobbyMessageRecipient?.name, + _login: showModeratorLogin, + _hangUp: showHangUp, _isLobbyChatActive: isLobbyChatActive, _meetingName: getConferenceName(state), _membersOnlyConference: membersOnly, diff --git a/react/features/lobby/components/native/LobbyScreen.tsx b/react/features/lobby/components/native/LobbyScreen.tsx index 8ffbb9da5d..e72e509943 100644 --- a/react/features/lobby/components/native/LobbyScreen.tsx +++ b/react/features/lobby/components/native/LobbyScreen.tsx @@ -3,6 +3,7 @@ import { Text, TextStyle, View, ViewStyle } from 'react-native'; import { connect } from 'react-redux'; import { IReduxState } from '../../../app/types'; +import { login } from '../../../authentication/actions.any'; import { getConferenceName } from '../../../base/conference/functions'; import { translate } from '../../../base/i18n/functions'; import JitsiScreen from '../../../base/modal/components/JitsiScreen'; @@ -18,6 +19,7 @@ import { navigate } from '../../../mobile/navigation/components/lobby/LobbyNavigationContainerRef'; import { screen } from '../../../mobile/navigation/routes'; import { preJoinStyles } from '../../../prejoin/components/native/styles'; +import HangupButton from '../../../toolbox/components/HangupButton'; import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton'; import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton'; import AbstractLobbyScreen, { @@ -43,6 +45,18 @@ interface IProps extends AbstractProps { * Implements a waiting screen that represents the participant being in the lobby. */ class LobbyScreen extends AbstractLobbyScreen { + /** + * Initializes a new LobbyScreen instance. + * + * @param {IProps} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: IProps) { + super(props); + + this._onLogin = this._onLogin.bind(this); + } + /** * Implements {@code PureComponent#render}. * @@ -197,12 +211,19 @@ class LobbyScreen extends AbstractLobbyScreen { * @inheritdoc */ _renderToolbarButtons() { + const { _hangUp } = this.props; + return ( + { + _hangUp + && + } ); } @@ -213,7 +234,7 @@ class LobbyScreen extends AbstractLobbyScreen { * @inheritdoc */ _renderStandardButtons() { - const { _knocking, _renderPassword, _isLobbyChatActive } = this.props; + const { _knocking, _renderPassword, _isLobbyChatActive, _login } = this.props; const { displayName } = this.state; return ( @@ -246,9 +267,28 @@ class LobbyScreen extends AbstractLobbyScreen { style = { preJoinStyles.joinButton } type = { BUTTON_TYPES.PRIMARY } /> } + { + _login + &&