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 11:46:47 -08:00 committed by yanas
parent 0bbcd3181c
commit 59d046dca9
25 changed files with 1274 additions and 205 deletions

View File

@ -33,6 +33,8 @@ deploy-appbundle:
$(BUILD_DIR)/external_api.min.map \ $(BUILD_DIR)/external_api.min.map \
$(BUILD_DIR)/device_selection_popup_bundle.min.js \ $(BUILD_DIR)/device_selection_popup_bundle.min.js \
$(BUILD_DIR)/device_selection_popup_bundle.min.map \ $(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.js \
$(BUILD_DIR)/alwaysontop.min.map \ $(BUILD_DIR)/alwaysontop.min.map \
$(OUTPUT_DIR)/analytics-ga.js \ $(OUTPUT_DIR)/analytics-ga.js \

View File

@ -4,16 +4,20 @@
.info-dialog-action-link { .info-dialog-action-link {
display: inline-block; display: inline-block;
line-height: 1.5em;
a { a {
cursor: pointer; cursor: pointer;
vertical-align: middle;
} }
} }
.info-dialog-action-link:before { .info-dialog-action-link:before {
color: $linkFontColor; color: $linkFontColor;
content: '\2022'; content: '\2022';
font-size: 1.5em;
padding: 0 10px; padding: 0 10px;
vertical-align: middle;
} }
.info-dialog-action-link:first-child:before { .info-dialog-action-link:first-child:before {
@ -22,6 +26,8 @@
} }
.info-dialog-action-links { .info-dialog-action-links {
font-weight: bold;
margin-top: 10px;
white-space: nowrap; white-space: nowrap;
} }
@ -39,16 +45,27 @@
.info-dialog-column { .info-dialog-column {
margin-right: 10px; margin-right: 10px;
overflow: hidden;
a,
a:active,
a:focus,
a:hover {
text-decoration: none;
}
} }
.info-dialog-conference-url { .info-dialog-conference-url {
margin: 10px 0;
max-width: 250px; max-width: 250px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.info-dialog-dial-in {
white-space: nowrap;
}
.info-dialog-icon { .info-dialog-icon {
color: #6453C0; color: #6453C0;
font-size: 16px; font-size: 16px;
@ -56,5 +73,55 @@
.info-dialog-title { .info-dialog-title {
font-weight: bold; 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%;
} }
} }

View File

@ -533,9 +533,22 @@
"veryGood": "Very Good" "veryGood": "Very Good"
}, },
"info": { "info": {
"copy": "Copy link", "cancelPassword": "Cancel password",
"invite": "Invite in __app__", "conferenceURL": "Link: __url__",
"title": "Call access info", "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" "tooltip": "Get access info about the meeting"
}, },
"profileModal": { "profileModal": {

View File

@ -100,6 +100,7 @@
"string-replace-loader": "1.3.0", "string-replace-loader": "1.3.0",
"style-loader": "0.19.0", "style-loader": "0.19.0",
"uglifyjs-webpack-plugin": "1.1.2", "uglifyjs-webpack-plugin": "1.1.2",
"whatwg-fetch": "2.0.3",
"webpack": "3.9.1", "webpack": "3.9.1",
"webpack-dev-server": "2.9.5" "webpack-dev-server": "2.9.5"
}, },

View File

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

View File

@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox'; import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox';
import { setInfoDialogVisibility } from '../actions'; import { setInfoDialogVisibility } from '../actions';
import InfoDialog from './InfoDialog'; import { InfoDialog } from './info-dialog';
const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig; const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig;

View File

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

View File

@ -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'));
});

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as DialInInfoApp } from './DialInInfoApp';

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as InfoDialog } from './InfoDialog';

View File

@ -26,10 +26,16 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
}; };
case UPDATE_DIAL_IN_NUMBERS_SUCCESS: { case UPDATE_DIAL_IN_NUMBERS_SUCCESS: {
const { numbers, numbersEnabled } = action.dialInNumbers; const {
defaultCountry,
numbers,
numbersEnabled
} = action.dialInNumbers;
return { return {
...state,
conferenceID: action.conferenceID, conferenceID: action.conferenceID,
defaultCountry,
numbers, numbers,
numbersEnabled numbersEnabled
}; };

17
static/dialInInfo.html Normal file
View File

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

View File

@ -150,6 +150,18 @@ module.exports = [
'alwaysontop': 'alwaysontop':
'./react/features/always-on-top/index.js', './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': 'do_external_connect':
'./connection_optimization/do_external_connect.js' './connection_optimization/do_external_connect.js'
} }