{
* @param {Object} state - The Redux state.
* @private
* @returns {{
- * _hovered: boolean,
* _hideInviteButton: boolean,
+ * _hovered: boolean,
+ * _isAddToCallAvailable: boolean,
+ * _isDialOutAvailable: boolean,
* _remoteVideosVisible: boolean
* }}
*/
function _mapStateToProps(state) {
+ const { conference } = state['features/base/conference'];
+ const {
+ enableUserRolesBasedOnToken,
+ iAmRecorder
+ } = state['features/base/config'];
+ const { isGuest } = state['features/base/jwt'];
+ const { hovered } = state['features/filmstrip'];
+
+ const isAddToCallAvailable = !isGuest;
+ const isDialOutAvailable
+ = getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
+ && conference && conference.isSIPCallingSupported()
+ && (!enableUserRolesBasedOnToken || !isGuest);
+
return {
- _hovered: state['features/filmstrip'].hovered,
- _hideInviteButton: state['features/base/config'].iAmRecorder,
+ _hideInviteButton: iAmRecorder
+ || (!isAddToCallAvailable && !isDialOutAvailable),
+ _hovered: hovered,
+ _isAddToCallAvailable: isAddToCallAvailable,
+ _isDialOutAvailable: isDialOutAvailable,
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
};
}
diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js
index cb4af975a..1da468247 100644
--- a/react/features/invite/components/AddPeopleDialog.web.js
+++ b/react/features/invite/components/AddPeopleDialog.web.js
@@ -11,12 +11,21 @@ import { getInviteURL } from '../../base/connection';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { MultiSelectAutocomplete } from '../../base/react';
-
-import { invitePeopleAndChatRooms, searchDirectory } from '../functions';
import { inviteVideoRooms } from '../../videosipgw';
+import {
+ checkDialNumber,
+ invitePeopleAndChatRooms,
+ searchDirectory
+} from '../functions';
+
+const logger = require('jitsi-meet-logger').getLogger(__filename);
+
declare var interfaceConfig: Object;
+const isPhoneNumberRegex
+ = new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
+
/**
* The dialog that allows to invite people to the call.
*/
@@ -33,6 +42,11 @@ class AddPeopleDialog extends Component<*, *> {
*/
_conference: PropTypes.object,
+ /**
+ * The URL for validating if a phone number can be called.
+ */
+ _dialOutAuthUrl: PropTypes.string,
+
/**
* The URL pointing to the service allowing for people invite.
*/
@@ -58,6 +72,16 @@ class AddPeopleDialog extends Component<*, *> {
*/
_peopleSearchUrl: PropTypes.string,
+ /**
+ * Whether or not to show Add People functionality.
+ */
+ enableAddPeople: PropTypes.bool,
+
+ /**
+ * Whether or not to show Dial Out functionality.
+ */
+ enableDialOut: PropTypes.bool,
+
/**
* The function closing the dialog.
*/
@@ -76,33 +100,7 @@ class AddPeopleDialog extends Component<*, *> {
_multiselect = null;
- _resourceClient = {
- makeQuery: text => {
- const {
- _jwt,
- _peopleSearchQueryTypes,
- _peopleSearchUrl
- } = this.props; // eslint-disable-line no-invalid-this
-
- return (
- searchDirectory(
- _peopleSearchUrl,
- _jwt,
- text,
- _peopleSearchQueryTypes));
- },
-
- parseResults: response => response.map(user => {
- return {
- content: user.name,
- elemBefore:
,
- item: user,
- value: user.id
- };
- })
- };
+ _resourceClient: Object;
state = {
/**
@@ -116,6 +114,12 @@ class AddPeopleDialog extends Component<*, *> {
*/
addToCallInProgress: false,
+
+ // FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete}
+ // will default to having its internal implementation use a plain array
+ // if no {@link defaultValue} is passed in. As such is the case, this
+ // instance of Immutable.List gets overridden with an array on the first
+ // search.
/**
* The list of invite items.
*/
@@ -133,9 +137,17 @@ class AddPeopleDialog extends Component<*, *> {
// Bind event handlers so they are only bound once per instance.
this._isAddDisabled = this._isAddDisabled.bind(this);
+ this._onItemSelected = this._onItemSelected.bind(this);
this._onSelectionChange = this._onSelectionChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
+ this._parseQueryResults = this._parseQueryResults.bind(this);
+ this._query = this._query.bind(this);
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
+
+ this._resourceClient = {
+ makeQuery: this._query,
+ parseResults: this._parseQueryResults
+ };
}
/**
@@ -153,7 +165,7 @@ class AddPeopleDialog extends Component<*, *> {
&& !this.state.addToCallInProgress
&& !this.state.addToCallError
&& this._multiselect) {
- this._multiselect.clear();
+ this._multiselect.setSelectedItems([]);
}
}
@@ -163,18 +175,69 @@ class AddPeopleDialog extends Component<*, *> {
* @returns {ReactElement}
*/
render() {
+ const { enableAddPeople, enableDialOut, t } = this.props;
+ let isMultiSelectDisabled = this.state.addToCallInProgress || false;
+ let placeholder;
+ let loadingMessage;
+ let noMatches;
+
+ if (enableAddPeople && enableDialOut) {
+ loadingMessage = 'addPeople.loading';
+ noMatches = 'addPeople.noResults';
+ placeholder = 'addPeople.searchPeopleAndNumbers';
+ } else if (enableAddPeople) {
+ loadingMessage = 'addPeople.loadingPeople';
+ noMatches = 'addPeople.noResults';
+ placeholder = 'addPeople.searchPeople';
+ } else if (enableDialOut) {
+ loadingMessage = 'addPeople.loadingNumber';
+ noMatches = 'addPeople.noValidNumbers';
+ placeholder = 'addPeople.searchNumbers';
+ } else {
+ isMultiSelectDisabled = true;
+ noMatches = 'addPeople.noResults';
+ placeholder = 'addPeople.disabled';
+ }
+
return (
);
}
+ _getDigitsOnly: (string) => string;
+
+ /**
+ * Removes all non-numeric characters from a string.
+ *
+ * @param {string} text - The string from which to remove all characters
+ * except numbers.
+ * @private
+ * @returns {string} A string with only numbers.
+ */
+ _getDigitsOnly(text = '') {
+ return text.replace(/\D/g, '');
+ }
+
_isAddDisabled: () => boolean;
/**
@@ -189,6 +252,45 @@ class AddPeopleDialog extends Component<*, *> {
|| this.state.addToCallInProgress;
}
+ _isMaybeAPhoneNumber: (string) => boolean;
+
+ /**
+ * 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
+ * number.
+ */
+ _isMaybeAPhoneNumber(text) {
+ if (!isPhoneNumberRegex.test(text)) {
+ return false;
+ }
+
+ const digits = this._getDigitsOnly(text);
+
+ return Boolean(digits.length);
+ }
+
+ _onItemSelected: (Object) => Object;
+
+ /**
+ * Callback invoked when a selection has been made but before it has been
+ * set as selected.
+ *
+ * @param {Object} item - The item that has just been selected.
+ * @private
+ * @returns {Object} The item to display as selected in the input.
+ */
+ _onItemSelected(item) {
+ if (item.item.type === 'phone') {
+ item.content = item.item.number;
+ }
+
+ return item;
+ }
+
_onSelectionChange: (Map<*, *>) => void;
/**
@@ -199,55 +301,279 @@ class AddPeopleDialog extends Component<*, *> {
* @returns {void}
*/
_onSelectionChange(selectedItems) {
- const selectedIds = selectedItems.map(o => o.item);
-
this.setState({
- inviteItems: selectedIds
+ inviteItems: selectedItems
});
}
_onSubmit: () => void;
/**
- * Handles the submit button action.
+ * Invite people and numbers to the conference. The logic works by inviting
+ * numbers, people/rooms, and videosipgw in parallel. All invitees are
+ * stored in an array. As each invite succeeds, the invitee is removed
+ * from the array. After all invites finish, close the modal if there are
+ * no invites left to send. If any are left, that means an invite failed
+ * and an error state should display.
*
* @private
* @returns {void}
*/
_onSubmit() {
- if (!this._isAddDisabled()) {
- this.setState({
- addToCallInProgress: true
+ if (this._isAddDisabled()) {
+ return;
+ }
+
+ this.setState({
+ addToCallInProgress: true
+ });
+
+ let allInvitePromises = [];
+ let invitesLeftToSend = [
+ ...this.state.inviteItems
+ ];
+
+ // First create all promises for dialing out.
+ if (this.props.enableDialOut && this.props._conference) {
+ const phoneNumbers = invitesLeftToSend.filter(
+ ({ item }) => item.type === 'phone');
+
+ // For each number, dial out. On success, remove the number from
+ // {@link invitesLeftToSend}.
+ const phoneInvitePromises = phoneNumbers.map(number => {
+ const numberToInvite = this._getDigitsOnly(number.item.number);
+
+ return this.props._conference.dial(numberToInvite)
+ .then(() => {
+ invitesLeftToSend
+ = invitesLeftToSend.filter(invite =>
+ invite !== number);
+ })
+ .catch(error => logger.error(
+ 'Error inviting phone number:', error));
+
});
- const vrooms = this.state.inviteItems.filter(
- i => i.type === 'videosipgw');
+ allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
+ }
+
+ if (this.props.enableAddPeople) {
+ const usersAndRooms = invitesLeftToSend.filter(i =>
+ i.item.type === 'user' || i.item.type === 'room')
+ .map(i => i.item);
+
+ 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(
+ this.props._inviteServiceUrl,
+ this.props._inviteUrl,
+ this.props._jwt,
+ usersAndRooms)
+ .then(() => {
+ invitesLeftToSend = invitesLeftToSend.filter(i =>
+ i.item.type !== 'user' && i.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(i =>
+ i.item.type === 'videosipgw')
+ .map(i => i.item);
this.props._conference
&& vrooms.length > 0
- && this.props.inviteVideoRooms(this.props._conference, vrooms);
+ && this.props.inviteVideoRooms(
+ this.props._conference, vrooms);
- invitePeopleAndChatRooms(
- this.props._inviteServiceUrl,
- this.props._inviteUrl,
- this.props._jwt,
- this.state.inviteItems.filter(
- i => i.type === 'user' || i.type === 'room'))
- .then(
- /* onFulfilled */ () => {
- this.setState({
- addToCallInProgress: false
- });
+ invitesLeftToSend = invitesLeftToSend.filter(i =>
+ i.item.type !== 'videosipgw');
+ }
+
+ Promise.all(allInvitePromises)
+ .then(() => {
+ // If any invites are left that means something failed to send
+ // so treat it as an error.
+ if (invitesLeftToSend.length) {
+ logger.error(`${invitesLeftToSend.length} invites failed`);
- this.props.hideDialog();
- },
- /* onRejected */ () => {
this.setState({
addToCallInProgress: false,
addToCallError: true
});
+
+ if (this._multiselect) {
+ this._multiselect.setSelectedItems(invitesLeftToSend);
+ }
+
+ return;
+ }
+
+ this.setState({
+ addToCallInProgress: false
});
+
+ this.props.hideDialog();
+ });
+ }
+
+ _parseQueryResults: (Array