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 ( +
+
+ {`+${dialCode}`} + +
+ ); +} + +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')} +
+
+ + +
+
+
+ {number} +
+
{passCode}
+
+
+ + {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 + ); +}