554 lines
16 KiB
JavaScript
554 lines
16 KiB
JavaScript
// @flow
|
|
|
|
import InlineMessage from '@atlaskit/inline-message';
|
|
import React from 'react';
|
|
import type { Dispatch } from 'redux';
|
|
|
|
import { Avatar } from '../../../../base/avatar';
|
|
import { translate } from '../../../../base/i18n';
|
|
import { Icon, IconPhoneRinging } from '../../../../base/icons';
|
|
import { MultiSelectAutocomplete } from '../../../../base/react';
|
|
import { connect } from '../../../../base/redux';
|
|
import { isVpaasMeeting } from '../../../../jaas/functions';
|
|
import { hideAddPeopleDialog } from '../../../actions';
|
|
import { INVITE_TYPES } from '../../../constants';
|
|
import AbstractAddPeopleDialog, {
|
|
type Props as AbstractProps,
|
|
type State,
|
|
_mapStateToProps as _abstractMapStateToProps
|
|
} from '../AbstractAddPeopleDialog';
|
|
|
|
declare var interfaceConfig: Object;
|
|
|
|
type Props = AbstractProps & {
|
|
|
|
/**
|
|
* The {@link JitsiMeetConference} which will be used to invite "room" participants.
|
|
*/
|
|
_conference: Object,
|
|
|
|
/**
|
|
* Whether the meeting belongs to JaaS user.
|
|
*/
|
|
_isVpaas: boolean,
|
|
|
|
/**
|
|
* The redux {@code dispatch} function.
|
|
*/
|
|
dispatch: Dispatch<any>,
|
|
|
|
/**
|
|
* Invoked to obtain translated strings.
|
|
*/
|
|
t: Function,
|
|
};
|
|
|
|
/**
|
|
* Form that enables inviting others to the call.
|
|
*/
|
|
class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
|
_multiselect = null;
|
|
|
|
_resourceClient: Object;
|
|
|
|
_translations: Object;
|
|
|
|
state = {
|
|
addToCallError: false,
|
|
addToCallInProgress: false,
|
|
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: Props) {
|
|
super(props);
|
|
|
|
// Bind event handlers so they are only bound once per instance.
|
|
this._onClearItems = this._onClearItems.bind(this);
|
|
this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
|
|
this._onItemSelected = this._onItemSelected.bind(this);
|
|
this._onSelectionChange = this._onSelectionChange.bind(this);
|
|
this._onSubmit = this._onSubmit.bind(this);
|
|
this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
|
|
this._parseQueryResults = this._parseQueryResults.bind(this);
|
|
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
|
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
|
|
this._resourceClient = {
|
|
makeQuery: this._query,
|
|
parseResults: this._parseQueryResults
|
|
};
|
|
|
|
|
|
const { t } = props;
|
|
|
|
this._translations = {
|
|
_dialOutEnabled: t('addPeople.phoneNumbers'),
|
|
_addPeopleEnabled: t('addPeople.contacts'),
|
|
_sipInviteEnabled: t('addPeople.sipAddresses')
|
|
};
|
|
|
|
}
|
|
|
|
/**
|
|
* React Component method that executes once component is updated.
|
|
*
|
|
* @param {Object} prevProps - The state object before the update.
|
|
* @param {Object} prevState - The state object before the update.
|
|
* @returns {void}
|
|
*/
|
|
componentDidUpdate(prevProps, 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([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the content of this component.
|
|
*
|
|
* @returns {ReactElement}
|
|
*/
|
|
render() {
|
|
const {
|
|
_addPeopleEnabled,
|
|
_dialOutEnabled,
|
|
_isVpaas,
|
|
_sipInviteEnabled,
|
|
t
|
|
} = this.props;
|
|
let isMultiSelectDisabled = this.state.addToCallInProgress;
|
|
const loadingMessage = 'addPeople.searching';
|
|
const noMatches = 'addPeople.noResults';
|
|
|
|
const features = {
|
|
_dialOutEnabled,
|
|
_addPeopleEnabled,
|
|
_sipInviteEnabled
|
|
};
|
|
|
|
const computedPlaceholder = Object.keys(features)
|
|
.filter(v => Boolean(features[v]))
|
|
.map(v => this._translations[v])
|
|
.join(', ');
|
|
|
|
const placeholder = computedPlaceholder ? `${t('dialog.add')} ${computedPlaceholder}` : t('addPeople.disabled');
|
|
|
|
if (!computedPlaceholder) {
|
|
isMultiSelectDisabled = true;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className = 'add-people-form-wrap'
|
|
onKeyDown = { this._onKeyDown }>
|
|
{ this._renderErrorMessage() }
|
|
<MultiSelectAutocomplete
|
|
isDisabled = { isMultiSelectDisabled }
|
|
loadingMessage = { t(loadingMessage) }
|
|
noMatchesFound = { t(noMatches) }
|
|
onItemSelected = { this._onItemSelected }
|
|
onSelectionChange = { this._onSelectionChange }
|
|
placeholder = { placeholder }
|
|
ref = { this._setMultiSelectElement }
|
|
resourceClient = { this._resourceClient }
|
|
shouldFitContainer = { true }
|
|
shouldFocus = { true }
|
|
showSupportLink = { !_isVpaas } />
|
|
{ this._renderFormActions() }
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_invite: Array<Object> => Promise<*>;
|
|
|
|
_isAddDisabled: () => boolean;
|
|
|
|
_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 === INVITE_TYPES.PHONE) {
|
|
item.content = item.item.number;
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
_onSelectionChange: (Map<*, *>) => void;
|
|
|
|
/**
|
|
* Handles a selection change.
|
|
*
|
|
* @param {Array} selectedItems - The list of selected items.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onSelectionChange(selectedItems) {
|
|
this.setState({
|
|
inviteItems: selectedItems
|
|
});
|
|
}
|
|
|
|
_onSubmit: () => void;
|
|
|
|
/**
|
|
* Submits the selection for inviting.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onSubmit() {
|
|
const { inviteItems } = this.state;
|
|
const invitees = inviteItems.map(({ item }) => item);
|
|
|
|
this._invite(invitees)
|
|
.then(invitesLeftToSend => {
|
|
if (invitesLeftToSend.length) {
|
|
const unsentInviteIDs
|
|
= invitesLeftToSend.map(invitee =>
|
|
invitee.id || invitee.user_id || invitee.number);
|
|
const itemsToSelect
|
|
= inviteItems.filter(({ item }) =>
|
|
unsentInviteIDs.includes(item.id || item.user_id || item.number));
|
|
|
|
if (this._multiselect) {
|
|
this._multiselect.setSelectedItems(itemsToSelect);
|
|
}
|
|
} else {
|
|
this.props.dispatch(hideAddPeopleDialog());
|
|
}
|
|
});
|
|
}
|
|
|
|
_onSubmitKeyPress: (Object) => void;
|
|
|
|
/**
|
|
* KeyPress handler for accessibility.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onSubmitKeyPress(e) {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this._onSubmit();
|
|
}
|
|
}
|
|
|
|
_onKeyDown: (Object) => void;
|
|
|
|
/**
|
|
* Handles 'Enter' key in the form to trigger the invite.
|
|
*
|
|
* @param {Object} event - The key event.
|
|
* @returns {void}
|
|
*/
|
|
_onKeyDown(event) {
|
|
const { inviteItems } = this.state;
|
|
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
if (!this._isAddDisabled() && inviteItems.length) {
|
|
this._onSubmit();
|
|
}
|
|
}
|
|
}
|
|
|
|
_parseQueryResults: (?Array<Object>) => Array<Object>;
|
|
|
|
/**
|
|
* Returns the avatar component for a user.
|
|
*
|
|
* @param {Object} user - The user.
|
|
* @param {string} className - The CSS class for the avatar component.
|
|
* @private
|
|
* @returns {ReactElement}
|
|
*/
|
|
_getAvatar(user, className = 'avatar-small') {
|
|
return (
|
|
<Avatar
|
|
className = { className }
|
|
status = { user.status }
|
|
url = { user.avatar } />
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Processes results from requesting available numbers and people by munging
|
|
* each result into a format {@code MultiSelectAutocomplete} can use for
|
|
* display.
|
|
*
|
|
* @param {Array} response - The response object from the server for the
|
|
* query.
|
|
* @private
|
|
* @returns {Object[]} Configuration objects for items to display in the
|
|
* search autocomplete.
|
|
*/
|
|
_parseQueryResults(response = []) {
|
|
const { t, _dialOutEnabled } = this.props;
|
|
|
|
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
|
|
const users = response.filter(item => userTypes.includes(item.type));
|
|
const userDisplayItems = [];
|
|
|
|
for (const user of users) {
|
|
const { name, phone } = user;
|
|
const tagAvatar = this._getAvatar(user, 'avatar-xsmall');
|
|
const elemAvatar = this._getAvatar(user);
|
|
|
|
userDisplayItems.push({
|
|
content: name,
|
|
elemBefore: elemAvatar,
|
|
item: user,
|
|
tag: {
|
|
elemBefore: tagAvatar
|
|
},
|
|
value: user.id || user.user_id
|
|
});
|
|
|
|
if (phone && _dialOutEnabled) {
|
|
userDisplayItems.push({
|
|
filterValues: [ name, phone ],
|
|
content: `${phone} (${name})`,
|
|
elemBefore: elemAvatar,
|
|
item: {
|
|
type: INVITE_TYPES.PHONE,
|
|
number: phone
|
|
},
|
|
tag: {
|
|
elemBefore: tagAvatar
|
|
},
|
|
value: phone
|
|
});
|
|
}
|
|
}
|
|
|
|
const numbers = response.filter(item => item.type === INVITE_TYPES.PHONE);
|
|
const telephoneIcon = this._renderTelephoneIcon();
|
|
|
|
const numberDisplayItems = numbers.map(number => {
|
|
const numberNotAllowedMessage
|
|
= number.allowed ? '' : t('addPeople.countryNotSupported');
|
|
const countryCodeReminder = number.showCountryCodeReminder
|
|
? t('addPeople.countryReminder') : '';
|
|
const description
|
|
= `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
|
|
|
|
return {
|
|
filterValues: [
|
|
number.originalEntry,
|
|
number.number
|
|
],
|
|
content: t('addPeople.telephone', { number: number.number }),
|
|
description,
|
|
isDisabled: !number.allowed,
|
|
elemBefore: telephoneIcon,
|
|
item: number,
|
|
tag: {
|
|
elemBefore: telephoneIcon
|
|
},
|
|
value: number.number
|
|
};
|
|
});
|
|
|
|
|
|
const sipAddresses = response.filter(item => item.type === INVITE_TYPES.SIP);
|
|
|
|
const sipDisplayItems = sipAddresses.map(sip => {
|
|
return {
|
|
filterValues: [
|
|
sip.address
|
|
],
|
|
content: sip.address,
|
|
description: '',
|
|
item: sip,
|
|
value: sip.address
|
|
};
|
|
});
|
|
|
|
return [
|
|
...userDisplayItems,
|
|
...numberDisplayItems,
|
|
...sipDisplayItems
|
|
];
|
|
}
|
|
|
|
_query: (string) => Promise<Array<Object>>;
|
|
|
|
_onClearItems: () => void;
|
|
|
|
/**
|
|
* Clears the selected items from state and form.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onClearItems() {
|
|
if (this._multiselect) {
|
|
this._multiselect.setSelectedItems([]);
|
|
}
|
|
this.setState({ inviteItems: [] });
|
|
}
|
|
|
|
_onClearItemsKeyPress: () => void;
|
|
|
|
/**
|
|
* Clears the selected items from state and form.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onClearItemsKeyPress(e) {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this._onClearItems();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the add/cancel actions for the form.
|
|
*
|
|
* @returns {ReactElement|null}
|
|
*/
|
|
_renderFormActions() {
|
|
const { inviteItems } = this.state;
|
|
const { t } = this.props;
|
|
|
|
if (!inviteItems.length) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
|
|
<a
|
|
aria-label = { t('dialog.Cancel') }
|
|
className = 'invite-more-dialog invite-buttons-cancel'
|
|
onClick = { this._onClearItems }
|
|
onKeyPress = { this._onClearItemsKeyPress }
|
|
role = 'button'
|
|
tabIndex = { 0 }>
|
|
{t('dialog.Cancel')}
|
|
</a>
|
|
<a
|
|
aria-label = { t('addPeople.add') }
|
|
className = 'invite-more-dialog invite-buttons-add'
|
|
onClick = { this._onSubmit }
|
|
onKeyPress = { this._onSubmitKeyPress }
|
|
role = 'button'
|
|
tabIndex = { 0 }>
|
|
{t('addPeople.add')}
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the error message if the add doesn't succeed.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement|null}
|
|
*/
|
|
_renderErrorMessage() {
|
|
if (!this.state.addToCallError) {
|
|
return null;
|
|
}
|
|
|
|
const { t } = this.props;
|
|
const supportString = t('inlineDialogFailure.supportMsg');
|
|
const supportLink = interfaceConfig.SUPPORT_URL;
|
|
|
|
const supportLinkContent = supportLink ? (
|
|
<span>
|
|
<span>
|
|
{ supportString.padEnd(supportString.length + 1) }
|
|
</span>
|
|
<span>
|
|
<a
|
|
aria-label = { supportLink }
|
|
href = { supportLink }
|
|
rel = 'noopener noreferrer'
|
|
target = '_blank'>
|
|
{ t('inlineDialogFailure.support') }
|
|
</a>
|
|
</span>
|
|
<span>.</span>
|
|
</span>
|
|
) : null;
|
|
|
|
return (
|
|
<div className = 'modal-dialog-form-error'>
|
|
<InlineMessage
|
|
title = { t('addPeople.failedToAdd') }
|
|
type = 'error'>
|
|
{ supportLinkContent }
|
|
</InlineMessage>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders a telephone icon.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement}
|
|
*/
|
|
_renderTelephoneIcon() {
|
|
return (
|
|
<span className = 'add-telephone-icon'>
|
|
<Icon src = { IconPhoneRinging } />
|
|
</span>
|
|
);
|
|
}
|
|
|
|
_setMultiSelectElement: (React$ElementRef<*> | null) => void;
|
|
|
|
/**
|
|
* Sets the instance variable for the multi select component
|
|
* element so it can be accessed directly.
|
|
*
|
|
* @param {Object} element - The DOM element for the component's dialog.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_setMultiSelectElement(element) {
|
|
this._multiselect = element;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the Redux state to the associated
|
|
* {@code AddPeopleDialog}'s props.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @private
|
|
* @returns {Props}
|
|
*/
|
|
function _mapStateToProps(state) {
|
|
return {
|
|
..._abstractMapStateToProps(state),
|
|
_isVpaas: isVpaasMeeting(state)
|
|
};
|
|
}
|
|
|
|
export default translate(connect(_mapStateToProps)(InviteContactsForm));
|