diff --git a/modules/API/API.js b/modules/API/API.js index d25b4596e..7ad6daba5 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -1,12 +1,12 @@ // @flow import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; -import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { createApiEvent, sendAnalytics } from '../../react/features/analytics'; -import { sendInvitesForItems } from '../../react/features/invite'; +import { parseJWTFromURLParams } from '../../react/features/base/jwt'; +import { invite } from '../../react/features/invite'; import { getJitsiMeetTransport } from '../transport'; import { API_ID } from './constants'; @@ -114,15 +114,20 @@ function initCommands() { switch (name) { case 'invite': APP.store.dispatch( - sendInvitesForItems(request.invitees)) - .then(failedInvites => { - const failed = failedInvites.length === 0; + invite(request.invitees)) + .then(failedInvitees => { + let error; + let result; + + if (failedInvitees.length) { + error = new Error('One or more invites failed!'); + } else { + result = true; + } callback({ - result: failed ? undefined : true, - error: failed - ? new Error('One or more invites failed!') - : undefined + error, + result }); }); break; diff --git a/react/features/invite/actions.js b/react/features/invite/actions.js index 921f091b4..e0ec964a2 100644 --- a/react/features/invite/actions.js +++ b/react/features/invite/actions.js @@ -32,6 +32,91 @@ export function beginAddPeople() { }; } +/** + * Invites (i.e. sends invites to) an array of invitees (which may be a + * combination of users, rooms, phone numbers, and video rooms). + * + * @param {Array} invitees - The recepients to send invites to. + * @returns {Promise>} A {@code Promise} resolving with an array + * of invitees who were not invited (i.e. invites were not sent to them). + */ +export function invite(invitees: Array) { + return ( + dispatch: Dispatch<*>, + getState: Function): Promise> => { + let allInvitePromises = []; + let invitesLeftToSend = [ ...invitees ]; + + const state = getState(); + const { conference } = state['features/base/conference']; + const { inviteServiceUrl } = state['features/base/config']; + const inviteUrl = getInviteURL(state); + const { jwt } = state['features/base/jwt']; + + // First create all promises for dialing out. + if (conference) { + const phoneNumbers + = invitesLeftToSend.filter(({ type }) => type === 'phone'); + + // For each number, dial out. On success, remove the number from + // {@link invitesLeftToSend}. + const phoneInvitePromises = phoneNumbers.map(item => { + const numberToInvite = getDigitsOnly(item.number); + + return conference.dial(numberToInvite) + .then(() => { + invitesLeftToSend + = invitesLeftToSend.filter( + invitee => invitee !== item); + }) + .catch(error => + logger.error('Error inviting phone number:', error)); + }); + + allInvitePromises = allInvitePromises.concat(phoneInvitePromises); + } + + const usersAndRooms + = invitesLeftToSend.filter( + ({ type }) => type === 'user' || type === 'room'); + + if (usersAndRooms.length) { + // Send a request to invite all the rooms and users. On success, + // filter all rooms and users from {@link invitesLeftToSend}. + const peopleInvitePromise + = invitePeopleAndChatRooms( + inviteServiceUrl, + inviteUrl, + jwt, + usersAndRooms) + .then(() => { + invitesLeftToSend + = invitesLeftToSend.filter( + ({ type }) => type !== 'user' && type !== 'room'); + }) + .catch(error => logger.error('Error inviting people:', error)); + + allInvitePromises.push(peopleInvitePromise); + } + + // Sipgw calls are fire and forget. Invite them to the conference, then + // immediately remove them from invitesLeftToSend. + const vrooms + = invitesLeftToSend.filter(({ type }) => type === 'videosipgw'); + + conference + && vrooms.length > 0 + && dispatch(inviteVideoRooms(conference, vrooms)); + + invitesLeftToSend + = invitesLeftToSend.filter(({ type }) => type !== 'videosipgw'); + + return ( + Promise.all(allInvitePromises) + .then(() => invitesLeftToSend)); + }; +} + /** * Sends AJAX requests for dial-in numbers and conference ID. * @@ -74,85 +159,3 @@ export function updateDialInNumbers() { }); }; } - -/** - * Send invites for a list of items (may be a combination of users, rooms, phone - * numbers, and video rooms). - * - * @param {Array} invites - Items for which invites should be sent. - * @returns {Promise} Promise containing the list of invites that were not sent. - */ -export function sendInvitesForItems(invites: Array) { - return ( - dispatch: Dispatch<*>, - getState: Function): Promise> => { - let allInvitePromises = []; - let invitesLeftToSend = [ ...invites ]; - const state = getState(); - const { conference } = state['features/base/conference']; - const { inviteServiceUrl } = state['features/base/config']; - const inviteUrl = getInviteURL(state); - const jwt = state['features/base/jwt'].jwt; - - // First create all promises for dialing out. - if (conference) { - const phoneNumbers - = invitesLeftToSend.filter(({ type }) => type === 'phone'); - - // For each number, dial out. On success, remove the number from - // {@link invitesLeftToSend}. - const phoneInvitePromises = phoneNumbers.map(item => { - const numberToInvite = getDigitsOnly(item.number); - - return conference.dial(numberToInvite) - .then(() => { - invitesLeftToSend - = invitesLeftToSend.filter(invite => - invite !== item); - }) - .catch(error => logger.error( - 'Error inviting phone number:', error)); - }); - - allInvitePromises = allInvitePromises.concat(phoneInvitePromises); - } - - const usersAndRooms = invitesLeftToSend.filter(item => - item.type === 'user' || item.type === 'room'); - - if (usersAndRooms.length) { - // Send a request to invite all the rooms and users. On success, - // filter all rooms and users from {@link invitesLeftToSend}. - const peopleInvitePromise = invitePeopleAndChatRooms( - inviteServiceUrl, - inviteUrl, - jwt, - usersAndRooms) - .then(() => { - invitesLeftToSend = invitesLeftToSend.filter(item => - item.type !== 'user' && item.type !== 'room'); - }) - .catch(error => logger.error( - 'Error inviting people:', error)); - - allInvitePromises.push(peopleInvitePromise); - } - - // Sipgw calls are fire and forget. Invite them to the conference - // then immediately remove them from {@link invitesLeftToSend}. - const vrooms = invitesLeftToSend.filter(item => - item.type === 'videosipgw'); - - conference - && vrooms.length > 0 - && dispatch(inviteVideoRooms(conference, vrooms)); - - invitesLeftToSend = invitesLeftToSend.filter(item => - item.type !== 'videosipgw'); - - return ( - Promise.all(allInvitePromises) - .then(() => invitesLeftToSend) - ); - }; -} diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js index 6913c8ec4..c63c18f2a 100644 --- a/react/features/invite/components/AddPeopleDialog.web.js +++ b/react/features/invite/components/AddPeopleDialog.web.js @@ -11,11 +11,8 @@ import { Dialog, hideDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { MultiSelectAutocomplete } from '../../base/react'; -import { - getInviteResultsForQuery, - getInviteTypeCounts -} from '../functions'; -import { sendInvitesForItems } from '../actions'; +import { invite } from '../actions'; +import { getInviteResultsForQuery, getInviteTypeCounts } from '../functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -68,7 +65,7 @@ class AddPeopleDialog extends Component<*, *> { dialOutEnabled: PropTypes.bool, /** - * The redux dispatch method. + * The redux {@code dispatch} function. */ dispatch: PropTypes.func, @@ -282,8 +279,8 @@ class AddPeopleDialog extends Component<*, *> { */ _onSubmit() { const { inviteItems } = this.state; - const items = inviteItems.map(item => item.item); - const inviteTypeCounts = getInviteTypeCounts(items); + const invitees = inviteItems.map(({ item }) => item); + const inviteTypeCounts = getInviteTypeCounts(invitees); sendAnalytics(createInviteDialogEvent( 'clicked', 'inviteButton', { @@ -301,7 +298,7 @@ class AddPeopleDialog extends Component<*, *> { const { dispatch } = this.props; - dispatch(sendInvitesForItems(items)) + dispatch(invite(invitees)) .then(invitesLeftToSend => { // If any invites are left that means something failed to send // so treat it as an error. @@ -322,15 +319,12 @@ class AddPeopleDialog extends Component<*, *> { addToCallError: true }); - const unsentInviteIDs = invitesLeftToSend.map(invite => - invite.id || invite.number - ); - - const itemsToSelect = inviteItems.filter(invite => - unsentInviteIDs.includes( - invite.item.id || invite.item.number - ) - ); + const unsentInviteIDs + = invitesLeftToSend.map(invitee => + invitee.id || invitee.number); + const itemsToSelect + = inviteItems.filter(({ item }) => + unsentInviteIDs.includes(item.id || item.number)); if (this._multiselect) { this._multiselect.setSelectedItems(itemsToSelect); @@ -431,7 +425,6 @@ class AddPeopleDialog extends Component<*, *> { _peopleSearchQueryTypes, _peopleSearchUrl } = this.props; - const options = { dialOutAuthUrl: _dialOutAuthUrl, addPeopleEnabled, diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index 9afc1e63c..bf690e926 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -76,9 +76,8 @@ export function getDialInNumbers(url: string): Promise<*> { /** * Removes all non-numeric characters from a string. * - * @param {string} text - The string from which to remove all characters - * except numbers. - * @private + * @param {string} text - The string from which to remove all characters except + * numbers. * @returns {string} A string with only numbers. */ export function getDigitsOnly(text: string = ''): string { @@ -126,8 +125,8 @@ export type GetInviteResultsOptions = { * Combines directory search with phone number validation to produce a single * set of invite search results. * - * @param {string} query - Text to search. - * @param {GetInviteResultsOptions} options - Options to use when searching. + * @param {string} query - Text to search. + * @param {GetInviteResultsOptions} options - Options to use when searching. * @returns {Promise<*>} */ export function getInviteResultsForQuery( @@ -166,19 +165,18 @@ export function getInviteResultsForQuery( let numberToVerify = text; // When the number to verify does not start with a +, we assume no - // proper country code has been entered. In such a case, prepend 1 - // for the country code. The service currently takes care of - // prepending the +. + // proper country code has been entered. In such a case, prepend 1 for + // the country code. The service currently takes care of prepending the + // +. if (!hasCountryCode && !text.startsWith('1')) { numberToVerify = `1${numberToVerify}`; } - // The validation service works properly when the query is digits - // only so ensure only digits get sent. + // The validation service works properly when the query is digits only + // so ensure only digits get sent. numberToVerify = getDigitsOnly(numberToVerify); - phoneNumberPromise - = checkDialNumber(numberToVerify, dialOutAuthUrl); + phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl); } else { phoneNumberPromise = Promise.resolve({}); } @@ -190,18 +188,16 @@ export function getInviteResultsForQuery( ]; /** - * This check for phone results is for the day the call to - * searching people might return phone results as well. When - * that day comes this check will make it so the server checks - * are honored and the local appending of the number is not - * done. The local appending of the phone number can then be - * cleaned up when convenient. + * This check for phone results is for the day the call to searching + * people might return phone results as well. When that day comes + * this check will make it so the server checks are honored and the + * local appending of the number is not done. The local appending of + * the phone number can then be cleaned up when convenient. */ - const hasPhoneResult = peopleResults.find( - result => result.type === 'phone'); + const hasPhoneResult + = peopleResults.find(result => result.type === 'phone'); - if (!hasPhoneResult - && typeof phoneResults.allow === 'boolean') { + if (!hasPhoneResult && typeof phoneResults.allow === 'boolean') { results.push({ allowed: phoneResults.allow, country: phoneResults.country, @@ -216,6 +212,28 @@ export function getInviteResultsForQuery( }); } +/** + * Helper for determining how many of each type of user is being invited. Used + * for logging and sending analytics related to invites. + * + * @param {Array} inviteItems - An array with the invite items, as created in + * {@link _parseQueryResults}. + * @returns {Object} An object with keys as user types and values as the number + * of invites for that type. + */ +export function getInviteTypeCounts(inviteItems: Array = []) { + const inviteTypeCounts = {}; + + inviteItems.forEach(({ type }) => { + if (!inviteTypeCounts[type]) { + inviteTypeCounts[type] = 0; + } + inviteTypeCounts[type]++; + }); + + return inviteTypeCounts; +} + /** * Sends a post request to an invite service. * @@ -223,8 +241,8 @@ export function getInviteResultsForQuery( * invitation. * @param {string} inviteUrl - The url to the conference. * @param {string} jwt - The jwt token to pass to the search service. - * @param {Immutable.List} inviteItems - The list of the "user" or "room" - * type items to invite. + * @param {Immutable.List} inviteItems - The list of the "user" or "room" type + * items to invite. * @returns {Promise} - The promise created by the request. */ export function invitePeopleAndChatRooms( // eslint-disable-line max-params @@ -282,24 +300,37 @@ export function isAddPeopleEnabled(state: Object): boolean { * @returns {boolean} Indication of whether dial out is currently enabled. */ export function isDialOutEnabled(state: Object): boolean { + const participant = getLocalParticipant(state); const { conference } = state['features/base/conference']; const { isGuest } = state['features/base/jwt']; const { enableUserRolesBasedOnToken } = state['features/base/config']; - const participant = getLocalParticipant(state); + let dialOutEnabled + = participant && participant.role === PARTICIPANT_ROLE.MODERATOR + && conference && conference.isSIPCallingSupported() + && (!enableUserRolesBasedOnToken || !isGuest); - return participant && participant.role === PARTICIPANT_ROLE.MODERATOR - && conference && conference.isSIPCallingSupported() - && (!enableUserRolesBasedOnToken || !isGuest); + if (dialOutEnabled) { + // XXX The mobile/react-native app is capable of disabling of dial-out. + // Anyway, the Web/React app does not have that capability so default + // appropriately. + const { app } = state['features/app']; + + dialOutEnabled = app && app.props.dialoOutEnabled; + + return ( + (typeof dialOutEnabled === 'undefined') || Boolean(dialOutEnabled)); + } + + return false; } /** * Checks whether a string looks like it could be for a phone number. * - * @param {string} text - The text to check whether or not it could be a - * phone number. - * @private - * @returns {boolean} True if the string looks like it could be a phone + * @param {string} text - The text to check whether or not it could be a phone * number. + * @private + * @returns {boolean} True if the string looks like it could be a phone number. */ function isMaybeAPhoneNumber(text: string): boolean { if (!isPhoneNumberRegex().test(text)) { @@ -365,28 +396,3 @@ export function searchDirectory( // eslint-disable-line max-params return Promise.reject(error); }); } - -/** - * Helper for determining how many of each type of user is being invited. - * Used for logging and sending analytics related to invites. - * - * @param {Array} inviteItems - An array with the invite items, as created - * in {@link _parseQueryResults}. - * @private - * @returns {Object} An object with keys as user types and values as the - * number of invites for that type. - */ -export function getInviteTypeCounts(inviteItems: Array = []) { - const inviteTypeCounts = {}; - - inviteItems.forEach(({ type }) => { - - if (!inviteTypeCounts[type]) { - inviteTypeCounts[type] = 0; - } - - inviteTypeCounts[type]++; - }); - - return inviteTypeCounts; -} diff --git a/react/features/invite/middleware.native.js b/react/features/invite/middleware.native.js index 5de8bc7b6..0e7cc851e 100644 --- a/react/features/invite/middleware.native.js +++ b/react/features/invite/middleware.native.js @@ -6,6 +6,7 @@ import { NativeEventEmitter, NativeModules } from 'react-native'; import { MiddlewareRegistry } from '../base/redux'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { invite } from './actions'; import { BEGIN_ADD_PEOPLE, _SET_EMITTER_SUBSCRIPTIONS @@ -15,7 +16,6 @@ import { isAddPeopleEnabled, isDialOutEnabled } from './functions'; -import { sendInvitesForItems } from './actions'; import './middleware.any'; /** @@ -133,8 +133,7 @@ function _beginAddPeople({ getState }, next, action) { * @param {Object} event - The details of the event. * @returns {void} */ -function _onInvite( - { addPeopleControllerScope, externalAPIScope, invitees }) { +function _onInvite({ addPeopleControllerScope, externalAPIScope, invitees }) { const { dispatch, getState } = this; // eslint-disable-line no-invalid-this // If there are multiple JitsiMeetView instances alive, they will all get @@ -145,7 +144,7 @@ function _onInvite( return; } - dispatch(sendInvitesForItems(invitees)) + dispatch(invite(invitees)) .then(failedInvitees => Invite.inviteSettled( externalAPIScope,