feat(lobby) integrate login in lobby + configs (#16401)

* feat(lobby) integrate login in lobby + configs

* fixed toolboxContainer styles, used HangupButton

* make hangup button visible by default

* use hangup button

* feat(prejoin): fixed indent, import extension

* squash: Restore back wait for owner dialog.

* squash: Drops not used state and functions.

---------

Co-authored-by: Calin-Teodor <calin.chitu@8x8.com>
Co-authored-by: damencho <damencho@jitsi.org>
This commit is contained in:
Mihaela Dumitru
2025-09-22 16:56:29 +03:00
committed by GitHub
parent 566b3ba2d5
commit 6b6920693b
16 changed files with 255 additions and 47 deletions

View File

@@ -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

View File

@@ -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.
*

View File

@@ -67,7 +67,7 @@ class WaitForOwnerDialog extends Component<IProps> {
<ConfirmDialog
cancelLabel = { this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }
confirmLabel = 'dialog.IamHost'
descriptionKey = 'dialog.WaitForHostMsg'
descriptionKey = 'lobby.waitForModerator'
isConfirmHidden = { _isConfirmHidden }
onCancel = { this._onCancel }
onSubmit = { this._onLogin } />

View File

@@ -91,7 +91,7 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
onSubmit = { this._onIAmHost }
titleKey = { t('dialog.WaitingForHostTitle') }>
<span>
{ this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('dialog.WaitForHostMsg') }
{ this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('lobby.waitForModerator') }
</span>
</Dialog>
);

View File

@@ -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.
*

View File

@@ -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<IAuthenticationState>('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<IAuthenticationState>('features/authentication',
waitForOwnerTimeoutID: undefined
});
case DISABLE_MODERATOR_LOGIN:
return assign(state, {
error: undefined,
showModeratorLogin: false
});
case UPGRADE_ROLE_FINISHED: {
let { thenableWithCancel } = action;

View File

@@ -464,6 +464,7 @@ export interface IConfig {
lobby?: {
autoKnock?: boolean;
enableChat?: boolean;
showHangUp?: boolean;
};
localRecording?: {
disable?: boolean;

View File

@@ -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 && <ConnectionStatus />}
<div className = { classes.contentControls }>
<h1 className = { classes.title }>
{title}
</h1>
{_roomName && (
<span className = { classes.roomNameContainer }>
{isOverflowing ? (
<Tooltip content = { _roomName }>
<div className = { classes.paddedContent }>
<h1 className = { classes.title }>
{title}
</h1>
{_roomName && (
<span className = { classes.roomNameContainer }>
{isOverflowing ? (
<Tooltip content = { _roomName }>
<span
className = { classes.roomName }
ref = { roomNameRef }>
{_roomName}
</span>
</Tooltip>
) : (
<span
className = { classes.roomName }
ref = { roomNameRef }>
{_roomName}
</span>
</Tooltip>
) : (
<span
className = { classes.roomName }
ref = { roomNameRef }>
{_roomName}
</span>
)}
</span>
)}
{children}
)}
</span>
)}
{children}
</div>
{_buttons.length && <Toolbox toolbarButtons = { _buttons } />}
{skipPrejoinButton}
{showUnsafeRoomWarning && <UnsafeRoomWarning />}
{showDeviceStatus && <DeviceStatus />}
{showRecordingWarning && <RecordingWarning />}
<div className = { classes.paddedContent }>
{skipPrejoinButton}
{showUnsafeRoomWarning && <UnsafeRoomWarning />}
{showDeviceStatus && <DeviceStatus />}
{showRecordingWarning && <RecordingWarning />}
</div>
</div>
</div>
</div>
@@ -269,10 +284,16 @@ const PreMeetingScreen = ({
function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
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 {

View File

@@ -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,

View File

@@ -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<IProps> {
/**
* 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<IProps> {
* @inheritdoc
*/
_renderToolbarButtons() {
const { _hangUp } = this.props;
return (
<View style = { preJoinStyles.toolboxContainer as ViewStyle }>
<AudioMuteButton
styles = { preJoinStyles.buttonStylesBorderless } />
<VideoMuteButton
styles = { preJoinStyles.buttonStylesBorderless } />
{
_hangUp
&& <HangupButton
styles = { preJoinStyles.buttonStylesBorderless } />
}
</View>
);
}
@@ -213,7 +234,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
* @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<IProps> {
style = { preJoinStyles.joinButton }
type = { BUTTON_TYPES.PRIMARY } />
}
{
_login
&& <Button
accessibilityLabel = 'dialog.IamHost'
labelKey = 'dialog.IamHost'
onClick = { this._onLogin }
style = { preJoinStyles.joinButton }
type = { BUTTON_TYPES.PRIMARY } />
}
</View>
);
}
/**
* Handles login button click.
*
* @private
* @returns {void}
*/
_onLogin() {
this.props.dispatch(login());
}
}
/**

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { login } from '../../../authentication/actions.any';
import { leaveConference } from '../../../base/conference/actions';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
@@ -8,6 +10,7 @@ import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeeting
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 { BUTTON_TYPES } from '../../../base/ui/constants.any';
import ChatInput from '../../../chat/components/web/ChatInput';
import MessageContainer from '../../../chat/components/web/MessageContainer';
import AbstractLobbyScreen, {
@@ -35,6 +38,10 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
super(props);
this._messageContainerRef = React.createRef<MessageContainer>();
// Bind authentication methods
this._onLogin = this._onLogin.bind(this);
this._onHangup = this._onHangup.bind(this);
}
/**
@@ -84,7 +91,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
* @inheritdoc
*/
override _renderJoining() {
const { _isLobbyChatActive } = this.props;
const { _login, _isLobbyChatActive } = this.props;
return (
<div className = 'lobby-screen-content'>
@@ -96,7 +103,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
<LoadingIndicator size = 'large' />
</div>
<span className = 'joining-message'>
{ this.props.t('lobby.joiningMessage') }
{ this.props.t(_login ? 'lobby.waitForModerator' : 'lobby.joiningMessage') }
</span>
</>
)}
@@ -215,14 +222,14 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
labelKey = 'prejoin.joinMeeting'
onClick = { this._onJoinWithPassword }
testId = 'lobby.passwordJoinButton'
type = 'primary' />
type = { BUTTON_TYPES.PRIMARY } />
<Button
className = 'lobby-button-margin'
fullWidth = { true }
labelKey = 'lobby.backToKnockModeButton'
onClick = { this._onSwitchToKnockMode }
testId = 'lobby.backToKnockModeButton'
type = 'secondary' />
type = { BUTTON_TYPES.SECONDARY } />
</>
);
}
@@ -233,7 +240,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
* @inheritdoc
*/
override _renderStandardButtons() {
const { _knocking, _isLobbyChatActive, _renderPassword } = this.props;
const { _knocking, _login, _isLobbyChatActive, _renderPassword } = this.props;
return (
<>
@@ -260,7 +267,15 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
labelKey = 'lobby.enterPasswordButton'
onClick = { this._onSwitchToPasswordMode }
testId = 'lobby.enterPasswordButton'
type = 'secondary' />
type = { BUTTON_TYPES.SECONDARY } />
}
{_login && <Button
className = 'lobby-button-margin'
fullWidth = { true }
labelKey = 'dialog.IamHost'
onClick = { this._onLogin }
testId = 'lobby.loginButton'
type = { BUTTON_TYPES.PRIMARY } />
}
</>
);
@@ -279,6 +294,26 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
this._messageContainerRef.current.scrollToElement(withAnimation, null);
}
}
/**
* Handles login button click.
*
* @private
* @returns {void}
*/
_onLogin() {
this.props.dispatch(login());
}
/**
* Handles hangup button click.
*
* @private
* @returns {void}
*/
_onHangup() {
this.props.dispatch(leaveConference());
}
}
export default translate(connect(_mapStateToProps)(LobbyScreen));

View File

@@ -33,12 +33,14 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { openDisplayNamePrompt } from '../../../display-name/actions';
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
import LargeVideo from '../../../large-video/components/LargeVideo.native';
import { getLobbyConfig } from '../../../lobby/functions';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import HangupButton from '../../../toolbox/components/HangupButton';
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions';
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions.native';
import { IPrejoinProps } from '../../types';
import { hasDisplayName } from '../../utils';
@@ -59,6 +61,8 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
const isDisplayNameReadonly = useSelector(isNameReadOnly);
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
const { showHangUp = true } = useSelector((state: IReduxState) => getLobbyConfig(state));
const { knocking } = useSelector((state: IReduxState) => state['features/lobby']);
const participantName = localParticipant?.name;
const [ displayName, setDisplayName ]
= useState(participantName || '');
@@ -185,6 +189,11 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
{
showHangUp && knocking
&& <HangupButton
styles = { styles.buttonStylesBorderless } />
}
</View>
{
showDisplayNameInput && <Input

View File

@@ -74,8 +74,8 @@ export const preJoinStyles = {
height: 60,
justifyContent: 'space-between',
marginBottom: BaseTheme.spacing[3],
paddingHorizontal: BaseTheme.spacing[2],
width: 148
padding: BaseTheme.spacing[2],
width: 'auto'
},
customInput: {

View File

@@ -228,7 +228,7 @@ class Whiteboard extends PureComponent<IProps> {
/**
* Maps (parts of) the redux state to the associated
* {@code WaitForOwnerDialog}'s props.
* {@code Whiteboard}'s props.
*
* @param {Object} state - The redux state.
* @private