diff --git a/Makefile b/Makefile
index 84023eca60..7c5cbdfb4a 100644
--- a/Makefile
+++ b/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 \
diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss
index 0c81b4ee6d..c8761c7435 100644
--- a/css/modals/invite/_info.scss
+++ b/css/modals/invite/_info.scss
@@ -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%;
}
}
diff --git a/lang/main.json b/lang/main.json
index 2e6a815e17..9d90574a80 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -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": {
diff --git a/package.json b/package.json
index 06aa21fe6f..88fc792f38 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/react/features/invite/components/InfoDialog.web.js b/react/features/invite/components/InfoDialog.web.js
deleted file mode 100644
index 9be57ee0ed..0000000000
--- a/react/features/invite/components/InfoDialog.web.js
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- { this.props.t('info.title') }
-
-
- { this.props._inviteURL }
-
-
-
-
- { this.props._showAddPeople
- ?
- : null }
-
-
-
- );
- }
-
- /**
- * 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));
diff --git a/react/features/invite/components/InfoDialogButton.web.js b/react/features/invite/components/InfoDialogButton.web.js
index 7cf0ffa0fe..47fdce6821 100644
--- a/react/features/invite/components/InfoDialogButton.web.js
+++ b/react/features/invite/components/InfoDialogButton.web.js
@@ -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;
diff --git a/react/features/invite/components/InfoDialog.native.js b/react/features/invite/components/dial-in-info-page/ConferenceID.native.js
similarity index 100%
rename from react/features/invite/components/InfoDialog.native.js
rename to react/features/invite/components/dial-in-info-page/ConferenceID.native.js
diff --git a/react/features/invite/components/dial-in-info-page/ConferenceID.web.js b/react/features/invite/components/dial-in-info-page/ConferenceID.web.js
new file mode 100644
index 0000000000..b921701443
--- /dev/null
+++ b/react/features/invite/components/dial-in-info-page/ConferenceID.web.js
@@ -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 (
+
+ { t('info.dialANumber', { conferenceID }) }
+
+ );
+ }
+}
+
+export default translate(ConferenceID);
diff --git a/react/features/invite/components/dial-in-info-page/DialInInfoApp.native.js b/react/features/invite/components/dial-in-info-page/DialInInfoApp.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/dial-in-info-page/DialInInfoApp.web.js b/react/features/invite/components/dial-in-info-page/DialInInfoApp.web.js
new file mode 100644
index 0000000000..8e10ac3250
--- /dev/null
+++ b/react/features/invite/components/dial-in-info-page/DialInInfoApp.web.js
@@ -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(
+
+
+ ,
+ document.getElementById('react')
+ );
+});
+
+window.addEventListener('beforeunload', () => {
+ ReactDOM.unmountComponentAtNode(document.getElementById('react'));
+});
diff --git a/react/features/invite/components/dial-in-info-page/DialInInfoPage.native.js b/react/features/invite/components/dial-in-info-page/DialInInfoPage.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/dial-in-info-page/DialInInfoPage.web.js b/react/features/invite/components/dial-in-info-page/DialInInfoPage.web.js
new file mode 100644
index 0000000000..3d847c1991
--- /dev/null
+++ b/react/features/invite/components/dial-in-info-page/DialInInfoPage.web.js
@@ -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
+ ?
+ : null,
+
+ ];
+ }
+
+ return (
+
+ { contents }
+
+ );
+ }
+
+ /**
+ * 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);
diff --git a/react/features/invite/components/dial-in-info-page/NumbersList.native.js b/react/features/invite/components/dial-in-info-page/NumbersList.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/dial-in-info-page/NumbersList.web.js b/react/features/invite/components/dial-in-info-page/NumbersList.web.js
new file mode 100644
index 0000000000..554dd6c463
--- /dev/null
+++ b/react/features/invite/components/dial-in-info-page/NumbersList.web.js
@@ -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 (
+
+
+
+ { showWithoutCountries
+ ? null
+ : { t('info.country') } }
+ { t('info.numbers') }
+
+
+
+ { showWithoutCountries
+ ? numbers.map(this._renderNumberRow)
+ : this._renderWithCountries() }
+
+
);
+ }
+
+ /**
+ * 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(
+
+ { country }
+ { formattedNumbers }
+
+ );
+ }
+
+ return rows;
+ }
+
+ /**
+ * Renders a table row for a phone number.
+ *
+ * @param {string} number - The phone number to display.
+ * @private
+ * @returns {ReactElement[]}
+ */
+ _renderNumberRow(number) {
+ return (
+
+
+ { number }
+
+
+ );
+ }
+
+ /**
+ * Renders a div container for a phone number.
+ *
+ * @param {string} number - The phone number to display.
+ * @private
+ * @returns {ReactElement[]}
+ */
+ _renderNumberDiv(number) {
+ return (
+
+ { number }
+
+ );
+ }
+}
+
+export default translate(NumbersList);
diff --git a/react/features/invite/components/dial-in-info-page/index.js b/react/features/invite/components/dial-in-info-page/index.js
new file mode 100644
index 0000000000..dff1aba32f
--- /dev/null
+++ b/react/features/invite/components/dial-in-info-page/index.js
@@ -0,0 +1 @@
+export { default as DialInInfoApp } from './DialInInfoApp';
diff --git a/react/features/invite/components/info-dialog/DialInNumber.native.js b/react/features/invite/components/info-dialog/DialInNumber.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/info-dialog/DialInNumber.web.js b/react/features/invite/components/info-dialog/DialInNumber.web.js
new file mode 100644
index 0000000000..38e8af28bf
--- /dev/null
+++ b/react/features/invite/components/info-dialog/DialInNumber.web.js
@@ -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 (
+
+
+ { this.props.t('info.dialInNumber', { phoneNumber }) }
+
+
+ { this.props.t(
+ 'info.dialInConferenceID', { conferenceID }) }
+
+
+ );
+ }
+}
+
+export default translate(DialInNumber);
diff --git a/react/features/invite/components/info-dialog/InfoDialog.native.js b/react/features/invite/components/info-dialog/InfoDialog.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/info-dialog/InfoDialog.web.js b/react/features/invite/components/info-dialog/InfoDialog.web.js
new file mode 100644
index 0000000000..b7dfbcb769
--- /dev/null
+++ b/react/features/invite/components/info-dialog/InfoDialog.web.js
@@ -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 (
+
+
+
+
+
+
+
+
+ { t('info.title') }
+
+
+ { t('info.conferenceURL',
+ { url: this._getURLToDisplay() }) }
+
+
+
+ { this._renderDialInDisplay() }
+
+
+
+
+ { this._renderPasswordAction() }
+
+
+
+ );
+ }
+
+ /**
+ * Sets the internal state of which dial-in number to display.
+ *
+ * @param {Array|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 (
+
+ );
+ }
+
+ /**
+ * 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
+ ?
+ : 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));
diff --git a/react/features/invite/components/info-dialog/PasswordForm.native.js b/react/features/invite/components/info-dialog/PasswordForm.native.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/react/features/invite/components/info-dialog/PasswordForm.web.js b/react/features/invite/components/info-dialog/PasswordForm.web.js
new file mode 100644
index 0000000000..2121ca5717
--- /dev/null
+++ b/react/features/invite/components/info-dialog/PasswordForm.web.js
@@ -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 (
+
+
{ t('info.password') }
+
+ { this._renderPasswordField() }
+
+
+ );
+ }
+
+ /**
+ * 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 (
+
+ );
+ } else if (this.props.locked === LOCKED_LOCALLY) {
+ return (
+
+ { this.props.password }
+
+ );
+ } else if (this.props.locked) {
+ return (
+
+ { this.props.t('passwordSetRemotely') }
+
+ );
+ }
+
+ return (
+
+ { this.props.t('info.noPassword') }
+
+ );
+ }
+
+ /**
+ * 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);
diff --git a/react/features/invite/components/info-dialog/index.js b/react/features/invite/components/info-dialog/index.js
new file mode 100644
index 0000000000..1bba5d3900
--- /dev/null
+++ b/react/features/invite/components/info-dialog/index.js
@@ -0,0 +1 @@
+export { default as InfoDialog } from './InfoDialog';
diff --git a/react/features/invite/reducer.js b/react/features/invite/reducer.js
index f4f4a61730..4ee07f07ca 100644
--- a/react/features/invite/reducer.js
+++ b/react/features/invite/reducer.js
@@ -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
};
diff --git a/static/dialInInfo.html b/static/dialInInfo.html
new file mode 100644
index 0000000000..a99e8168d9
--- /dev/null
+++ b/static/dialInInfo.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webpack.config.js b/webpack.config.js
index f8124271c1..301c590e36 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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'
}