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:
parent
0bbcd3181c
commit
59d046dca9
2
Makefile
2
Makefile
|
@ -33,6 +33,8 @@ deploy-appbundle:
|
|||
$(BUILD_DIR)/external_api.min.map \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.map \
|
||||
$(BUILD_DIR)/alwaysontop.min.js \
|
||||
$(BUILD_DIR)/alwaysontop.min.map \
|
||||
$(OUTPUT_DIR)/analytics-ga.js \
|
||||
|
|
|
@ -4,16 +4,20 @@
|
|||
|
||||
.info-dialog-action-link {
|
||||
display: inline-block;
|
||||
line-height: 1.5em;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog-action-link:before {
|
||||
color: $linkFontColor;
|
||||
content: '\2022';
|
||||
font-size: 1.5em;
|
||||
padding: 0 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.info-dialog-action-link:first-child:before {
|
||||
|
@ -22,6 +26,8 @@
|
|||
}
|
||||
|
||||
.info-dialog-action-links {
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -39,16 +45,27 @@
|
|||
|
||||
.info-dialog-column {
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
a,
|
||||
a:active,
|
||||
a:focus,
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog-conference-url {
|
||||
margin: 10px 0;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-dialog-dial-in {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-dialog-icon {
|
||||
color: #6453C0;
|
||||
font-size: 16px;
|
||||
|
@ -56,5 +73,55 @@
|
|||
|
||||
.info-dialog-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-password,
|
||||
.info-dialog-password,
|
||||
.info-password-form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info-password-field {
|
||||
margin-left: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-password-none,
|
||||
.info-password-remote {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.info-password-input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.conference-id {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-in-page {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 24px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.dial-in-numbers-list {
|
||||
font-size: 24px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dial-in-conference-id {
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -533,9 +533,22 @@
|
|||
"veryGood": "Very Good"
|
||||
},
|
||||
"info": {
|
||||
"copy": "Copy link",
|
||||
"invite": "Invite in __app__",
|
||||
"title": "Call access info",
|
||||
"cancelPassword": "Cancel password",
|
||||
"conferenceURL": "Link: __url__",
|
||||
"country": "Country",
|
||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
|
||||
"dialInNumber": "Dial-in: __phoneNumber__",
|
||||
"dialInConferenceID": "PIN: __conferenceID__#",
|
||||
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
|
||||
"genericError": "Whoops, something went wrong.",
|
||||
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __pin__#",
|
||||
"invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
|
||||
"inviteURL": "To join the video meeting, click this link: __url__",
|
||||
"moreNumbers": "More numbers",
|
||||
"noPassword": "None",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "Password:",
|
||||
"title": "Call info",
|
||||
"tooltip": "Get access info about the meeting"
|
||||
},
|
||||
"profileModal": {
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"string-replace-loader": "1.3.0",
|
||||
"style-loader": "0.19.0",
|
||||
"uglifyjs-webpack-plugin": "1.1.2",
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"webpack": "3.9.1",
|
||||
"webpack-dev-server": "2.9.5"
|
||||
},
|
||||
|
|
|
@ -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';
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="../" />
|
||||
<!--#include virtual="/title.html" -->
|
||||
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react"></div>
|
||||
<script><!--#include virtual="/config.js" --></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script src="libs/dial_in_info_bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -150,6 +150,18 @@ module.exports = [
|
|||
'alwaysontop':
|
||||
'./react/features/always-on-top/index.js',
|
||||
|
||||
'dial_in_info_bundle': [
|
||||
|
||||
// babel-polyfill and fetch polyfill are required for IE11.
|
||||
'babel-polyfill',
|
||||
'whatwg-fetch',
|
||||
|
||||
// atlaskit does not support React 16 prop-types
|
||||
'./react/features/base/react/prop-types-polyfill.js',
|
||||
|
||||
'./react/features/invite/components/dial-in-info-page'
|
||||
],
|
||||
|
||||
'do_external_connect':
|
||||
'./connection_optimization/do_external_connect.js'
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue