feat(info): new dialog design (#2452)

* feat(info): new dialog design

- Add display of a dial in number.
- Add a static page to show a full list of dial in numbers.
- Add password management.
- Invite modal will be changed soon to remove password and
  dial-in.

* squash: add classes for torture tests

* squash: class for local lock for torture tests

* squash: more classes for torture tests

* squash: more classes, work around linter

* squash: remove unused string?

* squash: work around linter and avoid react warnings

* squash: pixel push, add bold

* squash: font size bump

* squash: NumbersTable -> NumbersList

* squash: document response from fetching numbers

* squash: showEdit -> editEnabled, pixel push padding for alignment

* squash: pin -> conferenceID

* squash: prepare to receive defaultCountry from api
This commit is contained in:
virtuacoplenny
2018-02-13 13:46:47 -06:00
committed by yanas
parent 0bbcd3181c
commit 59d046dca9
25 changed files with 1274 additions and 205 deletions
@@ -1,199 +0,0 @@
/* global interfaceConfig */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getInviteURL } from '../../base/connection';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import AddPeopleDialog from './AddPeopleDialog';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A React Component with the contents for a dialog that shows information about
* the current conference and provides ways to invite other participants.
*
* @extends Component
*/
class InfoDialog extends Component {
/**
* {@code InfoDialog} component's property types.
*
* @static
*/
static propTypes = {
/**
* The current url of the conference to be copied onto the clipboard.
*/
_inviteURL: PropTypes.string,
/**
* Whether or not the link to open the {@code AddPeopleDialog} should be
* displayed.
*/
_showAddPeople: PropTypes.bool,
/**
* Invoked to open a dialog for adding participants to the conference.
*/
dispatch: PropTypes.func,
/**
* Callback invoked when the dialog should be closed.
*/
onClose: PropTypes.func,
/**
* Callback invoked when a mouse-related event has been detected.
*/
onMouseOver: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes new {@code InfoDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The internal reference to the DOM/HTML element backing the React
* {@code Component} input. It is necessary for the implementation
* of copying to the clipboard.
*
* @private
* @type {HTMLInputElement}
*/
this._copyElement = null;
// Bind event handlers so they are only bound once for every instance.
this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
this._onOpenInviteDialog = this._onOpenInviteDialog.bind(this);
this._setCopyElement = this._setCopyElement.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'info-dialog'
onMouseOver = { this.props.onMouseOver } >
<div className = 'info-dialog-column'>
<h4 className = 'info-dialog-icon'>
<i className = 'icon-info' />
</h4>
</div>
<div className = 'info-dialog-column'>
<div className = 'info-dialog-title'>
{ this.props.t('info.title') }
</div>
<div
className = 'info-dialog-conference-url'
ref = { this._inviteUrlElement }>
{ this.props._inviteURL }
<input
className = 'info-dialog-copy-element'
readOnly = { true }
ref = { this._setCopyElement }
tabIndex = '-1'
value = { this.props._inviteURL } />
</div>
<div className = 'info-dialog-action-links'>
<div className = 'info-dialog-action-link'>
<a onClick = { this._onCopyInviteURL }>
{ this.props.t('info.copy') }
</a>
</div>
{ this.props._showAddPeople
? <div className = 'info-dialog-action-link'>
<a onClick = { this._onOpenInviteDialog }>
{ this.props.t('info.invite', {
app: interfaceConfig.ADD_PEOPLE_APP_NAME
}) }
</a>
</div>
: null }
</div>
</div>
</div>
);
}
/**
* Callback invoked to copy the contents of {@code this._copyElement} to the
* clipboard.
*
* @private
* @returns {void}
*/
_onCopyInviteURL() {
try {
this._copyElement.select();
document.execCommand('copy');
this._copyElement.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
/**
* Callback invoked to open the {@code AddPeople} dialog.
*
* @private
* @returns {void}
*/
_onOpenInviteDialog() {
this.props.dispatch(openDialog(AddPeopleDialog));
if (this.props.onClose) {
this.props.onClose();
}
}
/**
* Sets the internal reference to the DOM/HTML element backing the React
* {@code Component} input.
*
* @param {HTMLInputElement} element - The DOM/HTML element for this
* {@code Component}'s input.
* @private
* @returns {void}
*/
_setCopyElement(element) {
this._copyElement = element;
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code InfoDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _inviteURL: string
* }}
*/
function _mapStateToProps(state) {
return {
_inviteURL: getInviteURL(state),
_showAddPeople: !state['features/base/jwt'].isGuest
};
}
export default translate(connect(_mapStateToProps)(InfoDialog));
@@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox';
import { setInfoDialogVisibility } from '../actions';
import InfoDialog from './InfoDialog';
import { InfoDialog } from './info-dialog';
const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig;
@@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
/**
* Displays a conference ID used as a pin for dialing into a conferene.
*
* @extends Component
*/
class ConferenceID extends Component {
/**
* {@code ConferenceID} component's property types.
*
* @static
*/
static propTypes = {
/**
* The conference ID for dialing in.
*/
conferenceID: PropTypes.number,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { conferenceID, t } = this.props;
return (
<div className = 'dial-in-conference-id'>
{ t('info.dialANumber', { conferenceID }) }
</div>
);
}
}
export default translate(ConferenceID);
@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import parseURLParams from '../../../base/config/parseURLParams';
import { i18next } from '../../../base/i18n';
import DialInInfoPage from './DialInInfoPage';
document.addEventListener('DOMContentLoaded', () => {
const params = parseURLParams(window.location, true, 'search');
ReactDOM.render(
<I18nextProvider i18n = { i18next }>
<DialInInfoPage
room = { params.room } />
</I18nextProvider>,
document.getElementById('react')
);
});
window.addEventListener('beforeunload', () => {
ReactDOM.unmountComponentAtNode(document.getElementById('react'));
});
@@ -0,0 +1,220 @@
/* global config */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import ConferenceID from './ConferenceID';
import NumbersList from './NumbersList';
/**
* Displays a page listing numbers for dialing into a conference and pin to
* the a specific conference.
*
* @extends Component
*/
class DialInInfoPage extends Component {
/**
* {@code DialInInfoPage} component's property types.
*
* @static
*/
static propTypes = {
/**
* The name of the conference to show a conferenceID for.
*/
room: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* {@code DialInInfoPage} component's local state.
*
* @type {Object}
* @property {number} conferenceID - The numeric ID of the conference, used
* as a pin when dialing in.
* @property {string} error - An error message to display.
* @property {boolean} loading - Whether or not the app is fetching data.
* @property {Array|Object} numbers - The dial-in numbers.
* entered by the local participant.
* @property {boolean} numbersEnabled - Whether or not dial-in is allowed.
*/
state = {
conferenceID: null,
error: '',
loading: true,
numbers: null,
numbersEnabled: null
};
/**
* Initializes a new {@code DialInInfoPage} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onGetNumbersSuccess = this._onGetNumbersSuccess.bind(this);
this._onGetConferenceIDSuccess
= this._onGetConferenceIDSuccess.bind(this);
this._setErrorMessage = this._setErrorMessage.bind(this);
}
/**
* Implements {@link Component#componentDidMount()}. Invoked immediately
* after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
const getNumbers = this._getNumbers()
.then(this._onGetNumbersSuccess)
.catch(this._setErrorMessage);
const getID = this._getConferenceID()
.then(this._onGetConferenceIDSuccess)
.catch(this._setErrorMessage);
Promise.all([ getNumbers, getID ])
.then(() => {
this.setState({ loading: false });
});
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
let contents;
const { conferenceID, error, loading, numbersEnabled } = this.state;
if (loading) {
contents = '';
} else if (numbersEnabled === false) {
contents = this.props.t('invite.disabled');
} else if (error) {
contents = error;
} else {
contents = [
conferenceID
? <ConferenceID
conferenceID = { conferenceID }
key = 'conferenceID' />
: null,
<NumbersList
key = 'numbers'
numbers = { this.state.numbers } />
];
}
return (
<div className = 'dial-in-page'>
{ contents }
</div>
);
}
/**
* Creates an AJAX request for the conference ID.
*
* @private
* @returns {Promise}
*/
_getConferenceID() {
const { room } = this.props;
const { dialInConfCodeUrl, hosts } = config;
const mucURL = hosts && hosts.muc;
if (!dialInConfCodeUrl || !mucURL || !room) {
return Promise.resolve();
}
const conferenceIDURL
= `${dialInConfCodeUrl}?conference=${room}@${mucURL}`;
return fetch(conferenceIDURL)
.then(response => response.json())
.catch(() => Promise.reject(this.props.t('info.genericError')));
}
/**
* Creates an AJAX request for dial-in numbers.
*
* @private
* @returns {Promise}
*/
_getNumbers() {
const { dialInNumbersUrl } = config;
if (!dialInNumbersUrl) {
return Promise.reject(this.props.t('info.dialInNotSupported'));
}
return fetch(dialInNumbersUrl)
.then(response => response.json())
.catch(() => Promise.reject(this.props.t('info.genericError')));
}
/**
* Callback invoked when fetching the conference ID succeeds.
*
* @param {Object} response - The response from fetching the conference ID.
* @private
* @returns {void}
*/
_onGetConferenceIDSuccess(response = {}) {
const { conference, id } = response;
if (!conference || !id) {
return;
}
this.setState({ conferenceID: id });
}
/**
* Callback invoked when fetching dial-in numbers succeeds. Sets the
* internal to show the numbers.
*
* @param {Object} response - The response from fetching dial-in numbers.
* @param {Array|Object} response.numbers - The dial-in numbers.
* @param {boolean} reponse.numbersEnabled - Whether or not dial-in is
* enabled.
* @private
* @returns {void}
*/
_onGetNumbersSuccess({ numbers, numbersEnabled }) {
this.setState({
numbersEnabled,
numbers
});
}
/**
* Sets an error message to display on the page instead of content.
*
* @param {string} error - The error message to display.
* @private
* @returns {void}
*/
_setErrorMessage(error) {
this.setState({
error
});
}
}
export default translate(DialInInfoPage);
@@ -0,0 +1,120 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
/**
* Displays a table with phone numbers to dial in to a conference.
*
* @extends Component
*/
class NumbersList extends Component {
/**
* {@code NumbersList} component's property types.
*
* @static
*/
static propTypes = {
/**
* The phone numbers to display. Can be an array of numbers
* or an object with countries as keys and an array of numbers
* as values.
*/
numbers: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { numbers, t } = this.props;
const showWithoutCountries = Array.isArray(numbers);
return (
<table className = 'dial-in-numbers-list'>
<thead>
<tr>
{ showWithoutCountries
? null
: <th>{ t('info.country') }</th> }
<th>{ t('info.numbers') }</th>
</tr>
</thead>
<tbody>
{ showWithoutCountries
? numbers.map(this._renderNumberRow)
: this._renderWithCountries() }
</tbody>
</table>);
}
/**
* Renders rows of countries and associated phone numbers.
*
* @private
* @returns {ReactElement[]}
*/
_renderWithCountries() {
const rows = [];
for (const [ country, numbers ] of Object.entries(this.props.numbers)) {
const formattedNumbers = numbers.map(this._renderNumberDiv);
rows.push(
<tr key = { country }>
<td>{ country }</td>
<td className = 'dial-in-numbers'>{ formattedNumbers }</td>
</tr>
);
}
return rows;
}
/**
* Renders a table row for a phone number.
*
* @param {string} number - The phone number to display.
* @private
* @returns {ReactElement[]}
*/
_renderNumberRow(number) {
return (
<tr key = { number }>
<td className = 'dial-in-number'>
{ number }
</td>
</tr>
);
}
/**
* Renders a div container for a phone number.
*
* @param {string} number - The phone number to display.
* @private
* @returns {ReactElement[]}
*/
_renderNumberDiv(number) {
return (
<div
className = 'dial-in-number'
key = { number }>
{ number }
</div>
);
}
}
export default translate(NumbersList);
@@ -0,0 +1 @@
export { default as DialInInfoApp } from './DialInInfoApp';
@@ -0,0 +1,60 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from '../../../base/i18n';
/**
* React {@code Component} responsible for displaying a telephone number and
* conference ID for dialing into a conference.
*
* @extends Component
*/
class DialInNumber extends Component {
/**
* {@code DialInNumber} component's property types.
*
* @static
*/
static propTypes = {
/**
* The numberic identifier for the current conference, used after
* dialing a the number to join the conference.
*/
conferenceID: PropTypes.number,
/**
* The phone number to dial to begin the process of dialing into a
* conference.
*/
phoneNumber: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { conferenceID, phoneNumber } = this.props;
return (
<div className = 'dial-in-number'>
<span className = 'phone-number'>
{ this.props.t('info.dialInNumber', { phoneNumber }) }
</span>
<span className = 'conference-id'>
{ this.props.t(
'info.dialInConferenceID', { conferenceID }) }
</span>
</div>
);
}
}
export default translate(DialInNumber);
@@ -0,0 +1,503 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setPassword } from '../../../base/conference';
import { getInviteURL } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import {
PARTICIPANT_ROLE,
getLocalParticipant
} from '../../../base/participants';
import { updateDialInNumbers } from '../../actions';
import DialInNumber from './DialInNumber';
import PasswordForm from './PasswordForm';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A React Component with the contents for a dialog that shows information about
* the current conference.
*
* @extends Component
*/
class InfoDialog extends Component {
/**
* {@code InfoDialog} component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the current user can modify the current password.
*/
_canEditPassword: PropTypes.bool,
/**
* The JitsiConference for which to display a lock state and change the
* password.
*
* @type {JitsiConference}
*/
_conference: PropTypes.object,
/**
* The name of the current conference. Used as part of inviting users.
*/
_conferenceName: PropTypes.string,
/**
* The redux state representing the dial-in numbers feature.
*/
_dialIn: PropTypes.object,
/**
* The current url of the conference to be copied onto the clipboard.
*/
_inviteURL: PropTypes.string,
/**
* The value for how the conference is locked (or undefined if not
* locked) as defined by room-lock constants.
*/
_locked: PropTypes.string,
/**
* The current known password for the JitsiConference.
*/
_password: PropTypes.string,
/**
* Invoked to open a dialog for adding participants to the conference.
*/
dispatch: PropTypes.func,
/**
* Callback invoked when the dialog should be closed.
*/
onClose: PropTypes.func,
/**
* Callback invoked when a mouse-related event has been detected.
*/
onMouseOver: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* {@code InfoDialog} component's local state.
*
* @type {Object}
* @property {boolean} passwordEditEnabled - Whether or not to show the
* {@code PasswordForm} in its editing state.
* @property {string} phoneNumber - The number to display for dialing into
* the conference.
*/
state = {
passwordEditEnabled: false,
phoneNumber: ''
};
/**
* Initializes new {@code InfoDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
const { defaultCountry, numbers } = props._dialIn;
if (numbers) {
this.state.phoneNumber
= this._getDefaultPhoneNumber(numbers, defaultCountry);
}
/**
* The internal reference to the DOM/HTML element backing the React
* {@code Component} text area. It is necessary for the implementation
* of copying to the clipboard.
*
* @private
* @type {HTMLTextAreaElement}
*/
this._copyElement = null;
// Bind event handlers so they are only bound once for every instance.
this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
this._onPasswordRemove = this._onPasswordRemove.bind(this);
this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
this._onTogglePasswordEditState
= this._onTogglePasswordEditState.bind(this);
this._setCopyElement = this._setCopyElement.bind(this);
}
/**
* Implements {@link Component#componentDidMount()}. Invoked immediately
* after this component is mounted. Requests dial-in numbers if not
* already known.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (!this.state.phoneNumber) {
this.props.dispatch(updateDialInNumbers());
}
}
/**
* Implements React's {@link Component#componentWillReceiveProps()}. Invoked
* before this mounted component receives new props.
*
* @inheritdoc
* @param {Props} nextProps - New props component will receive.
*/
componentWillReceiveProps(nextProps) {
if (!this.props._password && nextProps._password) {
this.setState({ passwordEditEnabled: false });
}
if (!this.state.phoneNumber && nextProps._dialIn.numbers) {
const { defaultCountry, numbers } = nextProps._dialIn;
this.setState({
phoneNumber:
this._getDefaultPhoneNumber(numbers, defaultCountry)
});
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { onMouseOver, t } = this.props;
return (
<div
className = 'info-dialog'
onMouseOver = { onMouseOver } >
<div className = 'info-dialog-column'>
<h4 className = 'info-dialog-icon'>
<i className = 'icon-info' />
</h4>
</div>
<div className = 'info-dialog-column'>
<div className = 'info-dialog-title'>
{ t('info.title') }
</div>
<div className = 'info-dialog-conference-url'>
{ t('info.conferenceURL',
{ url: this._getURLToDisplay() }) }
<textarea
className = 'info-dialog-copy-element'
readOnly = { true }
ref = { this._setCopyElement }
tabIndex = '-1'
value = { this._getTextToCopy() } />
</div>
<div className = 'info-dialog-dial-in'>
{ this._renderDialInDisplay() }
</div>
<div className = 'info-dialog-password'>
<PasswordForm
editEnabled = { this.state.passwordEditEnabled }
locked = { this.props._locked }
onSubmit = { this._onPasswordSubmit }
password = { this.props._password } />
</div>
<div className = 'info-dialog-action-links'>
<div className = 'info-dialog-action-link'>
<a
className = 'info-copy'
onClick = { this._onCopyInviteURL }>
{ t('dialog.copy') }
</a>
</div>
{ this._renderPasswordAction() }
</div>
</div>
</div>
);
}
/**
* Sets the internal state of which dial-in number to display.
*
* @param {Array<string>|Object} dialInNumbers - The array or object of
* numbers to choose a number from.
* @param {string} defaultCountry - The country code for the country
* whose phone number should display.
* @private
* @returns {string|null}
*/
_getDefaultPhoneNumber(dialInNumbers, defaultCountry = 'US') {
if (Array.isArray(dialInNumbers)) {
// Dumbly return the first number if an array.
return dialInNumbers[0];
} else if (Object.keys(dialInNumbers).length > 0) {
const defaultNumbers = dialInNumbers[defaultCountry];
if (defaultNumbers) {
return defaultNumbers[0];
}
const firstRegion = Object.keys(dialInNumbers)[0];
return firstRegion && firstRegion[0];
}
return null;
}
/**
* Creates a message describing how to dial in to the conference.
*
* @private
* @returns {string}
*/
_getTextToCopy() {
const { _conferenceName, t } = this.props;
let invite = t('info.inviteURL', {
url: this.props._inviteURL
});
if (this._shouldDisplayDialIn()) {
const dial = t('info.invitePhone', {
number: this.state.phoneNumber,
conferenceID: this.props._dialIn.conferenceID
});
const moreNumbers = t('info.invitePhoneAlternatives', {
url: `${window.location.origin}/static/dialInInfo.html?room=${
encodeURIComponent(_conferenceName)}`
});
invite = `${invite}\n${dial}\n${moreNumbers}`;
}
return invite;
}
/**
* Modifies the inviteURL for display in the modal.
*
* @private
* @returns {string}
*/
_getURLToDisplay() {
return this.props._inviteURL.replace(/^https?:\/\//i, '');
}
/**
* Callback invoked to copy the contents of {@code this._copyElement} to the
* clipboard.
*
* @private
* @returns {void}
*/
_onCopyInviteURL() {
try {
this._copyElement.select();
document.execCommand('copy');
this._copyElement.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
/**
* Callback invoked to unlock the current JitsiConference.
*
* @private
* @returns {void}
*/
_onPasswordRemove() {
this._onPasswordSubmit('');
}
/**
* Callback invoked to set a password on the current JitsiConference.
*
* @param {string} enteredPassword - The new password to be used to lock the
* current JitsiConference.
* @private
* @returns {void}
*/
_onPasswordSubmit(enteredPassword) {
const { _conference } = this.props;
this.props.dispatch(setPassword(
_conference,
_conference.lock,
enteredPassword
));
}
/**
* Toggles whether or not the password should currently be shown as being
* edited locally.
*
* @private
* @returns {void}
*/
_onTogglePasswordEditState() {
this.setState({
passwordEditEnabled: !this.state.passwordEditEnabled
});
}
/**
* Returns a ReactElement for showing how to dial into the conference, if
* dialing in is available.
*
* @private
* @returns {null|ReactElement}
*/
_renderDialInDisplay() {
if (!this._shouldDisplayDialIn()) {
return null;
}
return (
<div>
<DialInNumber
conferenceID = { this.props._dialIn.conferenceID }
phoneNumber = { this.state.phoneNumber } />
<a
className = 'more-numbers'
href = { `static/dialInInfo.html?room=${
encodeURIComponent(this.props._conferenceName)}` }
rel = 'noopener noreferrer'
target = '_blank'>
{ this.props.t('info.moreNumbers') }
</a>
</div>
);
}
/**
* Returns a ReactElement for interacting with the password field.
*
* @private
* @returns {null|ReactElement}
*/
_renderPasswordAction() {
const { t } = this.props;
let className, onClick, textKey;
if (!this.props._canEditPassword) {
// intentionally left blank to prevent rendering anything
} else if (this.state.passwordEditEnabled) {
className = 'cancel-password';
onClick = this._onTogglePasswordEditState;
textKey = 'info.cancelPassword';
} else if (this.props._locked) {
className = 'remove-password';
onClick = this._onPasswordRemove;
textKey = 'dialog.removePassword';
} else {
className = 'add-password';
onClick = this._onTogglePasswordEditState;
textKey = 'invite.addPassword';
}
return className && onClick && textKey
? <div className = 'info-dialog-action-link'>
<a
className = { className }
onClick = { onClick }>
{ t(textKey) }
</a>
</div>
: null;
}
/**
* Returns whether or not dial-in related UI should be displayed.
*
* @private
* @returns {boolean}
*/
_shouldDisplayDialIn() {
const { conferenceID, numbers, numbersEnabled } = this.props._dialIn;
const { phoneNumber } = this.state;
return Boolean(
conferenceID
&& numbers
&& numbersEnabled
&& phoneNumber);
}
/**
* Sets the internal reference to the DOM/HTML element backing the React
* {@code Component} input.
*
* @param {HTMLInputElement} element - The DOM/HTML element for this
* {@code Component}'s input.
* @private
* @returns {void}
*/
_setCopyElement(element) {
this._copyElement = element;
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code InfoDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _canEditPassword: boolean,
* _conference: Object,
* _conferenceName: string,
* _dialIn: Object,
* _inviteURL: string,
* _locked: string,
* _password: string
* }}
*/
function _mapStateToProps(state) {
const {
conference,
locked,
password,
room
} = state['features/base/conference'];
const isModerator
= getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR;
let canEditPassword;
if (state['features/base/config'].enableUserRolesBasedOnToken) {
canEditPassword = isModerator && !state['features/base/jwt'].isGuest;
} else {
canEditPassword = isModerator;
}
return {
_canEditPassword: canEditPassword,
_conference: conference,
_conferenceName: room,
_dialIn: state['features/invite'],
_inviteURL: getInviteURL(state),
_locked: locked,
_password: password
};
}
export default translate(connect(_mapStateToProps)(InfoDialog));
@@ -0,0 +1,175 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../room-lock';
/**
* React {@code Component} for displaying and editing the conference password.
*
* @extends Component
*/
class PasswordForm extends Component {
/**
* {@code PasswordForm} component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not to show the password editing field.
*/
editEnabled: PropTypes.bool,
/**
* The value for how the conference is locked (or undefined if not
* locked) as defined by room-lock constants.
*/
locked: PropTypes.string,
/**
* Callback to invoke when the local participant is submitting a
* password set request.
*/
onSubmit: PropTypes.func,
/**
* The current known password for the JitsiConference.
*/
password: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* {@code PasswordForm} component's local state.
*
* @type {Object}
* @property {string} enteredPassword - The value of the password being
* entered by the local participant.
*/
state = {
enteredPassword: ''
};
/**
* Initializes a new {@code PasswordForm} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code PasswordForm} instance with.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onEnteredPasswordChange
= this._onEnteredPasswordChange.bind(this);
this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
}
/**
* Implements React's {@link Component#componentWillReceiveProps()}. Invoked
* before this mounted component receives new props.
*
* @inheritdoc
* @param {Props} nextProps - New props component will receive.
*/
componentWillReceiveProps(nextProps) {
if (this.props.editEnabled && !nextProps.editEnabled) {
this.setState({ enteredPassword: '' });
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<div className = 'info-password'>
<div>{ t('info.password') }</div>
<div className = 'info-password-field'>
{ this._renderPasswordField() }
</div>
</div>
);
}
/**
* Returns a ReactElement for showing the current state of the password or
* for editing the current password.
*
* @private
* @returns {ReactElement}
*/
_renderPasswordField() {
if (this.props.editEnabled) {
return (
<form
className = 'info-password-form'
onSubmit = { this._onPasswordSubmit }>
<input
autoFocus = { true }
className = 'info-password-input'
onChange = { this._onEnteredPasswordChange }
spellCheck = { 'false' }
type = 'text'
value = { this.state.enteredPassword } />
</form>
);
} else if (this.props.locked === LOCKED_LOCALLY) {
return (
<div className = 'info-password-local'>
{ this.props.password }
</div>
);
} else if (this.props.locked) {
return (
<div className = 'info-password-remote'>
{ this.props.t('passwordSetRemotely') }
</div>
);
}
return (
<div className = 'info-password-none'>
{ this.props.t('info.noPassword') }
</div>
);
}
/**
* Updates the internal state of entered password.
*
* @param {Object} event - DOM Event for value change.
* @private
* @returns {void}
*/
_onEnteredPasswordChange(event) {
this.setState({ enteredPassword: event.target.value });
}
/**
* Invokes the passed in onSubmit callback to notify the parent that a
* password submission has been attempted.
*
* @param {Object} event - DOM Event for form submission.
* @private
* @returns {void}
*/
_onPasswordSubmit(event) {
event.preventDefault();
this.props.onSubmit(this.state.enteredPassword);
}
}
export default translate(PasswordForm);
@@ -0,0 +1 @@
export { default as InfoDialog } from './InfoDialog';
+7 -1
View File
@@ -26,10 +26,16 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
};
case UPDATE_DIAL_IN_NUMBERS_SUCCESS: {
const { numbers, numbersEnabled } = action.dialInNumbers;
const {
defaultCountry,
numbers,
numbersEnabled
} = action.dialInNumbers;
return {
...state,
conferenceID: action.conferenceID,
defaultCountry,
numbers,
numbersEnabled
};