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)/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 \
|
||||||
|
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 { 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;
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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':
|
'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'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue