diff --git a/css/_country-picker.scss b/css/_country-picker.scss
new file mode 100644
index 000000000..47e9ec214
--- /dev/null
+++ b/css/_country-picker.scss
@@ -0,0 +1,75 @@
+.cpick {
+ border: 1px solid #A4B8D1;
+ color: #fff;
+ display: flex;
+ font-size: 15px;
+ height: 38px;
+ line-height: 24px;
+
+ &-selector {
+ align-items: center;
+ background-color: #283447;
+ border-right: 1px solid #A4B8D1;
+ cursor: pointer;
+ display: flex;
+ padding: 8px 10px;
+ position: relative;
+ width: 88px;
+ }
+
+ &-icon {
+ margin-right: 8px;
+ position: absolute;
+ right: 0;
+ top: 12px;
+
+ & > svg {
+ fill: #fff;
+ }
+ }
+
+ &-input {
+ padding: 8px;
+ background: #1C2025;
+ border: 0;
+ margin: 0;
+ color: #fff;
+ caret-color: #0376DA;
+ flex-grow: 1;
+ }
+
+ &-dropdown {
+ height: 190px;
+ overflow-y: auto;
+ width: 343px;
+ }
+
+ &-dropdown-entry {
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ height: 40px;
+ padding: 0 10px;
+
+ &:hover {
+ background-color: #66768b;
+ }
+
+ &-text {
+ color: #fff;
+ flex-grow: 1;
+ font-size: 15px;
+ line-height: 24px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ }
+ }
+}
+
+// Override @Atlaskit/inline-dialog styles
+.cpick-container > div > div:nth-child(2) > div > div {
+ outline: none;
+ padding: 8px 0 0 0;
+}
diff --git a/css/_prejoin-dialog.scss b/css/_prejoin-dialog.scss
new file mode 100644
index 000000000..5c084bcfc
--- /dev/null
+++ b/css/_prejoin-dialog.scss
@@ -0,0 +1,182 @@
+.prejoin-dialog {
+ background: #1C2025;
+ box-shadow: 0px 2px 20px rgba(0, 0, 0, 0.5);
+ border-radius: 5px;
+ color: #fff;
+ height: 400px;
+ width: 375px;
+
+ &--small {
+ height: 300;
+ width: 400;
+ }
+
+ &-label {
+ font-size: 15px;
+ line-height: 24px;
+
+ &-num {
+ background: #2b3b4b;
+ border: 1px solid #A4B8D1;
+ border-radius: 50%;
+ color: #fff;
+ display: inline-block;
+ height: 24px;
+ margin-right: 8px;
+ width: 24px;
+ }
+ }
+
+ &-container {
+ align-items: center;
+ background: rgba(0,0,0,0.6);
+ display: flex;
+ height: 100vh;
+ justify-content: center;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100vw;
+ z-index: 3;
+ }
+
+ &-flag {
+ display: inline-block;
+ margin-right: 8px;
+ transform: scale(1.2);
+ }
+
+ &-title {
+ display: inline-block;
+ font-size: 24px;
+ line-height: 32px;
+ }
+
+ &-icon {
+ cursor: pointer;
+
+ > svg {
+ fill: #A4B8D1;
+ }
+ }
+
+ &-btn {
+ width: 309px;
+ }
+
+ &-dialin-container {
+ text-align: center;
+ }
+
+ &-delimiter {
+ background: #5f6266;
+ border: 0;
+ height: 1px;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+
+ &-container {
+ margin: 16px 0 24px 0;
+ position: relative;
+ }
+
+ &-txt-container {
+ position: absolute;
+ text-align: center;
+ top: -8px;
+ width: 100%;
+ }
+
+ &-txt {
+ background: #1C2025;
+ color: #5f6266;
+ font-size: 11px;
+ text-transform: uppercase;
+ padding: 0 8px;
+ }
+ }
+}
+
+.prejoin-dialog-callout {
+ padding: 16px;
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ }
+
+ &-picker {
+ margin: 8px 0 16px 0;
+ }
+}
+
+.prejoin-dialog-dialin {
+ text-align: center;
+
+ &-header {
+ align-items: center;
+ margin: 16px 0 32px 16px;
+ display: flex;
+ }
+
+ &-icon {
+ margin-right: 16px;
+ }
+
+ &-num {
+ background: #3e474f;
+ border-radius: 4px;
+ display: inline-block;
+ font-size: 15px;
+ line-height: 24px;
+ margin: 4px;
+ padding: 8px;
+
+ &-container {
+ min-height: 48px;
+ margin: 8px 0;
+ }
+ }
+
+ &-link {
+ color: #6FB1EA;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 13px;
+ line-height: 20px;
+ margin-bottom: 24px;
+ }
+
+ &-spaced-label {
+ margin-bottom: 16px;
+ margin-top: 28px;
+ }
+
+ &-btns {
+ &> div {
+ margin-bottom: 16px;
+ }
+ }
+}
+
+.prejoin-dialog-calling {
+ padding: 16px;
+ text-align: center;
+
+ &-header {
+ text-align: right;
+ }
+
+ &-label {
+ font-size: 15px;
+ margin: 8px 0 16px 0;
+ }
+
+ &-number {
+ font-size: 19px;
+ line-height: 28px;
+ margin: 16px 0;
+ }
+}
diff --git a/css/main.scss b/css/main.scss
index 70b047019..7dc6aa996 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -91,5 +91,7 @@ $flagsImagePath: "../images/";
@import 'audio-preview';
@import 'video-preview';
@import 'prejoin';
+@import 'prejoin-dialog';
+@import 'country-picker';
/* Modules END */
diff --git a/lang/main.json b/lang/main.json
index f26ad90d1..95bee5960 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -489,6 +489,12 @@
"dialInPin": "Dial into the meeting and enter PIN code:",
"dialing": "Dialing",
"doNotShow": "Don't show this again",
+ "errorDialOut": "Could not dial out",
+ "errorDialOutDisconnected": "Could not dial out. Disconnected",
+ "errorDialOutFailed": "Could not dial out. Call failed",
+ "errorDialOutStatus": "Error getting dial out status",
+ "errorStatusCode": "Error dialing out, status code: {{status}}",
+ "errorValidation": "Number validation failed",
"iWantToDialIn": "I want to dial in",
"joinAudioByPhone": "Join with phone audio",
"joinMeeting": "Join meeting",
diff --git a/react/features/base/config/functions.web.js b/react/features/base/config/functions.web.js
index 59bba0486..f92e03e01 100644
--- a/react/features/base/config/functions.web.js
+++ b/react/features/base/config/functions.web.js
@@ -10,3 +10,23 @@ export * from './functions.any';
*/
export function _cleanupConfig(config: Object) { // eslint-disable-line no-unused-vars
}
+
+/**
+ * Returns the dial out url.
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getDialOutStatusUrl(state: Object): string {
+ return state['features/base/config'].guestDialOutStatusUrl;
+}
+
+/**
+ * Returns the dial out status url.
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getDialOutUrl(state: Object): string {
+ return state['features/base/config'].guestDialOutUrl;
+}
diff --git a/react/features/base/util/openURLInBrowser.web.js b/react/features/base/util/openURLInBrowser.web.js
index cee50e12e..c1572ad13 100644
--- a/react/features/base/util/openURLInBrowser.web.js
+++ b/react/features/base/util/openURLInBrowser.web.js
@@ -4,8 +4,11 @@
* Opens URL in the browser.
*
* @param {string} url - The URL to be opened.
+ * @param {boolean} openInNewTab - If the link should be opened in a new tab.
* @returns {void}
*/
-export function openURLInBrowser(url: string) {
- window.open(url, '', 'noopener');
+export function openURLInBrowser(url: string, openInNewTab?: boolean) {
+ const target = openInNewTab ? '_blank' : '';
+
+ window.open(url, target, 'noopener');
}
diff --git a/react/features/invite/actions.any.js b/react/features/invite/actions.any.js
index f1fa93f75..fe2fc77e9 100644
--- a/react/features/invite/actions.any.js
+++ b/react/features/invite/actions.any.js
@@ -165,9 +165,10 @@ export function updateDialInNumbers() {
const state = getState();
const { dialInConfCodeUrl, dialInNumbersUrl, hosts }
= state['features/base/config'];
+ const { numbersFetched } = state['features/invite'];
const mucURL = hosts && hosts.muc;
- if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
+ if (numbersFetched || !dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
// URLs for fetching dial in numbers not defined
return;
}
diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js
index cfac4730d..208e857ec 100644
--- a/react/features/invite/functions.js
+++ b/react/features/invite/functions.js
@@ -2,6 +2,7 @@
import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants';
+import { toState } from '../base/redux';
import { doGetJSON, parseURIString } from '../base/util';
import logger from './logger';
@@ -616,3 +617,69 @@ export function _decodeRoomURI(url: string) {
return roomUrl;
}
+
+/**
+ * Returns the stored conference id.
+ *
+ * @param {Object | Function} stateful - The Object or Function that can be
+ * resolved to a Redux state object with the toState function.
+ * @returns {string}
+ */
+export function getConferenceId(stateful: Object | Function) {
+ return toState(stateful)['features/invite'].conferenceID;
+}
+
+/**
+ * Returns the default dial in number from the store.
+ *
+ * @param {Object | Function} stateful - The Object or Function that can be
+ * resolved to a Redux state object with the toState function.
+ * @returns {string | null}
+ */
+export function getDefaultDialInNumber(stateful: Object | Function) {
+ return _getDefaultPhoneNumber(toState(stateful)['features/invite'].numbers);
+}
+
+/**
+ * Executes the dial out request.
+ *
+ * @param {string} url - The url for dialing out.
+ * @param {Object} body - The body of the request.
+ * @param {string} reqId - The unique request id.
+ * @returns {Object}
+ */
+export async function executeDialOutRequest(url: string, body: Object, reqId: string) {
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'request-id': reqId
+ },
+ body: JSON.stringify(body)
+ });
+
+ const json = await res.json();
+
+ return res.ok ? json : Promise.reject(json);
+}
+
+/**
+ * Executes the dial out status request.
+ *
+ * @param {string} url - The url for dialing out.
+ * @param {string} reqId - The unique request id used on the dial out request.
+ * @returns {Object}
+ */
+export async function executeDialOutStatusRequest(url: string, reqId: string) {
+ const res = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'request-id': reqId
+ }
+ });
+
+ const json = await res.json();
+
+ return res.ok ? json : Promise.reject(json);
+}
diff --git a/react/features/invite/reducer.js b/react/features/invite/reducer.js
index d66ff902e..b43e3e2b0 100644
--- a/react/features/invite/reducer.js
+++ b/react/features/invite/reducer.js
@@ -20,6 +20,7 @@ const DEFAULT_STATE = {
*/
calleeInfoVisible: false,
numbersEnabled: true,
+ numbersFetched: false,
pendingInviteRequests: []
};
@@ -59,7 +60,8 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
...state,
conferenceID: action.conferenceID,
numbers: action.dialInNumbers,
- numbersEnabled: true
+ numbersEnabled: true,
+ numbersFetched: true
};
}
@@ -72,7 +74,8 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
...state,
conferenceID: action.conferenceID,
numbers: action.dialInNumbers,
- numbersEnabled
+ numbersEnabled,
+ numbersFetched: true
};
}
}
diff --git a/react/features/prejoin/actionTypes.js b/react/features/prejoin/actionTypes.js
index 990b6717f..b98cb5501 100644
--- a/react/features/prejoin/actionTypes.js
+++ b/react/features/prejoin/actionTypes.js
@@ -29,6 +29,21 @@ export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
*/
export const SET_SKIP_PREJOIN = 'SET_SKIP_PREJOIN';
+/**
+ * Action type to set the country to dial out to.
+ */
+export const SET_DIALOUT_COUNTRY = 'SET_DIALOUT_COUNTRY';
+
+/**
+ * Action type to set the dial out number.
+ */
+export const SET_DIALOUT_NUMBER = 'SET_DIALOUT_NUMBER';
+
+/**
+ * Action type to set the dial out status while dialing.
+ */
+export const SET_DIALOUT_STATUS = 'SET_DIALOUT_STATUS';
+
/**
* Action type to set the visiblity of the 'JoinByPhone' dialog.
*/
diff --git a/react/features/prejoin/actions.js b/react/features/prejoin/actions.js
index dc95c2309..6b1212f19 100644
--- a/react/features/prejoin/actions.js
+++ b/react/features/prejoin/actions.js
@@ -1,11 +1,17 @@
// @flow
+import uuid from 'uuid';
+
+import { getRoomName } from '../base/conference';
import {
ADD_PREJOIN_AUDIO_TRACK,
ADD_PREJOIN_CONTENT_SHARING_TRACK,
ADD_PREJOIN_VIDEO_TRACK,
PREJOIN_START_CONFERENCE,
SET_DEVICE_STATUS,
+ SET_DIALOUT_COUNTRY,
+ SET_DIALOUT_NUMBER,
+ SET_DIALOUT_STATUS,
SET_SKIP_PREJOIN,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PREJOIN_AUDIO_DISABLED,
@@ -15,9 +21,44 @@ import {
SET_PREJOIN_VIDEO_DISABLED,
SET_PREJOIN_VIDEO_MUTED
} from './actionTypes';
+import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { createLocalTrack } from '../base/lib-jitsi-meet';
-import { getAudioTrack, getVideoTrack } from './functions';
+import { openURLInBrowser } from '../base/util';
+import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import logger from './logger';
+import { showErrorNotification } from '../notifications';
+
+import {
+ getFullDialOutNumber,
+ getAudioTrack,
+ getDialOutConferenceUrl,
+ getDialOutCountry,
+ getVideoTrack,
+ isJoinByPhoneDialogVisible
+} from './functions';
+
+const dialOutStatusToKeyMap = {
+ INITIATED: 'presenceStatus.calling',
+ RINGING: 'presenceStatus.ringing'
+};
+
+const DIAL_OUT_STATUS = {
+ INITIATED: 'INITIATED',
+ RINGING: 'RINGING',
+ CONNECTED: 'CONNECTED',
+ DISCONNECTED: 'DISCONNECTED',
+ FAILED: 'FAILED'
+};
+
+/**
+ * The time interval used between requests while polling for dial out status.
+ */
+const STATUS_REQ_FREQUENCY = 2000;
+
+/**
+ * The maximum number of retries while polling for dial out status.
+ */
+const STATUS_REQ_CAP = 45;
/**
* Action used to add an audio track to the store.
@@ -58,6 +99,129 @@ export function addPrejoinContentSharingTrack(value: Object) {
};
}
+/**
+ * Polls for status change after dial out.
+ * Changes dialog message based on response, closes the dialog if there is an error,
+ * joins the meeting when CONNECTED.
+ *
+ * @param {string} reqId - The request id used to correlate the dial out request with this one.
+ * @param {Function} onSuccess - Success handler.
+ * @param {Function} onFail - Fail handler.
+ * @param {number} count - The number of retried calls. When it hits STATUS_REQ_CAP it should no longer make requests.
+ * @returns {Function}
+ */
+function pollForStatus(
+ reqId: string,
+ onSuccess: Function,
+ onFail: Function,
+ count = 0) {
+ return async function(dispatch: Function, getState: Function) {
+ const state = getState();
+
+ try {
+ if (!isJoinByPhoneDialogVisible(state)) {
+ return;
+ }
+
+ const res = await executeDialOutStatusRequest(getDialOutStatusUrl(state), reqId);
+
+ switch (res) {
+ case DIAL_OUT_STATUS.INITIATED:
+ case DIAL_OUT_STATUS.RINGING: {
+ dispatch(setDialOutStatus(dialOutStatusToKeyMap[res]));
+
+ if (count < STATUS_REQ_CAP) {
+ return setTimeout(() => {
+ dispatch(pollForStatus(reqId, onSuccess, onFail, count + 1));
+ }, STATUS_REQ_FREQUENCY);
+ }
+
+ return onFail();
+ }
+
+ case DIAL_OUT_STATUS.CONNECTED: {
+ return onSuccess();
+ }
+
+ case DIAL_OUT_STATUS.DISCONNECTED: {
+ dispatch(showErrorNotification({
+ titleKey: 'prejoin.errorDialOutDisconnected'
+ }));
+
+ return onFail();
+ }
+
+ case DIAL_OUT_STATUS.FAILED: {
+ dispatch(showErrorNotification({
+ titleKey: 'prejoin.errorDialOutFailed'
+ }));
+
+ return onFail();
+ }
+ }
+ } catch (err) {
+ dispatch(showErrorNotification({
+ titleKey: 'prejoin.errorDialOutStatus'
+ }));
+ logger.error('Error getting dial out status', err);
+ onFail();
+ }
+ };
+}
+
+
+/**
+ * Action used for joining the meeting with phone audio.
+ * A dial out connection is tried and a polling mechanism is used for getting the status.
+ * If the connection succeeds the `onSuccess` callback is executed.
+ * If the phone connection fails or the number is invalid the `onFail` callback is executed.
+ *
+ * @param {Function} onSuccess - Success handler.
+ * @param {Function} onFail - Fail handler.
+ * @returns {Function}
+ */
+export function dialOut(onSuccess: Function, onFail: Function) {
+ return async function(dispatch: Function, getState: Function) {
+ const state = getState();
+ const reqId = uuid.v4();
+ const url = getDialOutUrl(state);
+ const conferenceUrl = getDialOutConferenceUrl(state);
+ const phoneNumber = getFullDialOutNumber(state);
+ const countryCode = getDialOutCountry(state).code.toUpperCase();
+
+ const body = {
+ conferenceUrl,
+ countryCode,
+ name: phoneNumber,
+ phoneNumber
+ };
+
+ try {
+ await executeDialOutRequest(url, body, reqId);
+
+ dispatch(pollForStatus(reqId, onSuccess, onFail));
+ } catch (err) {
+ const notification = {
+ titleKey: 'prejoin.errorDialOut',
+ titleArguments: undefined
+ };
+
+ if (err.status) {
+ if (err.messageKey === 'validation.failed') {
+ notification.titleKey = 'prejoin.errorValidation';
+ } else {
+ notification.titleKey = 'prejoin.errorStatusCode';
+ notification.titleArguments = { status: err.status };
+ }
+ }
+
+ dispatch(showErrorNotification(notification));
+ logger.error('Error dialing out', err);
+ onFail();
+ }
+ };
+}
+
/**
* Adds all the newly created tracks to store on init.
*
@@ -121,6 +285,22 @@ export function joinConferenceWithoutAudio() {
};
}
+/**
+ * Opens an external page with all the dial in numbers.
+ *
+ * @returns {Function}
+ */
+export function openDialInPage() {
+ return function(dispatch: Function, getState: Function) {
+ const state = getState();
+ const locationURL = state['features/base/connection'].locationURL;
+ const roomName = getRoomName(state);
+ const dialInPage = getDialInfoPageURL(roomName, locationURL);
+
+ openURLInBrowser(dialInPage, true);
+ };
+}
+
/**
* Replaces the existing audio track with a new one.
*
@@ -273,6 +453,45 @@ export function setDeviceStatusWarning(deviceStatusText: string) {
};
}
+/**
+ * Action used to set the dial out status.
+ *
+ * @param {string} value - The status.
+ * @returns {Object}
+ */
+function setDialOutStatus(value: string) {
+ return {
+ type: SET_DIALOUT_STATUS,
+ value
+ };
+}
+
+/**
+ * Action used to set the dial out country.
+ *
+ * @param {{ name: string, dialCode: string, code: string }} value - The country.
+ * @returns {Object}
+ */
+export function setDialOutCountry(value: Object) {
+ return {
+ type: SET_DIALOUT_COUNTRY,
+ value
+ };
+}
+
+/**
+ * Action used to set the dial out number.
+ *
+ * @param {string} value - The dial out number.
+ * @returns {Object}
+ */
+export function setDialOutNumber(value: string) {
+ return {
+ type: SET_DIALOUT_NUMBER,
+ value
+ };
+}
+
/**
* Sets the visibility of the prejoin page for future uses.
*
diff --git a/react/features/prejoin/components/Label.js b/react/features/prejoin/components/Label.js
new file mode 100644
index 000000000..d772a44a3
--- /dev/null
+++ b/react/features/prejoin/components/Label.js
@@ -0,0 +1,48 @@
+// @flow
+
+import React from 'react';
+
+type Props = {
+
+ /**
+ * The text for the Label.
+ */
+ children: React$Node,
+
+ /**
+ * The CSS class of the label.
+ */
+ className?: string,
+
+ /**
+ * The (round) number prefix for the Label.
+ */
+ number?: string | number,
+
+ /**
+ * The click handler.
+ */
+ onClick?: Function,
+};
+
+/**
+ * Label for the dialogs.
+ *
+ * @returns {ReactElement}
+ */
+function Label({ children, className, number, onClick }: Props) {
+ const containerClass = className
+ ? `prejoin-dialog-label ${className}`
+ : 'prejoin-dialog-label';
+
+ return (
+
+ {number &&
{number}
}
+
{children}
+
+ );
+}
+
+export default Label;
diff --git a/react/features/prejoin/components/Prejoin.js b/react/features/prejoin/components/Prejoin.js
index 713e206e9..900c69cd4 100644
--- a/react/features/prejoin/components/Prejoin.js
+++ b/react/features/prejoin/components/Prejoin.js
@@ -25,6 +25,8 @@ import DeviceStatus from './preview/DeviceStatus';
import ParticipantName from './preview/ParticipantName';
import Preview from './preview/Preview';
import { VideoSettingsButton, AudioSettingsButton } from '../../toolbox';
+import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
+
type Props = {
@@ -112,6 +114,8 @@ class Prejoin extends Component {
this.state = {
showJoinByPhoneButtons: false
};
+
+ this._closeDialog = this._closeDialog.bind(this);
this._showDialog = this._showDialog.bind(this);
this._onCheckboxChange = this._onCheckboxChange.bind(this);
this._onDropdownClose = this._onDropdownClose.bind(this);
@@ -174,6 +178,17 @@ class Prejoin extends Component {
});
}
+ _closeDialog: () => void;
+
+ /**
+ * Closes the join by phone dialog.
+ *
+ * @returns {undefined}
+ */
+ _closeDialog() {
+ this.props.setJoinByPhoneDialogVisiblity(false);
+ }
+
_showDialog: () => void;
/**
@@ -183,6 +198,7 @@ class Prejoin extends Component {
*/
_showDialog() {
this.props.setJoinByPhoneDialogVisiblity(true);
+ this._onDropdownClose();
}
/**
@@ -199,9 +215,11 @@ class Prejoin extends Component {
joinConferenceWithoutAudio,
name,
hasJoinByPhoneButtons,
+ showDialog,
t
} = this.props;
- const { _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
+
+ const { _closeDialog, _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
const { showJoinByPhoneButtons } = this.state;
return (
@@ -271,6 +289,11 @@ class Prejoin extends Component {
{ deviceStatusVisible && }
+ { showDialog && (
+
+ )}
);
}
diff --git a/react/features/prejoin/components/country-picker/CountryDropdown.js b/react/features/prejoin/components/country-picker/CountryDropdown.js
new file mode 100644
index 000000000..7fe1f8b26
--- /dev/null
+++ b/react/features/prejoin/components/country-picker/CountryDropdown.js
@@ -0,0 +1,33 @@
+// @flow
+
+import React from 'react';
+import { countries } from '../../utils';
+import CountryRow from './CountryRow';
+
+type Props = {
+
+ /**
+ * Click handler for a single entry.
+ */
+ onEntryClick: Function,
+};
+
+/**
+ * This component displays the dropdown for the country picker.
+ *
+ * @returns {ReactElement}
+ */
+function CountryDropdown({ onEntryClick }: Props) {
+ return (
+
+ {countries.map(country => (
+
+ ))}
+
+ );
+}
+
+export default CountryDropdown;
diff --git a/react/features/prejoin/components/country-picker/CountryPicker.js b/react/features/prejoin/components/country-picker/CountryPicker.js
new file mode 100644
index 000000000..1c96826d9
--- /dev/null
+++ b/react/features/prejoin/components/country-picker/CountryPicker.js
@@ -0,0 +1,250 @@
+// @flow
+
+import React, { PureComponent } from 'react';
+import InlineDialog from '@atlaskit/inline-dialog';
+
+import { connect } from '../../../base/redux';
+import { setDialOutCountry, setDialOutNumber } from '../../actions';
+import { getDialOutCountry, getDialOutNumber } from '../../functions';
+import { getCountryFromDialCodeText } from '../../utils';
+
+import CountrySelector from './CountrySelector';
+import CountryDropDown from './CountryDropdown';
+
+const PREFIX_REG = /^(00)|\+/;
+
+type Props = {
+
+ /**
+ * The country to dial out to.
+ */
+ dialOutCountry: { name: string, dialCode: string, code: string },
+
+ /**
+ * The number to dial out to.
+ */
+ dialOutNumber: string,
+
+ /**
+ * Handler used when user presses 'Enter'.
+ */
+ onSubmit: Function,
+
+ /**
+ * Sets the dial out number.
+ */
+ setDialOutNumber: Function,
+
+ /**
+ * Sets the dial out country.
+ */
+ setDialOutCountry: Function,
+};
+
+type State = {
+
+ /**
+ * If the country picker is open or not.
+ */
+ isOpen: boolean,
+
+ /**
+ * The value of the input.
+ */
+ value: string
+}
+
+/**
+ * This component displays a country picker with an input for the phone number.
+ */
+class CountryPicker extends PureComponent {
+ /**
+ * A React ref to the HTML element containing the {@code input} instance.
+ */
+ inputRef: Object;
+
+ /**
+ * Initializes a new {@code CountryPicker} instance.
+ *
+ * @inheritdoc
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isOpen: false,
+ value: ''
+ };
+ this.inputRef = React.createRef();
+ this._onChange = this._onChange.bind(this);
+ this._onDropdownClose = this._onDropdownClose.bind(this);
+ this._onCountrySelectorClick = this._onCountrySelectorClick.bind(this);
+ this._onEntryClick = this._onEntryClick.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ }
+
+
+ /**
+ * Implements React's {@link Component#componentDidUnmount()}.
+ *
+ * @inheritdoc
+ */
+ componentDidMount() {
+ this.inputRef.current.focus();
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { dialOutCountry, dialOutNumber } = this.props;
+ const { isOpen } = this.state;
+ const {
+ inputRef,
+ _onChange,
+ _onCountrySelectorClick,
+ _onDropdownClose,
+ _onKeyPress,
+ _onEntryClick
+ } = this;
+
+ return (
+
+
}
+ isOpen = { isOpen }
+ onClose = { _onDropdownClose }>
+
+
+
+
+
+
+ );
+ }
+
+ _onChange: (Object) => void;
+
+ /**
+ * Handles the input text change.
+ * Automatically updates the country from the 'CountrySelector' if a
+ * phone number prefix is entered (00 or +).
+ *
+ * @param {Object} e - The synthetic event.
+ * @returns {void}
+ */
+ _onChange({ target: { value } }) {
+ if (PREFIX_REG.test(value)) {
+ const textWithDialCode = value.replace(PREFIX_REG, '');
+
+ if (textWithDialCode.length >= 4) {
+ const country = getCountryFromDialCodeText(textWithDialCode);
+
+ if (country) {
+ const rest = textWithDialCode.replace(country.dialCode, '');
+
+ this.props.setDialOutCountry(country);
+ this.props.setDialOutNumber(rest);
+
+ return;
+ }
+ }
+ }
+
+ this.props.setDialOutNumber(value);
+ }
+
+ _onCountrySelectorClick: (Object) => void;
+
+ /**
+ * Click handler for country selector.
+ *
+ * @param {Object} e - The synthetic event.
+ * @returns {void}
+ */
+ _onCountrySelectorClick(e) {
+ e.stopPropagation();
+
+ this.setState({
+ isOpen: !this.setState.isOpen
+ });
+ }
+
+ _onDropdownClose: () => void;
+
+ /**
+ * Closes the dropdown.
+ *
+ * @returns {void}
+ */
+ _onDropdownClose() {
+ this.setState({
+ isOpen: false
+ });
+ }
+
+ _onEntryClick: (Object) => void;
+
+ /**
+ * Click handler for a single entry from the dropdown.
+ *
+ * @param {Object} country - The country used for dialing out.
+ * @returns {void}
+ */
+ _onEntryClick(country) {
+ this.props.setDialOutCountry(country);
+ this._onDropdownClose();
+ }
+
+ _onKeyPress: (Object) => void;
+
+ /**
+ * Handler for key presses.
+ *
+ * @param {Object} e - The synthetic event.
+ * @returns {void}
+ */
+ _onKeyPress(e) {
+ if (e.key === 'Enter') {
+ this.props.onSubmit();
+ }
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to the React {@code Component} props.
+ *
+ * @param {Object} state - The redux state.
+ * @returns {Props}
+ */
+function mapStateToProps(state) {
+ return {
+ dialOutCountry: getDialOutCountry(state),
+ dialOutNumber: getDialOutNumber(state)
+ };
+}
+
+/**
+ * Maps redux actions to the props of the component.
+ *
+ * @type {{
+ * setDialOutCountry: Function,
+ * setDialOutNumber: Function
+ * }}
+ */
+const mapDispatchToProps = {
+ setDialOutCountry,
+ setDialOutNumber
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(CountryPicker);
diff --git a/react/features/prejoin/components/country-picker/CountryRow.js b/react/features/prejoin/components/country-picker/CountryRow.js
new file mode 100644
index 000000000..0eb98a45a
--- /dev/null
+++ b/react/features/prejoin/components/country-picker/CountryRow.js
@@ -0,0 +1,68 @@
+// @flow
+
+import React, { PureComponent } from 'react';
+
+type Props = {
+
+ /**
+ * Country of the entry.
+ */
+ country: { name: string, dialCode: string, code: string },
+
+ /**
+ * Entry click handler.
+ */
+ onEntryClick: Function,
+};
+
+/**
+ * This component displays a row from the country picker dropdown.
+ */
+class CountryRow extends PureComponent {
+ /**
+ * Initializes a new {@code CountryRow} instance.
+ *
+ * @param {Props} props - The props of the component.
+ */
+ constructor(props: Props) {
+ super(props);
+
+ this._onClick = this._onClick.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ country: { code, dialCode, name }
+ } = this.props;
+
+ return (
+
+
+
+ {`${name} (+${dialCode})`}
+
+
+ );
+ }
+
+ _onClick: () => void;
+
+ /**
+ * Click handler.
+ *
+ * @returns {void}
+ */
+ _onClick() {
+ this.props.onEntryClick(this.props.country);
+ }
+}
+
+export default CountryRow;
diff --git a/react/features/prejoin/components/country-picker/CountrySelector.js b/react/features/prejoin/components/country-picker/CountrySelector.js
new file mode 100644
index 000000000..74e45cf9d
--- /dev/null
+++ b/react/features/prejoin/components/country-picker/CountrySelector.js
@@ -0,0 +1,39 @@
+// @flow
+
+import React from 'react';
+import { Icon, IconArrowDown } from '../../../base/icons';
+
+type Props = {
+
+ /**
+ * Country object of the entry.
+ */
+ country: { name: string, dialCode: string, code: string },
+
+ /**
+ * Click handler for the selector.
+ */
+ onClick: Function,
+};
+
+/**
+ * This component displays the country selector with the flag.
+ *
+ * @returns {ReactElement}
+ */
+function CountrySelector({ country: { code, dialCode }, onClick }: Props) {
+ return (
+
+ );
+}
+
+export default CountrySelector;
diff --git a/react/features/prejoin/components/dialogs/CallingDialog.js b/react/features/prejoin/components/dialogs/CallingDialog.js
new file mode 100644
index 000000000..135216922
--- /dev/null
+++ b/react/features/prejoin/components/dialogs/CallingDialog.js
@@ -0,0 +1,64 @@
+// @flow
+
+import React from 'react';
+import { Avatar } from '../../../base/avatar';
+import { translate } from '../../../base/i18n';
+import { Icon, IconClose } from '../../../base/icons';
+import Label from '../Label';
+
+type Props = {
+
+ /**
+ * The phone number that is being called.
+ */
+ number: string,
+
+ /**
+ * Closes the dialog.
+ */
+ onClose: Function,
+
+ /**
+ * Handler used on hangup click.
+ */
+ onHangup: Function,
+
+ /**
+ * The status of the call.
+ */
+ status: string,
+
+ /**
+ * Used for translation.
+ */
+ t: Function,
+};
+
+/**
+ * Dialog displayed when the user gets called by the meeting.
+ *
+ * @param {Props} props - The props of the component.
+ * @returns {ReactElement}
+ */
+function CallingDialog(props: Props) {
+ const { number, onClose, status, t } = props;
+
+ return (
+
+
+
+
+
+
+
{number}
+
+ );
+}
+
+export default translate(CallingDialog);
diff --git a/react/features/prejoin/components/dialogs/DialInDialog.js b/react/features/prejoin/components/dialogs/DialInDialog.js
new file mode 100644
index 000000000..325a12a7a
--- /dev/null
+++ b/react/features/prejoin/components/dialogs/DialInDialog.js
@@ -0,0 +1,121 @@
+// @flow
+
+import React from 'react';
+import { translate } from '../../../base/i18n';
+import { Icon, IconArrowLeft } from '../../../base/icons';
+import { getCountryCodeFromPhone } from '../../utils';
+import ActionButton from '../buttons/ActionButton';
+import Label from '../Label';
+
+type Props = {
+
+ /**
+ * The number to call in order to join the conference.
+ */
+ number: string,
+
+ /**
+ * Handler used when clicking the back button.
+ */
+ onBack: Function,
+
+ /**
+ * Click handler for the text button.
+ */
+ onTextButtonClick: Function,
+
+ /**
+ * Click handler for primary button.
+ */
+ onPrimaryButtonClick: Function,
+
+ /**
+ * Click handler for the small additional text.
+ */
+ onSmallTextClick: Function,
+
+ /**
+ * The passCode of the conference.
+ */
+ passCode: string,
+
+ /**
+ * Used for translation.
+ */
+ t: Function,
+};
+
+/**
+ * This component displays the dialog whith all the information
+ * to join a meeting by calling it.
+ *
+ * @param {Props} props - The props of the component.
+ * @returns {React$Element}
+ */
+function DialinDialog(props: Props) {
+ const {
+ number,
+ onBack,
+ onPrimaryButtonClick,
+ onSmallTextClick,
+ onTextButtonClick,
+ passCode,
+ t
+ } = props;
+ const flagClassName = `prejoin-dialog-flag iti-flag ${getCountryCodeFromPhone(
+ number,
+ )}`;
+
+ return (
+
+
+
+
+ {t('prejoin.dialInMeeting')}
+
+
+
+
+
+
+
+ {t('prejoin.viewAllNumbers')}
+
+
+
+
+
+
+ {t('prejoin.joinMeeting')}
+
+
+ {t('dialog.Cancel')}
+
+
+
+ );
+}
+
+export default translate(DialinDialog);
diff --git a/react/features/prejoin/components/dialogs/DialOutDialog.js b/react/features/prejoin/components/dialogs/DialOutDialog.js
new file mode 100644
index 000000000..ba93e44fd
--- /dev/null
+++ b/react/features/prejoin/components/dialogs/DialOutDialog.js
@@ -0,0 +1,86 @@
+// @flow
+
+import React from 'react';
+
+import { translate } from '../../../base/i18n';
+import { Icon, IconClose } from '../../../base/icons';
+import ActionButton from '../buttons/ActionButton';
+import CountryPicker from '../country-picker/CountryPicker';
+import Label from '../Label';
+
+type Props = {
+
+ /**
+ * Closes a dialog.
+ */
+ onClose: Function,
+
+ /**
+ * Submit handler.
+ */
+ onSubmit: Function,
+
+ /**
+ * Handler for text button.
+ */
+ onTextButtonClick: Function,
+
+ /**
+ * Used for translation.
+ */
+ t: Function,
+};
+
+/**
+ * This component displays the dialog from wich the user can enter the
+ * phone number in order to be called by the meeting.
+ *
+ * @param {Props} props - The props of the component.
+ * @returns {React$Element}
+ */
+function DialOutDialog(props: Props) {
+ const { onClose, onTextButtonClick, onSubmit, t } = props;
+
+ return (
+
+
+
+ {t('prejoin.startWithPhone')}
+
+
+
+
+
+
+
+
+ {t('prejoin.callMe')}
+
+
+
+
+
+ {t('prejoin.or')}
+
+
+
+
+
+ {t('prejoin.iWantToDialIn')}
+
+
+
+ );
+}
+
+export default translate(DialOutDialog);
diff --git a/react/features/prejoin/components/dialogs/JoinByPhoneDialog.js b/react/features/prejoin/components/dialogs/JoinByPhoneDialog.js
new file mode 100644
index 000000000..0b11c6127
--- /dev/null
+++ b/react/features/prejoin/components/dialogs/JoinByPhoneDialog.js
@@ -0,0 +1,248 @@
+// @flow
+
+import React, { PureComponent } from 'react';
+
+import { connect } from '../../../base/redux';
+import {
+ getConferenceId,
+ getDefaultDialInNumber,
+ updateDialInNumbers
+} from '../../../invite';
+import {
+ dialOut as dialOutAction,
+ joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
+ openDialInPage as openDialInPageAction
+} from '../../actions';
+import { getDialOutStatus, getFullDialOutNumber } from '../../functions';
+
+import CallingDialog from './CallingDialog';
+import DialInDialog from './DialInDialog';
+import DialOutDialog from './DialOutDialog';
+
+type Props = {
+
+ /**
+ * The number to call in order to join the conference.
+ */
+ dialInNumber: string,
+
+ /**
+ * The status of the call when the meeting calss the user.
+ */
+ dialOutStatus: string,
+
+ /**
+ * The action by which the meeting calls the user.
+ */
+ dialOut: Function,
+
+ /**
+ * The number the conference should call.
+ */
+ dialOutNumber: string,
+
+ /**
+ * Fetches conference dial in numbers & conference id
+ */
+ fetchConferenceDetails: Function,
+
+ /**
+ * Joins the conference without audio.
+ */
+ joinConferenceWithoutAudio: Function,
+
+ /**
+ * Closes the dialog.
+ */
+ onClose: Function,
+
+ /**
+ * Opens a web page with all the dial in numbers.
+ */
+ openDialInPage: Function,
+
+ /**
+ * The passCode of the conference used when joining a meeting by phone.
+ */
+ passCode: string,
+};
+
+type State = {
+
+ /**
+ * The dialout call is ongoing, 'CallingDialog' is shown;
+ */
+ isCalling: boolean,
+
+ /**
+ * If should show 'DialInDialog'.
+ */
+ showDialIn: boolean,
+
+ /**
+ * If should show 'DialOutDialog'.
+ */
+ showDialOut: boolean
+}
+
+/**
+ * This is the dialog shown when a user wants to join with phone audio.
+ */
+class JoinByPhoneDialog extends PureComponent {
+ /**
+ * Initializes a new {@code JoinByPhoneDialog} instance.
+ *
+ * @param {Props} props - The props of the component.
+ * @inheritdoc
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isCalling: false,
+ showDialOut: true,
+ showDialIn: false
+ };
+
+ this._dialOut = this._dialOut.bind(this);
+ this._showDialInDialog = this._showDialInDialog.bind(this);
+ this._showDialOutDialog = this._showDialOutDialog.bind(this);
+ }
+
+ _dialOut: () => void;
+
+ /**
+ * Meeting calls the user & shows the 'CallingDialog'.
+ *
+ * @returns {void}
+ */
+ _dialOut() {
+ const { dialOut, joinConferenceWithoutAudio } = this.props;
+
+ this.setState({
+ isCalling: true,
+ showDialOut: false,
+ showDialIn: false
+ });
+ dialOut(joinConferenceWithoutAudio, this._showDialOutDialog);
+ }
+
+ _showDialInDialog: () => void;
+
+ /**
+ * Shows the 'DialInDialog'.
+ *
+ * @returns {void}
+ */
+ _showDialInDialog() {
+ this.setState({
+ isCalling: false,
+ showDialOut: false,
+ showDialIn: true
+ });
+ }
+
+ _showDialOutDialog: () => void;
+
+ /**
+ * Shows the 'DialOutDialog'.
+ *
+ * @returns {void}
+ */
+ _showDialOutDialog() {
+ this.setState({
+ isCalling: false,
+ showDialOut: true,
+ showDialIn: false
+ });
+ }
+
+ /**
+ * Implements React's {@link Component#componentDidMount()}.
+ *
+ * @inheritdoc
+ */
+ componentDidMount() {
+ this.props.fetchConferenceDetails();
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ dialOutStatus,
+ dialInNumber,
+ dialOutNumber,
+ joinConferenceWithoutAudio,
+ passCode,
+ onClose,
+ openDialInPage
+ } = this.props;
+ const {
+ _dialOut,
+ _showDialInDialog,
+ _showDialOutDialog
+ } = this;
+ const { isCalling, showDialOut, showDialIn } = this.state;
+ const className = isCalling
+ ? 'prejoin-dialog prejoin-dialog--small'
+ : 'prejoin-dialog';
+
+ return (
+
+
+ {showDialOut && (
+
+ )}
+ {showDialIn && (
+
+ )}
+ {isCalling && (
+
+ )}
+
+
+ );
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to the React {@code Component} props.
+ *
+ * @param {Object} state - The redux state.
+ * @returns {Object}
+ */
+function mapStateToProps(state): Object {
+ return {
+ dialInNumber: getDefaultDialInNumber(state),
+ dialOutNumber: getFullDialOutNumber(state),
+ dialOutStatus: getDialOutStatus(state),
+ passCode: getConferenceId(state)
+ };
+}
+
+const mapDispatchToProps = {
+ dialOut: dialOutAction,
+ fetchConferenceDetails: updateDialInNumbers,
+ joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
+ openDialInPage: openDialInPageAction
+};
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(JoinByPhoneDialog);
diff --git a/react/features/prejoin/functions.js b/react/features/prejoin/functions.js
index d007c677e..2de66c644 100644
--- a/react/features/prejoin/functions.js
+++ b/react/features/prejoin/functions.js
@@ -1,5 +1,7 @@
// @flow
+import { getRoomName } from '../base/conference';
+import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
/**
* Mutes or unmutes a track.
@@ -27,7 +29,7 @@ function applyMuteOptionsToTrack(track, shouldMute) {
* @returns {boolean}
*/
export function areJoinByPhoneButtonsVisible(state: Object): boolean {
- return state['features/prejoin'].buttonsVisible;
+ return Boolean(getDialOutUrl(state) && getDialOutStatusUrl(state));
}
/**
@@ -126,6 +128,59 @@ export function getDeviceStatusType(state: Object): string {
return state['features/prejoin']?.deviceStatusType;
}
+/**
+ * Returns the 'conferenceUrl' used for dialing out.
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getDialOutConferenceUrl(state: Object): string {
+ return `${getRoomName(state)}@${state['features/base/config'].hosts.muc}`;
+}
+
+/**
+ * Selector for getting the dial out country.
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {Object}
+ */
+export function getDialOutCountry(state: Object): Object {
+ return state['features/prejoin'].dialOutCountry;
+}
+
+/**
+ * Selector for getting the dial out number (without prefix).
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getDialOutNumber(state: Object): string {
+ return state['features/prejoin'].dialOutNumber;
+}
+
+/**
+ * Selector for getting the dial out status while calling.
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getDialOutStatus(state: Object): string {
+ return state['features/prejoin'].dialOutStatus;
+}
+
+/**
+ * Returns the full dial out number (containing country code and +).
+ *
+ * @param {Object} state - The state of the app.
+ * @returns {string}
+ */
+export function getFullDialOutNumber(state: Object): string {
+ const dialOutNumber = getDialOutNumber(state);
+ const country = getDialOutCountry(state);
+
+ return `+${country.dialCode}${dialOutNumber}`;
+}
+
/**
* Selector for getting the prejoin video track.
*
diff --git a/react/features/prejoin/reducer.js b/react/features/prejoin/reducer.js
index c817df838..d574d130d 100644
--- a/react/features/prejoin/reducer.js
+++ b/react/features/prejoin/reducer.js
@@ -5,6 +5,9 @@ import {
ADD_PREJOIN_CONTENT_SHARING_TRACK,
ADD_PREJOIN_VIDEO_TRACK,
SET_DEVICE_STATUS,
+ SET_DIALOUT_NUMBER,
+ SET_DIALOUT_COUNTRY,
+ SET_DIALOUT_STATUS,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_SKIP_PREJOIN,
SET_PREJOIN_AUDIO_DISABLED,
@@ -18,17 +21,26 @@ import {
const DEFAULT_STATE = {
audioDisabled: false,
audioMuted: false,
- videoMuted: false,
- videoDisabled: false,
+ audioTrack: null,
+ contentSharingTrack: null,
+ country: '',
deviceStatusText: 'prejoin.configuringDevices',
deviceStatusType: 'ok',
+ dialOutCountry: {
+ name: 'United States',
+ dialCode: '1',
+ code: 'us'
+ },
+ dialOutNumber: '',
+ dialOutStatus: 'prejoin.dialing',
+ name: '',
+ rawError: '',
showPrejoin: true,
showJoinByPhoneDialog: false,
userSelectedSkipPrejoin: false,
videoTrack: null,
- audioTrack: null,
- contentSharingTrack: null,
- rawError: ''
+ videoDisabled: false,
+ videoMuted: false
};
/**
@@ -114,6 +126,27 @@ ReducerRegistry.register(
};
}
+ case SET_DIALOUT_NUMBER: {
+ return {
+ ...state,
+ dialOutNumber: action.value
+ };
+ }
+
+ case SET_DIALOUT_COUNTRY: {
+ return {
+ ...state,
+ dialOutCountry: action.value
+ };
+ }
+
+ case SET_DIALOUT_STATUS: {
+ return {
+ ...state,
+ dialOutStatus: action.value
+ };
+ }
+
case SET_JOIN_BY_PHONE_DIALOG_VISIBLITY: {
return {
...state,
diff --git a/react/features/prejoin/utils.js b/react/features/prejoin/utils.js
new file mode 100644
index 000000000..dcb0b13d5
--- /dev/null
+++ b/react/features/prejoin/utils.js
@@ -0,0 +1,799 @@
+// @flow
+
+export const countries = [
+ { name: 'Afghanistan',
+ dialCode: '93',
+ code: 'af' },
+ { name: 'Aland Islands',
+ dialCode: '358',
+ code: 'ax' },
+ { name: 'Albania',
+ dialCode: '355',
+ code: 'al' },
+ { name: 'Algeria',
+ dialCode: '213',
+ code: 'dz' },
+ { name: 'AmericanSamoa',
+ dialCode: '1684',
+ code: 'as' },
+ { name: 'Andorra',
+ dialCode: '376',
+ code: 'ad' },
+ { name: 'Angola',
+ dialCode: '244',
+ code: 'ao' },
+ { name: 'Anguilla',
+ dialCode: '1264',
+ code: 'ai' },
+ { name: 'Antarctica',
+ dialCode: '672',
+ code: 'aq' },
+ { name: 'Antigua and Barbuda',
+ dialCode: '1268',
+ code: 'ag' },
+ { name: 'Argentina',
+ dialCode: '54',
+ code: 'ar' },
+ { name: 'Armenia',
+ dialCode: '374',
+ code: 'am' },
+ { name: 'Aruba',
+ dialCode: '297',
+ code: 'aw' },
+ { name: 'Australia',
+ dialCode: '61',
+ code: 'au' },
+ { name: 'Austria',
+ dialCode: '43',
+ code: 'at' },
+ { name: 'Azerbaijan',
+ dialCode: '994',
+ code: 'az' },
+ { name: 'Bahamas',
+ dialCode: '1242',
+ code: 'bs' },
+ { name: 'Bahrain',
+ dialCode: '973',
+ code: 'bh' },
+ { name: 'Bangladesh',
+ dialCode: '880',
+ code: 'bd' },
+ { name: 'Barbados',
+ dialCode: '1246',
+ code: 'bb' },
+ { name: 'Belarus',
+ dialCode: '375',
+ code: 'by' },
+ { name: 'Belgium',
+ dialCode: '32',
+ code: 'be' },
+ { name: 'Belize',
+ dialCode: '501',
+ code: 'bz' },
+ { name: 'Benin',
+ dialCode: '229',
+ code: 'bj' },
+ { name: 'Bermuda',
+ dialCode: '1441',
+ code: 'bm' },
+ { name: 'Bhutan',
+ dialCode: '975',
+ code: 'bt' },
+ { name: 'Bolivia, Plurinational State of',
+ dialCode: '591',
+ code: 'bo' },
+ { name: 'Bosnia and Herzegovina',
+ dialCode: '387',
+ code: 'ba' },
+ { name: 'Botswana',
+ dialCode: '267',
+ code: 'bw' },
+ { name: 'Brazil',
+ dialCode: '55',
+ code: 'br' },
+ { name: 'British Indian Ocean Territory',
+ dialCode: '246',
+ code: 'io' },
+ { name: 'Brunei Darussalam',
+ dialCode: '673',
+ code: 'bn' },
+ { name: 'Bulgaria',
+ dialCode: '359',
+ code: 'bg' },
+ { name: 'Burkina Faso',
+ dialCode: '226',
+ code: 'bf' },
+ { name: 'Burundi',
+ dialCode: '257',
+ code: 'bi' },
+ { name: 'Cambodia',
+ dialCode: '855',
+ code: 'kh' },
+ { name: 'Cameroon',
+ dialCode: '237',
+ code: 'cm' },
+ { name: 'Canada',
+ dialCode: '1',
+ code: 'ca' },
+ { name: 'Cape Verde',
+ dialCode: '238',
+ code: 'cv' },
+ { name: 'Cayman Islands',
+ dialCode: ' 345',
+ code: 'ky' },
+ { name: 'Central African Republic',
+ dialCode: '236',
+ code: 'cf' },
+ { name: 'Chad',
+ dialCode: '235',
+ code: 'td' },
+ { name: 'Chile',
+ dialCode: '56',
+ code: 'cl' },
+ { name: 'China',
+ dialCode: '86',
+ code: 'cn' },
+ { name: 'Christmas Island',
+ dialCode: '61',
+ code: 'cx' },
+ { name: 'Cocos (Keeling) Islands',
+ dialCode: '61',
+ code: 'cc' },
+ { name: 'Colombia',
+ dialCode: '57',
+ code: 'co' },
+ { name: 'Comoros',
+ dialCode: '269',
+ code: 'km' },
+ { name: 'Congo',
+ dialCode: '242',
+ code: 'cg' },
+ {
+ name: 'Congo, The Democratic Republic of the Congo',
+ dialCode: '243',
+ code: 'cd'
+ },
+ { name: 'Cook Islands',
+ dialCode: '682',
+ code: 'ck' },
+ { name: 'Costa Rica',
+ dialCode: '506',
+ code: 'cr' },
+ { name: 'Cote d\'Ivoire',
+ dialCode: '225',
+ code: 'ci' },
+ { name: 'Croatia',
+ dialCode: '385',
+ code: 'hr' },
+ { name: 'Cuba',
+ dialCode: '53',
+ code: 'cu' },
+ { name: 'Cyprus',
+ dialCode: '357',
+ code: 'cy' },
+ { name: 'Czech Republic',
+ dialCode: '420',
+ code: 'cz' },
+ { name: 'Denmark',
+ dialCode: '45',
+ code: 'dk' },
+ { name: 'Djibouti',
+ dialCode: '253',
+ code: 'dj' },
+ { name: 'Dominica',
+ dialCode: '1767',
+ code: 'dm' },
+ { name: 'Dominican Republic',
+ dialCode: '1849',
+ code: 'do' },
+ { name: 'Ecuador',
+ dialCode: '593',
+ code: 'ec' },
+ { name: 'Egypt',
+ dialCode: '20',
+ code: 'eg' },
+ { name: 'El Salvador',
+ dialCode: '503',
+ code: 'sv' },
+ { name: 'Equatorial Guinea',
+ dialCode: '240',
+ code: 'gq' },
+ { name: 'Eritrea',
+ dialCode: '291',
+ code: 'er' },
+ { name: 'Estonia',
+ dialCode: '372',
+ code: 'ee' },
+ { name: 'Ethiopia',
+ dialCode: '251',
+ code: 'et' },
+ { name: 'Falkland Islands (Malvinas)',
+ dialCode: '500',
+ code: 'fk' },
+ { name: 'Faroe Islands',
+ dialCode: '298',
+ code: 'fo' },
+ { name: 'Fiji',
+ dialCode: '679',
+ code: 'fj' },
+ { name: 'Finland',
+ dialCode: '358',
+ code: 'fi' },
+ { name: 'France',
+ dialCode: '33',
+ code: 'fr' },
+ { name: 'French Guiana',
+ dialCode: '594',
+ code: 'gf' },
+ { name: 'French Polynesia',
+ dialCode: '689',
+ code: 'pf' },
+ { name: 'Gabon',
+ dialCode: '241',
+ code: 'ga' },
+ { name: 'Gambia',
+ dialCode: '220',
+ code: 'gm' },
+ { name: 'Georgia',
+ dialCode: '995',
+ code: 'ge' },
+ { name: 'Germany',
+ dialCode: '49',
+ code: 'de' },
+ { name: 'Ghana',
+ dialCode: '233',
+ code: 'gh' },
+ { name: 'Gibraltar',
+ dialCode: '350',
+ code: 'gi' },
+ { name: 'Greece',
+ dialCode: '30',
+ code: 'gr' },
+ { name: 'Greenland',
+ dialCode: '299',
+ code: 'gl' },
+ { name: 'Grenada',
+ dialCode: '1473',
+ code: 'gd' },
+ { name: 'Guadeloupe',
+ dialCode: '590',
+ code: 'gp' },
+ { name: 'Guam',
+ dialCode: '1671',
+ code: 'gu' },
+ { name: 'Guatemala',
+ dialCode: '502',
+ code: 'gt' },
+ { name: 'Guernsey',
+ dialCode: '44',
+ code: 'gg' },
+ { name: 'Guinea',
+ dialCode: '224',
+ code: 'gn' },
+ { name: 'Guinea-Bissau',
+ dialCode: '245',
+ code: 'gw' },
+ { name: 'Guyana',
+ dialCode: '595',
+ code: 'gy' },
+ { name: 'Haiti',
+ dialCode: '509',
+ code: 'ht' },
+ { name: 'Holy See (Vatican City State)',
+ dialCode: '379',
+ code: 'va' },
+ { name: 'Honduras',
+ dialCode: '504',
+ code: 'hn' },
+ { name: 'Hong Kong',
+ dialCode: '852',
+ code: 'hk' },
+ { name: 'Hungary',
+ dialCode: '36',
+ code: 'hu' },
+ { name: 'Iceland',
+ dialCode: '354',
+ code: 'is' },
+ { name: 'India',
+ dialCode: '91',
+ code: 'in' },
+ { name: 'Indonesia',
+ dialCode: '62',
+ code: 'id' },
+ {
+ name: 'Iran, Islamic Republic of Persian Gulf',
+ dialCode: '98',
+ code: 'ir'
+ },
+ { name: 'Iraq',
+ dialCode: '964',
+ code: 'iq' },
+ { name: 'Ireland',
+ dialCode: '353',
+ code: 'ie' },
+ { name: 'Isle of Man',
+ dialCode: '44',
+ code: 'im' },
+ { name: 'Israel',
+ dialCode: '972',
+ code: 'il' },
+ { name: 'Italy',
+ dialCode: '39',
+ code: 'it' },
+ { name: 'Jamaica',
+ dialCode: '1876',
+ code: 'jm' },
+ { name: 'Japan',
+ dialCode: '81',
+ code: 'jp' },
+ { name: 'Jersey',
+ dialCode: '44',
+ code: 'je' },
+ { name: 'Jordan',
+ dialCode: '962',
+ code: 'jo' },
+ { name: 'Kazakhstan',
+ dialCode: '77',
+ code: 'kz' },
+ { name: 'Kenya',
+ dialCode: '254',
+ code: 'ke' },
+ { name: 'Kiribati',
+ dialCode: '686',
+ code: 'ki' },
+ {
+ name: 'Korea, Democratic People\'s Republic of Korea',
+ dialCode: '850',
+ code: 'kp'
+ },
+ { name: 'Korea, Republic of South Korea',
+ dialCode: '82',
+ code: 'kr' },
+ { name: 'Kuwait',
+ dialCode: '965',
+ code: 'kw' },
+ { name: 'Kyrgyzstan',
+ dialCode: '996',
+ code: 'kg' },
+ { name: 'Laos',
+ dialCode: '856',
+ code: 'la' },
+ { name: 'Latvia',
+ dialCode: '371',
+ code: 'lv' },
+ { name: 'Lebanon',
+ dialCode: '961',
+ code: 'lb' },
+ { name: 'Lesotho',
+ dialCode: '266',
+ code: 'ls' },
+ { name: 'Liberia',
+ dialCode: '231',
+ code: 'lr' },
+ { name: 'Libyan Arab Jamahiriya',
+ dialCode: '218',
+ code: 'ly' },
+ { name: 'Liechtenstein',
+ dialCode: '423',
+ code: 'li' },
+ { name: 'Lithuania',
+ dialCode: '370',
+ code: 'lt' },
+ { name: 'Luxembourg',
+ dialCode: '352',
+ code: 'lu' },
+ { name: 'Macao',
+ dialCode: '853',
+ code: 'mo' },
+ { name: 'Macedonia',
+ dialCode: '389',
+ code: 'mk' },
+ { name: 'Madagascar',
+ dialCode: '261',
+ code: 'mg' },
+ { name: 'Malawi',
+ dialCode: '265',
+ code: 'mw' },
+ { name: 'Malaysia',
+ dialCode: '60',
+ code: 'my' },
+ { name: 'Maldives',
+ dialCode: '960',
+ code: 'mv' },
+ { name: 'Mali',
+ dialCode: '223',
+ code: 'ml' },
+ { name: 'Malta',
+ dialCode: '356',
+ code: 'mt' },
+ { name: 'Marshall Islands',
+ dialCode: '692',
+ code: 'mh' },
+ { name: 'Martinique',
+ dialCode: '596',
+ code: 'mq' },
+ { name: 'Mauritania',
+ dialCode: '222',
+ code: 'mr' },
+ { name: 'Mauritius',
+ dialCode: '230',
+ code: 'mu' },
+ { name: 'Mayotte',
+ dialCode: '262',
+ code: 'yt' },
+ { name: 'Mexico',
+ dialCode: '52',
+ code: 'mx' },
+ {
+ name: 'Micronesia, Federated States of Micronesia',
+ dialCode: '691',
+ code: 'fm'
+ },
+ { name: 'Moldova',
+ dialCode: '373',
+ code: 'md' },
+ { name: 'Monaco',
+ dialCode: '377',
+ code: 'mc' },
+ { name: 'Mongolia',
+ dialCode: '976',
+ code: 'mn' },
+ { name: 'Montenegro',
+ dialCode: '382',
+ code: 'me' },
+ { name: 'Montserrat',
+ dialCode: '1664',
+ code: 'ms' },
+ { name: 'Morocco',
+ dialCode: '212',
+ code: 'ma' },
+ { name: 'Mozambique',
+ dialCode: '258',
+ code: 'mz' },
+ { name: 'Myanmar',
+ dialCode: '95',
+ code: 'mm' },
+ { name: 'Namibia',
+ dialCode: '264',
+ code: 'na' },
+ { name: 'Nauru',
+ dialCode: '674',
+ code: 'nr' },
+ { name: 'Nepal',
+ dialCode: '977',
+ code: 'np' },
+ { name: 'Netherlands',
+ dialCode: '31',
+ code: 'nl' },
+ { name: 'Netherlands Antilles',
+ dialCode: '599',
+ code: 'an' },
+ { name: 'New Caledonia',
+ dialCode: '687',
+ code: 'nc' },
+ { name: 'New Zealand',
+ dialCode: '64',
+ code: 'nz' },
+ { name: 'Nicaragua',
+ dialCode: '505',
+ code: 'ni' },
+ { name: 'Niger',
+ dialCode: '227',
+ code: 'ne' },
+ { name: 'Nigeria',
+ dialCode: '234',
+ code: 'ng' },
+ { name: 'Niue',
+ dialCode: '683',
+ code: 'nu' },
+ { name: 'Norfolk Island',
+ dialCode: '672',
+ code: 'nf' },
+ { name: 'Northern Mariana Islands',
+ dialCode: '1670',
+ code: 'mp' },
+ { name: 'Norway',
+ dialCode: '47',
+ code: 'no' },
+ { name: 'Oman',
+ dialCode: '968',
+ code: 'om' },
+ { name: 'Pakistan',
+ dialCode: '92',
+ code: 'pk' },
+ { name: 'Palau',
+ dialCode: '680',
+ code: 'pw' },
+ { name: 'Palestinian Territory, Occupied',
+ dialCode: '970',
+ code: 'ps' },
+ { name: 'Panama',
+ dialCode: '507',
+ code: 'pa' },
+ { name: 'Papua New Guinea',
+ dialCode: '675',
+ code: 'pg' },
+ { name: 'Paraguay',
+ dialCode: '595',
+ code: 'py' },
+ { name: 'Peru',
+ dialCode: '51',
+ code: 'pe' },
+ { name: 'Philippines',
+ dialCode: '63',
+ code: 'ph' },
+ { name: 'Pitcairn',
+ dialCode: '872',
+ code: 'pn' },
+ { name: 'Poland',
+ dialCode: '48',
+ code: 'pl' },
+ { name: 'Portugal',
+ dialCode: '351',
+ code: 'pt' },
+ { name: 'Puerto Rico',
+ dialCode: '1939',
+ code: 'pr' },
+ { name: 'Qatar',
+ dialCode: '974',
+ code: 'qa' },
+ { name: 'Romania',
+ dialCode: '40',
+ code: 'ro' },
+ { name: 'Russia',
+ dialCode: '7',
+ code: 'ru' },
+ { name: 'Rwanda',
+ dialCode: '250',
+ code: 'rw' },
+ { name: 'Reunion',
+ dialCode: '262',
+ code: 're' },
+ { name: 'Saint Barthelemy',
+ dialCode: '590',
+ code: 'bl' },
+ {
+ name: 'Saint Helena, Ascension and Tristan Da Cunha',
+ dialCode: '290',
+ code: 'sh'
+ },
+ { name: 'Saint Kitts and Nevis',
+ dialCode: '1869',
+ code: 'kn' },
+ { name: 'Saint Lucia',
+ dialCode: '1758',
+ code: 'lc' },
+ { name: 'Saint Martin',
+ dialCode: '590',
+ code: 'mf' },
+ { name: 'Saint Pierre and Miquelon',
+ dialCode: '508',
+ code: 'pm' },
+ { name: 'Saint Vincent and the Grenadines',
+ dialCode: '1784',
+ code: 'vc' },
+ { name: 'Samoa',
+ dialCode: '685',
+ code: 'ws' },
+ { name: 'San Marino',
+ dialCode: '378',
+ code: 'sm' },
+ { name: 'Sao Tome and Principe',
+ dialCode: '239',
+ code: 'st' },
+ { name: 'Saudi Arabia',
+ dialCode: '966',
+ code: 'sa' },
+ { name: 'Senegal',
+ dialCode: '221',
+ code: 'sn' },
+ { name: 'Serbia',
+ dialCode: '381',
+ code: 'rs' },
+ { name: 'Seychelles',
+ dialCode: '248',
+ code: 'sc' },
+ { name: 'Sierra Leone',
+ dialCode: '232',
+ code: 'sl' },
+ { name: 'Singapore',
+ dialCode: '65',
+ code: 'sg' },
+ { name: 'Slovakia',
+ dialCode: '421',
+ code: 'sk' },
+ { name: 'Slovenia',
+ dialCode: '386',
+ code: 'si' },
+ { name: 'Solomon Islands',
+ dialCode: '677',
+ code: 'sb' },
+ { name: 'Somalia',
+ dialCode: '252',
+ code: 'so' },
+ { name: 'South Africa',
+ dialCode: '27',
+ code: 'za' },
+ { name: 'South Sudan',
+ dialCode: '211',
+ code: 'ss' },
+ {
+ name: 'South Georgia and the South Sandwich Islands',
+ dialCode: '500',
+ code: 'gs'
+ },
+ { name: 'Spain',
+ dialCode: '34',
+ code: 'es' },
+ { name: 'Sri Lanka',
+ dialCode: '94',
+ code: 'lk' },
+ { name: 'Sudan',
+ dialCode: '249',
+ code: 'sd' },
+ { name: 'Suriname',
+ dialCode: '597',
+ code: 'sr' },
+ { name: 'Svalbard and Jan Mayen',
+ dialCode: '47',
+ code: 'sj' },
+ { name: 'Swaziland',
+ dialCode: '268',
+ code: 'sz' },
+ { name: 'Sweden',
+ dialCode: '46',
+ code: 'se' },
+ { name: 'Switzerland',
+ dialCode: '41',
+ code: 'ch' },
+ { name: 'Syrian Arab Republic',
+ dialCode: '963',
+ code: 'sy' },
+ { name: 'Taiwan',
+ dialCode: '886',
+ code: 'tw' },
+ { name: 'Tajikistan',
+ dialCode: '992',
+ code: 'tj' },
+ {
+ name: 'Tanzania, United Republic of Tanzania',
+ dialCode: '255',
+ code: 'tz'
+ },
+ { name: 'Thailand',
+ dialCode: '66',
+ code: 'th' },
+ { name: 'Timor-Leste',
+ dialCode: '670',
+ code: 'tl' },
+ { name: 'Togo',
+ dialCode: '228',
+ code: 'tg' },
+ { name: 'Tokelau',
+ dialCode: '690',
+ code: 'tk' },
+ { name: 'Tonga',
+ dialCode: '676',
+ code: 'to' },
+ { name: 'Trinidad and Tobago',
+ dialCode: '1868',
+ code: 'tt' },
+ { name: 'Tunisia',
+ dialCode: '216',
+ code: 'tn' },
+ { name: 'Turkey',
+ dialCode: '90',
+ code: 'tr' },
+ { name: 'Turkmenistan',
+ dialCode: '993',
+ code: 'tm' },
+ { name: 'Turks and Caicos Islands',
+ dialCode: '1649',
+ code: 'tc' },
+ { name: 'Tuvalu',
+ dialCode: '688',
+ code: 'tv' },
+ { name: 'Uganda',
+ dialCode: '256',
+ code: 'ug' },
+ { name: 'Ukraine',
+ dialCode: '380',
+ code: 'ua' },
+ { name: 'United Arab Emirates',
+ dialCode: '971',
+ code: 'ae' },
+ { name: 'United Kingdom',
+ dialCode: '44',
+ code: 'gb' },
+ { name: 'United States',
+ dialCode: '1',
+ code: 'us' },
+ { name: 'Uruguay',
+ dialCode: '598',
+ code: 'uy' },
+ { name: 'Uzbekistan',
+ dialCode: '998',
+ code: 'uz' },
+ { name: 'Vanuatu',
+ dialCode: '678',
+ code: 'vu' },
+ {
+ name: 'Venezuela, Bolivarian Republic of Venezuela',
+ dialCode: '58',
+ code: 've'
+ },
+ { name: 'Vietnam',
+ dialCode: '84',
+ code: 'vn' },
+ { name: 'Virgin Islands, British',
+ dialCode: '1284',
+ code: 'vg' },
+ { name: 'Virgin Islands, U.S.',
+ dialCode: '1340',
+ code: 'vi' },
+ { name: 'Wallis and Futuna',
+ dialCode: '681',
+ code: 'wf' },
+ { name: 'Yemen',
+ dialCode: '967',
+ code: 'ye' },
+ { name: 'Zambia',
+ dialCode: '260',
+ code: 'zm' },
+ { name: 'Zimbabwe',
+ dialCode: '263',
+ code: 'zw' }
+];
+
+const countriesByCodeMap = countries.reduce((result, country) => {
+ result[country.dialCode] = country;
+
+ return result;
+}, {});
+
+/**
+ * Map between country dial codes and country objects.
+ *
+ */
+const codesByNumbersMap = countries.reduce((result, country) => {
+ result[country.dialCode] = country.code;
+
+ return result;
+}, {});
+
+/**
+ * Returns the corresponding country code from a phone number.
+ *
+ * @param {string} phoneNumber - The phone number.
+ * @returns {string}
+ */
+export function getCountryCodeFromPhone(phoneNumber: string): string {
+ const number = phoneNumber.replace(/[+.\s]/g, '');
+
+
+ for (let i = 4; i > 0; i--) {
+ const prefix = number.slice(0, i);
+
+ if (codesByNumbersMap[prefix]) {
+ return codesByNumbersMap[prefix];
+ }
+ }
+
+ return '';
+}
+
+/**
+ * Returns the corresponding country for a text starting with the dial code.
+ *
+ * @param {string} text - The text containing the dial code.
+ * @returns {Object}
+ */
+export function getCountryFromDialCodeText(text: string): Object {
+ return (
+ countriesByCodeMap[text.slice(0, 4)]
+ || countriesByCodeMap[text.slice(0, 3)]
+ || countriesByCodeMap[text.slice(0, 2)]
+ || countriesByCodeMap[text.slice(0, 1)]
+ || null
+ );
+}