You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AddPeopleDialog.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. // @flow
  2. import Avatar from '@atlaskit/avatar';
  3. import InlineMessage from '@atlaskit/inline-message';
  4. import React from 'react';
  5. import { connect } from 'react-redux';
  6. import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
  7. import { Dialog, hideDialog } from '../../../../base/dialog';
  8. import { translate, translateToHTML } from '../../../../base/i18n';
  9. import { getLocalParticipant } from '../../../../base/participants';
  10. import { MultiSelectAutocomplete } from '../../../../base/react';
  11. import AbstractAddPeopleDialog, {
  12. type Props as AbstractProps,
  13. type State,
  14. _mapStateToProps as _abstractMapStateToProps
  15. } from '../AbstractAddPeopleDialog';
  16. declare var interfaceConfig: Object;
  17. /**
  18. * The type of the React {@code Component} props of {@link AddPeopleDialog}.
  19. */
  20. type Props = AbstractProps & {
  21. /**
  22. * The {@link JitsiMeetConference} which will be used to invite "room"
  23. * participants through the SIP Jibri (Video SIP gateway).
  24. */
  25. _conference: Object,
  26. /**
  27. * Whether to show a footer text after the search results as a last element.
  28. */
  29. _footerTextEnabled: boolean,
  30. /**
  31. * The redux {@code dispatch} function.
  32. */
  33. dispatch: Dispatch<*>,
  34. /**
  35. * Invoked to obtain translated strings.
  36. */
  37. t: Function,
  38. };
  39. /**
  40. * The dialog that allows to invite people to the call.
  41. */
  42. class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
  43. _multiselect = null;
  44. _resourceClient: Object;
  45. state = {
  46. addToCallError: false,
  47. addToCallInProgress: false,
  48. inviteItems: []
  49. };
  50. /**
  51. * Initializes a new {@code AddPeopleDialog} instance.
  52. *
  53. * @param {Object} props - The read-only properties with which the new
  54. * instance is to be initialized.
  55. */
  56. constructor(props: Props) {
  57. super(props);
  58. // Bind event handlers so they are only bound once per instance.
  59. this._onItemSelected = this._onItemSelected.bind(this);
  60. this._onSelectionChange = this._onSelectionChange.bind(this);
  61. this._onSubmit = this._onSubmit.bind(this);
  62. this._parseQueryResults = this._parseQueryResults.bind(this);
  63. this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
  64. this._resourceClient = {
  65. makeQuery: this._query,
  66. parseResults: this._parseQueryResults
  67. };
  68. }
  69. /**
  70. * Sends an analytics event to record the dialog has been shown.
  71. *
  72. * @inheritdoc
  73. * @returns {void}
  74. */
  75. componentDidMount() {
  76. sendAnalytics(createInviteDialogEvent(
  77. 'invite.dialog.opened', 'dialog'));
  78. }
  79. /**
  80. * React Component method that executes once component is updated.
  81. *
  82. * @param {Object} prevProps - The state object before the update.
  83. * @param {Object} prevState - The state object before the update.
  84. * @returns {void}
  85. */
  86. componentDidUpdate(prevProps, prevState) {
  87. /**
  88. * Clears selected items from the multi select component on successful
  89. * invite.
  90. */
  91. if (prevState.addToCallError
  92. && !this.state.addToCallInProgress
  93. && !this.state.addToCallError
  94. && this._multiselect) {
  95. this._multiselect.setSelectedItems([]);
  96. }
  97. }
  98. /**
  99. * Sends an analytics event to record the dialog has been closed.
  100. *
  101. * @inheritdoc
  102. * @returns {void}
  103. */
  104. componentWillUnmount() {
  105. sendAnalytics(createInviteDialogEvent(
  106. 'invite.dialog.closed', 'dialog'));
  107. }
  108. /**
  109. * Renders the content of this component.
  110. *
  111. * @returns {ReactElement}
  112. */
  113. render() {
  114. const {
  115. _addPeopleEnabled,
  116. _dialOutEnabled,
  117. _footerTextEnabled,
  118. t
  119. } = this.props;
  120. let isMultiSelectDisabled = this.state.addToCallInProgress || false;
  121. let placeholder;
  122. let loadingMessage;
  123. let noMatches;
  124. let footerText;
  125. if (_addPeopleEnabled && _dialOutEnabled) {
  126. loadingMessage = 'addPeople.loading';
  127. noMatches = 'addPeople.noResults';
  128. placeholder = 'addPeople.searchPeopleAndNumbers';
  129. } else if (_addPeopleEnabled) {
  130. loadingMessage = 'addPeople.loadingPeople';
  131. noMatches = 'addPeople.noResults';
  132. placeholder = 'addPeople.searchPeople';
  133. } else if (_dialOutEnabled) {
  134. loadingMessage = 'addPeople.loadingNumber';
  135. noMatches = 'addPeople.noValidNumbers';
  136. placeholder = 'addPeople.searchNumbers';
  137. } else {
  138. isMultiSelectDisabled = true;
  139. noMatches = 'addPeople.noResults';
  140. placeholder = 'addPeople.disabled';
  141. }
  142. if (_footerTextEnabled) {
  143. footerText = {
  144. content: <div className = 'footer-text-wrap'>
  145. <div>
  146. <span className = 'footer-telephone-icon'>
  147. <i className = 'icon-telephone' />
  148. </span>
  149. </div>
  150. { translateToHTML(t, 'addPeople.footerText') }
  151. </div>
  152. };
  153. }
  154. return (
  155. <Dialog
  156. okDisabled = { this._isAddDisabled() }
  157. okTitleKey = 'addPeople.add'
  158. onSubmit = { this._onSubmit }
  159. titleKey = 'addPeople.title'
  160. width = 'medium'>
  161. <div className = 'add-people-form-wrap'>
  162. { this._renderErrorMessage() }
  163. <MultiSelectAutocomplete
  164. footer = { footerText }
  165. isDisabled = { isMultiSelectDisabled }
  166. loadingMessage = { t(loadingMessage) }
  167. noMatchesFound = { t(noMatches) }
  168. onItemSelected = { this._onItemSelected }
  169. onSelectionChange = { this._onSelectionChange }
  170. placeholder = { t(placeholder) }
  171. ref = { this._setMultiSelectElement }
  172. resourceClient = { this._resourceClient }
  173. shouldFitContainer = { true }
  174. shouldFocus = { true } />
  175. </div>
  176. </Dialog>
  177. );
  178. }
  179. _invite: Array<Object> => Promise<*>
  180. _isAddDisabled: () => boolean;
  181. _onItemSelected: (Object) => Object;
  182. /**
  183. * Callback invoked when a selection has been made but before it has been
  184. * set as selected.
  185. *
  186. * @param {Object} item - The item that has just been selected.
  187. * @private
  188. * @returns {Object} The item to display as selected in the input.
  189. */
  190. _onItemSelected(item) {
  191. if (item.item.type === 'phone') {
  192. item.content = item.item.number;
  193. }
  194. return item;
  195. }
  196. _onSelectionChange: (Map<*, *>) => void;
  197. /**
  198. * Handles a selection change.
  199. *
  200. * @param {Map} selectedItems - The list of selected items.
  201. * @private
  202. * @returns {void}
  203. */
  204. _onSelectionChange(selectedItems) {
  205. this.setState({
  206. inviteItems: selectedItems
  207. });
  208. }
  209. _onSubmit: () => void;
  210. /**
  211. * Submits the selection for inviting.
  212. *
  213. * @private
  214. * @returns {void}
  215. */
  216. _onSubmit() {
  217. const { inviteItems } = this.state;
  218. const invitees = inviteItems.map(({ item }) => item);
  219. this._invite(invitees)
  220. .then(invitesLeftToSend => {
  221. if (invitesLeftToSend.length) {
  222. const unsentInviteIDs
  223. = invitesLeftToSend.map(invitee =>
  224. invitee.id || invitee.number);
  225. const itemsToSelect
  226. = inviteItems.filter(({ item }) =>
  227. unsentInviteIDs.includes(item.id || item.number));
  228. if (this._multiselect) {
  229. this._multiselect.setSelectedItems(itemsToSelect);
  230. }
  231. } else {
  232. this.props.dispatch(hideDialog());
  233. }
  234. });
  235. }
  236. _parseQueryResults: (Array<Object>, string) => Array<Object>;
  237. /**
  238. * Processes results from requesting available numbers and people by munging
  239. * each result into a format {@code MultiSelectAutocomplete} can use for
  240. * display.
  241. *
  242. * @param {Array} response - The response object from the server for the
  243. * query.
  244. * @private
  245. * @returns {Object[]} Configuration objects for items to display in the
  246. * search autocomplete.
  247. */
  248. _parseQueryResults(response = []) {
  249. const { t } = this.props;
  250. const users = response.filter(item => item.type !== 'phone');
  251. const userDisplayItems = users.map(user => {
  252. return {
  253. content: user.name,
  254. elemBefore: <Avatar
  255. size = 'small'
  256. src = { user.avatar } />,
  257. item: user,
  258. tag: {
  259. elemBefore: <Avatar
  260. size = 'xsmall'
  261. src = { user.avatar } />
  262. },
  263. value: user.id
  264. };
  265. });
  266. const numbers = response.filter(item => item.type === 'phone');
  267. const telephoneIcon = this._renderTelephoneIcon();
  268. const numberDisplayItems = numbers.map(number => {
  269. const numberNotAllowedMessage
  270. = number.allowed ? '' : t('addPeople.countryNotSupported');
  271. const countryCodeReminder = number.showCountryCodeReminder
  272. ? t('addPeople.countryReminder') : '';
  273. const description
  274. = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
  275. return {
  276. filterValues: [
  277. number.originalEntry,
  278. number.number
  279. ],
  280. content: t('addPeople.telephone', { number: number.number }),
  281. description,
  282. isDisabled: !number.allowed,
  283. elemBefore: telephoneIcon,
  284. item: number,
  285. tag: {
  286. elemBefore: telephoneIcon
  287. },
  288. value: number.number
  289. };
  290. });
  291. return [
  292. ...userDisplayItems,
  293. ...numberDisplayItems
  294. ];
  295. }
  296. _query: (string) => Promise<Array<Object>>;
  297. /**
  298. * Renders the error message if the add doesn't succeed.
  299. *
  300. * @private
  301. * @returns {ReactElement|null}
  302. */
  303. _renderErrorMessage() {
  304. if (!this.state.addToCallError) {
  305. return null;
  306. }
  307. const { t } = this.props;
  308. const supportString = t('inlineDialogFailure.supportMsg');
  309. const supportLink = interfaceConfig.SUPPORT_URL;
  310. const supportLinkContent
  311. = (
  312. <span>
  313. <span>
  314. { supportString.padEnd(supportString.length + 1) }
  315. </span>
  316. <span>
  317. <a
  318. href = { supportLink }
  319. rel = 'noopener noreferrer'
  320. target = '_blank'>
  321. { t('inlineDialogFailure.support') }
  322. </a>
  323. </span>
  324. <span>.</span>
  325. </span>
  326. );
  327. return (
  328. <div className = 'modal-dialog-form-error'>
  329. <InlineMessage
  330. title = { t('addPeople.failedToAdd') }
  331. type = 'error'>
  332. { supportLinkContent }
  333. </InlineMessage>
  334. </div>
  335. );
  336. }
  337. /**
  338. * Renders a telephone icon.
  339. *
  340. * @private
  341. * @returns {ReactElement}
  342. */
  343. _renderTelephoneIcon() {
  344. return (
  345. <span className = 'add-telephone-icon'>
  346. <i className = 'icon-telephone' />
  347. </span>
  348. );
  349. }
  350. _setMultiSelectElement: (React$ElementRef<*> | null) => mixed;
  351. /**
  352. * Sets the instance variable for the multi select component
  353. * element so it can be accessed directly.
  354. *
  355. * @param {Object} element - The DOM element for the component's dialog.
  356. * @private
  357. * @returns {void}
  358. */
  359. _setMultiSelectElement(element) {
  360. this._multiselect = element;
  361. }
  362. }
  363. /**
  364. * Maps (parts of) the Redux state to the associated
  365. * {@code AddPeopleDialog}'s props.
  366. *
  367. * @param {Object} state - The Redux state.
  368. * @private
  369. * @returns {{
  370. * _dialOutAuthUrl: string,
  371. * _jwt: string,
  372. * _peopleSearchQueryTypes: Array<string>,
  373. * _peopleSearchUrl: string
  374. * }}
  375. */
  376. function _mapStateToProps(state) {
  377. const {
  378. enableFeaturesBasedOnToken
  379. } = state['features/base/config'];
  380. let footerTextEnabled = false;
  381. if (enableFeaturesBasedOnToken) {
  382. const { features = {} } = getLocalParticipant(state);
  383. if (String(features['outbound-call']) !== 'true') {
  384. footerTextEnabled = true;
  385. }
  386. }
  387. return {
  388. ..._abstractMapStateToProps(state),
  389. _footerTextEnabled: footerTextEnabled
  390. };
  391. }
  392. export default translate(connect(_mapStateToProps)(AddPeopleDialog));