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,112 @@
import {
appNavigate,
maybeRedirectToWelcomePage
} from '../app/actions';
import { IStore } from '../app/types';
import { conferenceLeft, setPassword } from '../base/conference/actions';
import { JITSI_CONFERENCE_URL_KEY } from '../base/conference/constants';
import { IJitsiConference } from '../base/conference/reducer';
import { hideDialog, openDialog } from '../base/dialog/actions';
import { SecurityDialog } from '../security/components/security-dialog';
import PasswordRequiredPrompt from './components/PasswordRequiredPrompt';
/**
* Cancels a prompt for a password to join a specific conference/room.
*
* @param {JitsiConference} conference - The {@code JitsiConference} requesting
* the password to join.
* @protected
* @returns {Function}
*/
export function _cancelPasswordRequiredPrompt(conference: any) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (typeof APP !== 'undefined') {
// when we are redirecting the library should handle any
// unload and clean of the connection.
APP.API.notifyReadyToClose();
dispatch(maybeRedirectToWelcomePage());
return;
}
// Canceling PasswordRequiredPrompt is to navigate the app/user to
// WelcomePage. In other words, the canceling invalidates the
// locationURL. Make sure that the canceling indeed has the intent to
// invalidate the locationURL.
const state = getState();
if (conference === state['features/base/conference'].passwordRequired
&& conference[JITSI_CONFERENCE_URL_KEY]
=== state['features/base/connection'].locationURL) {
// XXX The error associated with CONFERENCE_FAILED was marked as
// recoverable by the feature room-lock and, consequently,
// recoverable-aware features such as mobile's external-api did not
// deliver the CONFERENCE_FAILED to the SDK clients/consumers. Since
// the app/user is going to nativate to WelcomePage, the SDK
// clients/consumers need an event.
dispatch(conferenceLeft(conference));
dispatch(appNavigate(undefined));
}
};
}
/**
* Ends a (user) request to lock a specific conference/room.
*
* @param {JitsiConference} conference - The JitsiConference to lock.
* @param {string|undefined} password - The password with which the specified
* conference is to be locked or undefined to cancel the (user) request to lock
* the specified conference.
* @returns {Function}
*/
export function endRoomLockRequest(
conference: IJitsiConference,
password?: string) {
return (dispatch: IStore['dispatch']) => {
const setPassword_
= password
? dispatch(setPassword(conference, conference.lock, password))
: Promise.resolve();
const endRoomLockRequest_ = () => dispatch(hideDialog(SecurityDialog));
setPassword_.then(endRoomLockRequest_, endRoomLockRequest_);
};
}
/**
* Begins a prompt for a password to join a specific conference/room.
*
* @param {JitsiConference} conference - The {@code JitsiConference}
* requesting the password to join.
* @protected
* @returns {{
* type: OPEN_DIALOG,
* component: Component,
* props: PropTypes
* }}
*/
export function _openPasswordRequiredPrompt(conference: IJitsiConference) {
return openDialog(PasswordRequiredPrompt, { conference });
}
/**
* Unlocks the current jitsi conference.
*
* @returns {Function}
*/
export function unlockRoom() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { conference } = getState()['features/base/conference'];
return dispatch(setPassword(
conference,
conference?.lock,
''
));
};
}

View File

@@ -0,0 +1,165 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../app/types';
import { setPassword } from '../../base/conference/actions';
import { IJitsiConference } from '../../base/conference/reducer';
import InputDialog from '../../base/dialog/components/native/InputDialog';
import { _cancelPasswordRequiredPrompt } from '../actions';
/**
* {@code PasswordRequiredPrompt}'s React {@code Component} prop types.
*/
interface IProps {
/**
* The previously entered password, if any.
*/
_password?: string;
/**
* Number of digits used in the room-lock password.
*/
_passwordNumberOfDigits?: number;
/**
* The {@code JitsiConference} which requires a password.
*
* @type {JitsiConference}
*/
conference: IJitsiConference;
/**
* The redux dispatch function.
*/
dispatch: IStore['dispatch'];
}
interface IState {
/**
* The previously entered password, if any.
*/
password?: string;
}
/**
* Implements a React {@code Component} which prompts the user when a password
* is required to join a conference.
*/
class PasswordRequiredPrompt extends Component<IProps, IState> {
/**
* Initializes a new {@code PasswordRequiredPrompt} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
this.state = {
password: props._password
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
override componentDidUpdate() {
const { _password } = this.props;
// The previous password in Redux gets cleared after the dialog appears and it ends up breaking the dialog
// logic. We move the prop into state and only update it if it has an actual value, avoiding losing the
// previously received value when Redux updates.
if (_password && _password !== this.state.password) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
password: _password
});
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { password } = this.state;
const { _passwordNumberOfDigits } = this.props;
const textInputProps: any = {
secureTextEntry: true
};
if (_passwordNumberOfDigits) {
textInputProps.keyboardType = 'numeric';
textInputProps.maxLength = _passwordNumberOfDigits;
}
return (
<InputDialog
descriptionKey = 'dialog.passwordLabel'
initialValue = { password }
messageKey = { password ? 'dialog.incorrectRoomLockPassword' : undefined }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
textInputProps = { textInputProps }
titleKey = 'dialog.password' />
);
}
/**
* Notifies this prompt that it has been dismissed by cancel.
*
* @private
* @returns {boolean} If this prompt is to be closed/hidden, {@code true};
* otherwise, {@code false}.
*/
_onCancel() {
this.props.dispatch(
_cancelPasswordRequiredPrompt(this.props.conference));
return true;
}
/**
* Notifies this prompt that it has been dismissed by submitting a specific
* value.
*
* @param {string|undefined} value - The submitted value.
* @private
* @returns {boolean} If this prompt is to be closed/hidden, {@code true};
* otherwise, {@code false}.
*/
_onSubmit(value?: string) {
const { conference } = this.props;
this.props.dispatch(setPassword(conference, conference.join, value));
return true;
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { roomPasswordNumberOfDigits } = state['features/base/config'];
return {
_password: state['features/base/conference'].password,
_passwordNumberOfDigits: roomPasswordNumberOfDigits
};
}
export default connect(_mapStateToProps)(PasswordRequiredPrompt);

View File

@@ -0,0 +1,159 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IStore } from '../../app/types';
import { setPassword } from '../../base/conference/actions';
import { IJitsiConference } from '../../base/conference/reducer';
import { translate } from '../../base/i18n/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import Input from '../../base/ui/components/web/Input';
import { _cancelPasswordRequiredPrompt } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link PasswordRequiredPrompt}.
*/
interface IProps extends WithTranslation {
/**
* The JitsiConference which requires a password.
*/
conference: IJitsiConference;
/**
* The redux store's {@code dispatch} function.
*/
dispatch: IStore['dispatch'];
}
/**
* The type of the React {@code Component} state of
* {@link PasswordRequiredPrompt}.
*/
interface IState {
/**
* The password entered by the local participant.
*/
password?: string;
}
/**
* Implements a React Component which prompts the user when a password is
* required to join a conference.
*/
class PasswordRequiredPrompt extends Component<IProps, IState> {
override state = {
password: ''
};
/**
* Initializes a new PasswordRequiredPrompt instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onPasswordChanged = this._onPasswordChanged.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<Dialog
disableBackdropClose = { true }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'dialog.passwordRequired'>
{ this._renderBody() }
</Dialog>
);
}
/**
* Display component in dialog body.
*
* @returns {ReactElement}
* @protected
*/
_renderBody() {
return (
<div>
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'required-password-input'
label = { this.props.t('dialog.passwordLabel') }
name = 'lockKey'
onChange = { this._onPasswordChanged }
type = 'password'
value = { this.state.password } />
</div>
);
}
/**
* Notifies this dialog that password has changed.
*
* @param {string} value - The details of the notification/event.
* @private
* @returns {void}
*/
_onPasswordChanged(value: string) {
this.setState({
password: value
});
}
/**
* Dispatches action to cancel and dismiss this dialog.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(
_cancelPasswordRequiredPrompt(this.props.conference));
return true;
}
/**
* Dispatches action to submit value from this dialog.
*
* @private
* @returns {boolean}
*/
_onSubmit() {
const { conference } = this.props;
// We received that password is required, but user is trying anyway to
// login without a password. Mark the room as not locked in case she
// succeeds (maybe someone removed the password meanwhile). If it is
// still locked, another password required will be received and the room
// again will be marked as locked.
this.props.dispatch(
setPassword(conference, conference.join, this.state.password));
// We have used the password so let's clean it.
this.setState({
password: ''
});
return true;
}
}
export default translate(connect()(PasswordRequiredPrompt));

View File

@@ -0,0 +1,15 @@
/**
* The conference/room lock state which identifies that the password was set by
* the current/local participant/user.
*
* @type {string}
*/
export const LOCKED_LOCALLY = 'LOCKED_LOCALLY';
/**
* The conference/room lock state which identifies that the password was set by
* a remote participant/user.
*
* @type {string}
*/
export const LOCKED_REMOTELY = 'LOCKED_REMOTELY';

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/room-lock');

View File

@@ -0,0 +1,149 @@
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
LOCK_STATE_CHANGED,
SET_PASSWORD_FAILED
} from '../base/conference/actionTypes';
import { hideDialog } from '../base/dialog/actions';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { showErrorNotification, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { _openPasswordRequiredPrompt } from './actions';
import PasswordRequiredPrompt from './components/PasswordRequiredPrompt';
import { LOCKED_REMOTELY } from './constants';
import logger from './logger';
/**
* Middleware that captures conference failed and checks for password required
* error and requests a dialog for user to enter password.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case LOCK_STATE_CHANGED: {
const previousLockedState = store.getState()['features/base/conference'].locked;
const result = next(action);
const currentLockedState = store.getState()['features/base/conference'].locked;
if (currentLockedState === LOCKED_REMOTELY) {
store.dispatch(
showNotification({
titleKey: 'notify.passwordSetRemotely'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
} else if (previousLockedState === LOCKED_REMOTELY && !currentLockedState) {
store.dispatch(
showNotification({
titleKey: 'notify.passwordRemovedRemotely'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
return result;
}
case SET_PASSWORD_FAILED:
return _setPasswordFailed(store, next, action);
}
return next(action);
});
/**
* Handles cleanup of lock prompt state when a conference is joined.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which
* specifies the details associated with joining the conference.
* @private
* @returns {*}
*/
function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) {
dispatch(hideDialog(PasswordRequiredPrompt));
return next(action);
}
/**
* Handles errors that occur when a conference fails.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code CONFERENCE_FAILED} which
* specifies the details associated with the error and the failed conference.
* @private
* @returns {*}
*/
function _conferenceFailed({ dispatch }: IStore, next: Function, action: AnyAction) {
const { conference, error } = action;
if (conference && error.name === JitsiConferenceErrors.PASSWORD_REQUIRED) {
// XXX The feature room-lock affords recovery after CONFERENCE_FAILED
// caused by JitsiConferenceErrors.PASSWORD_REQUIRED.
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
if (error.recoverable) {
dispatch(_openPasswordRequiredPrompt(conference));
}
} else {
dispatch(hideDialog(PasswordRequiredPrompt));
}
return next(action);
}
/**
* Handles errors that occur when a password fails to be set.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code SET_PASSWORD_ERROR} which
* has the error type that should be handled.
* @private
* @returns {*}
*/
function _setPasswordFailed(store: IStore, next: Function, action: AnyAction) {
if (typeof APP !== 'undefined') {
// TODO Remove this logic when displaying of error messages on web is
// handled through react/redux.
const { error } = action;
let descriptionKey;
let titleKey;
if (error === JitsiConferenceErrors.PASSWORD_NOT_SUPPORTED) {
logger.warn('room passwords not supported');
descriptionKey = 'dialog.passwordNotSupported';
titleKey = 'dialog.passwordNotSupportedTitle';
} else {
logger.warn('setting password failed', error);
descriptionKey = 'dialog.lockMessage';
titleKey = 'dialog.lockTitle';
}
APP.store.dispatch(showErrorNotification({
descriptionKey,
titleKey
}));
}
return next(action);
}