diff --git a/react/features/invite/actionTypes.js b/react/features/invite/actionTypes.js index 7481da176..508ff8d77 100644 --- a/react/features/invite/actionTypes.js +++ b/react/features/invite/actionTypes.js @@ -61,6 +61,15 @@ export const SET_DIAL_IN_SUMMARY_VISIBLE = 'SET_DIAL_IN_SUMMARY_VISIBLE'; */ export const SET_INVITE_DIALOG_VISIBLE = 'SET_INVITE_DIALOG_VISIBLE'; +/** + * The type of Redux action which stores pending dtmf. Check out the {@link storePendingDTMF} action for more details. + * { + * type: STORE_PENDING_DTMF, + * pendingDtmf: ?string + * } + */ +export const STORE_PENDING_DTMF = 'STORE_PENDING_DTMF'; + /** * The type of the action which signals an error occurred while requesting dial- * in numbers. diff --git a/react/features/invite/actions.js b/react/features/invite/actions.js index 1086dc90b..39891fe9c 100644 --- a/react/features/invite/actions.js +++ b/react/features/invite/actions.js @@ -13,13 +13,15 @@ import { SET_CALLEE_INFO_VISIBLE, SET_DIAL_IN_SUMMARY_VISIBLE, SET_INVITE_DIALOG_VISIBLE, + STORE_PENDING_DTMF, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; import { getDialInConferenceID, getDialInNumbers, - invitePeopleAndChatRooms + invitePeopleAndChatRooms, + splitPhoneNumberAndDTMF } from './functions'; import logger from './logger'; @@ -98,7 +100,12 @@ export function invite( // For each number, dial out. On success, remove the number from // {@link invitesLeftToSend}. const phoneInvitePromises = phoneNumbers.map(item => { - const numberToInvite = item.number; + const { + dtmf, + phoneNumber: numberToInvite + } = splitPhoneNumberAndDTMF(item.number); + + dtmf && dispatch(storePendingDTMF(dtmf)); return conference.dial(numberToInvite) .then(() => { @@ -157,6 +164,25 @@ export function invite( }; } +/** + * Stores pending DTMF tones to be sent after a phone connection is established. If more than one phone numbers are + * invited into the conference then weird things may happen, as there's no way to match participant with the invited + * phone number. + * + * @param {string} pendingDtmf - A string with digits and dtmf characters that will be sent after Jigasi participant + * joins the conference and enters the {@link CONNECTED_USER} state. + * @returns {{ + * pendingDtmf: ?string, + * type: STORE_PENDING_DTMF + * }} + */ +export function storePendingDTMF(pendingDtmf: ?string) { + return { + type: STORE_PENDING_DTMF, + pendingDtmf + }; +} + /** * Sends AJAX requests for dial-in numbers and conference ID. * diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index 41fbd2eb3..dbbba2901 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -340,7 +340,9 @@ function isMaybeAPhoneNumber(text: string): boolean { * @returns {RegExp} */ function isPhoneNumberRegex(): RegExp { - let regexString = '^[0-9+()-\\s]*$'; + // The ',' '*' '#' characters are allowed to be able to specify pending DTMF tones to be dialed just after the phone + // connection is established. + let regexString = '^[0-9+()-\\s,*#]*$'; if (typeof interfaceConfig !== 'undefined') { regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString; @@ -349,6 +351,37 @@ function isPhoneNumberRegex(): RegExp { return new RegExp(regexString); } +/** + * Extracts the DTMF part out of the phone number text. The DTMF is to be specified after a comma and must contain + * only DTMF tone characters for example: "+12223334444,23,4*9#". + * + * @param {string} phoneNumberText - See the text above. + * @returns {{ + * phoneNumber: string, + * dtmf: ?string + * }} + */ +export function splitPhoneNumberAndDTMF(phoneNumberText: string) { + const firstCommaIdx = phoneNumberText.indexOf(','); + let dtmf, phoneNumber = phoneNumberText; + + if (firstCommaIdx !== -1) { + dtmf = phoneNumberText.substr(firstCommaIdx); + + phoneNumber = phoneNumberText.substr(0, firstCommaIdx); + + // There's no point in playing just commas, so clear the dtmf var + if (!dtmf.replace(',', '').length) { + dtmf = undefined; + } + } + + return { + phoneNumber, + dtmf + }; +} + /** * Sends an ajax request to a directory service. * diff --git a/react/features/invite/index.js b/react/features/invite/index.js index 48450b4e0..18b0eefde 100644 --- a/react/features/invite/index.js +++ b/react/features/invite/index.js @@ -4,3 +4,4 @@ export * from './functions'; import './reducer'; import './middleware'; +import './pendingDtmfMiddleware'; diff --git a/react/features/invite/pendingDtmfMiddleware.js b/react/features/invite/pendingDtmfMiddleware.js new file mode 100644 index 000000000..35e4ac382 --- /dev/null +++ b/react/features/invite/pendingDtmfMiddleware.js @@ -0,0 +1,39 @@ +import { CONNECTED_USER } from '../presence-status'; + +import { getCurrentConference } from '../base/conference'; +import { StateListenerRegistry } from '../base/redux'; + +import { storePendingDTMF } from './actions'; +import logger from './logger'; +import { getPendingDtmf } from './selectors'; + +StateListenerRegistry.register( + state => { + const jigasiParticipantId + = state['features/base/participants'] + .find(p => p.isJigasi && p.presence?.toLowerCase() === CONNECTED_USER) + ?.id; + + return jigasiParticipantId; + }, + (jigasiParticipantId, { getState, dispatch }) => { + const state = getState(); + const pendingDtmf = jigasiParticipantId && getPendingDtmf(state); + const conference = getCurrentConference(state); + + if (conference && pendingDtmf) { + logger.info('Sending pending DTMF tones'); + conference.sendTones(pendingDtmf); + dispatch(storePendingDTMF(undefined)); + } + } +); + +StateListenerRegistry.register( + state => getCurrentConference(state), + (conference, { dispatch }, prevConference) => { + if (prevConference && conference !== prevConference) { + dispatch(storePendingDTMF(undefined)); + } + } +); diff --git a/react/features/invite/reducer.js b/react/features/invite/reducer.js index 4e11f5891..9479f575b 100644 --- a/react/features/invite/reducer.js +++ b/react/features/invite/reducer.js @@ -8,6 +8,7 @@ import { SET_CALLEE_INFO_VISIBLE, SET_DIAL_IN_SUMMARY_VISIBLE, SET_INVITE_DIALOG_VISIBLE, + STORE_PENDING_DTMF, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; @@ -23,6 +24,13 @@ const DEFAULT_STATE = { calleeInfoVisible: false, inviteDialogVisible: false, numbersEnabled: true, + + /** + * Pending DTMF tones. See {@link storePendingDTMF} for more info. + * + * @type {string|undefined} + */ + pendingDtmf: undefined, pendingInviteRequests: [] }; @@ -62,6 +70,12 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => { inviteDialogVisible: action.visible }; + case STORE_PENDING_DTMF: + return { + ...state, + pendingDtmf: action.pendingDtmf + }; + case UPDATE_DIAL_IN_NUMBERS_FAILED: return { ...state, diff --git a/react/features/invite/selectors.js b/react/features/invite/selectors.js new file mode 100644 index 000000000..63accd9c7 --- /dev/null +++ b/react/features/invite/selectors.js @@ -0,0 +1,9 @@ +/** + * Retrieves pending DTMF tones if any. See {@link storePendingDTMF} for more info. + * + * @param {Object} state - The Redux state. + * @returns {string} + */ +export function getPendingDtmf(state) { + return state['features/invite'].pendingDtmf; +}