// @flow
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate, translateToHTML } from '../../base/i18n';
import { getLocalParticipant } from '../../base/participants';
import { MultiSelectAutocomplete } from '../../base/react';
import { invite } from '../actions';
import { getInviteResultsForQuery, getInviteTypeCounts } from '../functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
declare var interfaceConfig: Object;
/**
* The dialog that allows to invite people to the call.
*/
class AddPeopleDialog extends Component<*, *> {
/**
* {@code AddPeopleDialog}'s property types.
*
* @static
*/
static propTypes = {
/**
* The {@link JitsiMeetConference} which will be used to invite "room"
* participants through the SIP Jibri (Video SIP gateway).
*/
_conference: PropTypes.object,
/**
* The URL for validating if a phone number can be called.
*/
_dialOutAuthUrl: PropTypes.string,
/**
* Whether to show a footer text after the search results
* as a last element.
*/
_footerTextEnabled: PropTypes.bool,
/**
* The JWT token.
*/
_jwt: PropTypes.string,
/**
* The query types used when searching people.
*/
_peopleSearchQueryTypes: PropTypes.arrayOf(PropTypes.string),
/**
* The URL pointing to the service allowing for people search.
*/
_peopleSearchUrl: PropTypes.string,
/**
* Whether or not to show Add People functionality.
*/
addPeopleEnabled: PropTypes.bool,
/**
* Whether or not to show Dial Out functionality.
*/
dialOutEnabled: PropTypes.bool,
/**
* The redux {@code dispatch} function.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
_multiselect = null;
_resourceClient: Object;
state = {
/**
* Indicating that an error occurred when adding people to the call.
*/
addToCallError: false,
/**
* Indicating that we're currently adding the new people to the
* call.
*/
addToCallInProgress: false,
/**
* The list of invite items.
*/
inviteItems: []
};
/**
* Initializes a new {@code AddPeopleDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// 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
};
}
/**
* Sends an analytics event to record the dialog has been shown.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.opened', 'dialog'));
}
/**
* React Component method that executes once component is updated.
*
* @param {Object} prevState - The state object before the update.
* @returns {void}
*/
componentDidUpdate(prevState) {
/**
* Clears selected items from the multi select component on successful
* invite.
*/
if (prevState.addToCallError
&& !this.state.addToCallInProgress
&& !this.state.addToCallError
&& this._multiselect) {
this._multiselect.setSelectedItems([]);
}
}
/**
* Sends an analytics event to record the dialog has been closed.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.closed', 'dialog'));
}
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { _footerTextEnabled,
addPeopleEnabled,
dialOutEnabled,
t } = this.props;
let isMultiSelectDisabled = this.state.addToCallInProgress || false;
let placeholder;
let loadingMessage;
let noMatches;
let footerText;
if (addPeopleEnabled && dialOutEnabled) {
loadingMessage = 'addPeople.loading';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeopleAndNumbers';
} else if (addPeopleEnabled) {
loadingMessage = 'addPeople.loadingPeople';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeople';
} else if (dialOutEnabled) {
loadingMessage = 'addPeople.loadingNumber';
noMatches = 'addPeople.noValidNumbers';
placeholder = 'addPeople.searchNumbers';
} else {
isMultiSelectDisabled = true;
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.disabled';
}
if (_footerTextEnabled) {
footerText = {
content:
{ translateToHTML(t, 'addPeople.footerText') }
};
}
return (
);
}
_isAddDisabled: () => boolean;
/**
* Indicates if the Add button should be disabled.
*
* @private
* @returns {boolean} - True to indicate that the Add button should
* be disabled, false otherwise.
*/
_isAddDisabled() {
return !this.state.inviteItems.length
|| this.state.addToCallInProgress;
}
_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;
/**
* Handles a selection change.
*
* @param {Map} selectedItems - The list of selected items.
* @private
* @returns {void}
*/
_onSelectionChange(selectedItems) {
this.setState({
inviteItems: selectedItems
});
}
_onSubmit: () => void;
/**
* 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() {
const { inviteItems } = this.state;
const invitees = inviteItems.map(({ item }) => item);
const inviteTypeCounts = getInviteTypeCounts(invitees);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return;
}
this.setState({
addToCallInProgress: true
});
const { dispatch } = this.props;
dispatch(invite(invitees))
.then(invitesLeftToSend => {
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallInProgress: false,
addToCallError: true
});
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);
}
return;
}
this.setState({
addToCallInProgress: false
});
dispatch(hideDialog());
});
}
_parseQueryResults: (Array