您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

AddPeopleDialog.web.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. // @flow
  2. import Avatar from '@atlaskit/avatar';
  3. import InlineMessage from '@atlaskit/inline-message';
  4. import PropTypes from 'prop-types';
  5. import React, { Component } from 'react';
  6. import { connect } from 'react-redux';
  7. import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
  8. import { Dialog, hideDialog } from '../../base/dialog';
  9. import { translate } from '../../base/i18n';
  10. import { MultiSelectAutocomplete } from '../../base/react';
  11. import {
  12. getInviteResultsForQuery,
  13. getInviteTypeCounts
  14. } from '../functions';
  15. import { sendInvitesForItems } from '../actions';
  16. const logger = require('jitsi-meet-logger').getLogger(__filename);
  17. declare var interfaceConfig: Object;
  18. /**
  19. * The dialog that allows to invite people to the call.
  20. */
  21. class AddPeopleDialog extends Component<*, *> {
  22. /**
  23. * {@code AddPeopleDialog}'s property types.
  24. *
  25. * @static
  26. */
  27. static propTypes = {
  28. /**
  29. * The {@link JitsiMeetConference} which will be used to invite "room"
  30. * participants through the SIP Jibri (Video SIP gateway).
  31. */
  32. _conference: PropTypes.object,
  33. /**
  34. * The URL for validating if a phone number can be called.
  35. */
  36. _dialOutAuthUrl: PropTypes.string,
  37. /**
  38. * The JWT token.
  39. */
  40. _jwt: PropTypes.string,
  41. /**
  42. * The query types used when searching people.
  43. */
  44. _peopleSearchQueryTypes: PropTypes.arrayOf(PropTypes.string),
  45. /**
  46. * The URL pointing to the service allowing for people search.
  47. */
  48. _peopleSearchUrl: PropTypes.string,
  49. /**
  50. * Whether or not to show Add People functionality.
  51. */
  52. addPeopleEnabled: PropTypes.bool,
  53. /**
  54. * Whether or not to show Dial Out functionality.
  55. */
  56. dialOutEnabled: PropTypes.bool,
  57. /**
  58. * The redux dispatch method.
  59. */
  60. dispatch: PropTypes.func,
  61. /**
  62. * Invoked to obtain translated strings.
  63. */
  64. t: PropTypes.func
  65. };
  66. _multiselect = null;
  67. _resourceClient: Object;
  68. state = {
  69. /**
  70. * Indicating that an error occurred when adding people to the call.
  71. */
  72. addToCallError: false,
  73. /**
  74. * Indicating that we're currently adding the new people to the
  75. * call.
  76. */
  77. addToCallInProgress: false,
  78. /**
  79. * The list of invite items.
  80. */
  81. inviteItems: []
  82. };
  83. /**
  84. * Initializes a new {@code AddPeopleDialog} instance.
  85. *
  86. * @param {Object} props - The read-only properties with which the new
  87. * instance is to be initialized.
  88. */
  89. constructor(props) {
  90. super(props);
  91. // Bind event handlers so they are only bound once per instance.
  92. this._isAddDisabled = this._isAddDisabled.bind(this);
  93. this._onItemSelected = this._onItemSelected.bind(this);
  94. this._onSelectionChange = this._onSelectionChange.bind(this);
  95. this._onSubmit = this._onSubmit.bind(this);
  96. this._parseQueryResults = this._parseQueryResults.bind(this);
  97. this._query = this._query.bind(this);
  98. this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
  99. this._resourceClient = {
  100. makeQuery: this._query,
  101. parseResults: this._parseQueryResults
  102. };
  103. }
  104. /**
  105. * Sends an analytics event to record the dialog has been shown.
  106. *
  107. * @inheritdoc
  108. * @returns {void}
  109. */
  110. componentDidMount() {
  111. sendAnalytics(createInviteDialogEvent(
  112. 'invite.dialog.opened', 'dialog'));
  113. }
  114. /**
  115. * React Component method that executes once component is updated.
  116. *
  117. * @param {Object} prevState - The state object before the update.
  118. * @returns {void}
  119. */
  120. componentDidUpdate(prevState) {
  121. /**
  122. * Clears selected items from the multi select component on successful
  123. * invite.
  124. */
  125. if (prevState.addToCallError
  126. && !this.state.addToCallInProgress
  127. && !this.state.addToCallError
  128. && this._multiselect) {
  129. this._multiselect.setSelectedItems([]);
  130. }
  131. }
  132. /**
  133. * Sends an analytics event to record the dialog has been closed.
  134. *
  135. * @inheritdoc
  136. * @returns {void}
  137. */
  138. componentWillUnmount() {
  139. sendAnalytics(createInviteDialogEvent(
  140. 'invite.dialog.closed', 'dialog'));
  141. }
  142. /**
  143. * Renders the content of this component.
  144. *
  145. * @returns {ReactElement}
  146. */
  147. render() {
  148. const { addPeopleEnabled, dialOutEnabled, t } = this.props;
  149. let isMultiSelectDisabled = this.state.addToCallInProgress || false;
  150. let placeholder;
  151. let loadingMessage;
  152. let noMatches;
  153. if (addPeopleEnabled && dialOutEnabled) {
  154. loadingMessage = 'addPeople.loading';
  155. noMatches = 'addPeople.noResults';
  156. placeholder = 'addPeople.searchPeopleAndNumbers';
  157. } else if (addPeopleEnabled) {
  158. loadingMessage = 'addPeople.loadingPeople';
  159. noMatches = 'addPeople.noResults';
  160. placeholder = 'addPeople.searchPeople';
  161. } else if (dialOutEnabled) {
  162. loadingMessage = 'addPeople.loadingNumber';
  163. noMatches = 'addPeople.noValidNumbers';
  164. placeholder = 'addPeople.searchNumbers';
  165. } else {
  166. isMultiSelectDisabled = true;
  167. noMatches = 'addPeople.noResults';
  168. placeholder = 'addPeople.disabled';
  169. }
  170. return (
  171. <Dialog
  172. okDisabled = { this._isAddDisabled() }
  173. okTitleKey = 'addPeople.add'
  174. onSubmit = { this._onSubmit }
  175. titleKey = 'addPeople.title'
  176. width = 'medium'>
  177. <div className = 'add-people-form-wrap'>
  178. { this._renderErrorMessage() }
  179. <MultiSelectAutocomplete
  180. isDisabled = { isMultiSelectDisabled }
  181. loadingMessage = { t(loadingMessage) }
  182. noMatchesFound = { t(noMatches) }
  183. onItemSelected = { this._onItemSelected }
  184. onSelectionChange = { this._onSelectionChange }
  185. placeholder = { t(placeholder) }
  186. ref = { this._setMultiSelectElement }
  187. resourceClient = { this._resourceClient }
  188. shouldFitContainer = { true }
  189. shouldFocus = { true } />
  190. </div>
  191. </Dialog>
  192. );
  193. }
  194. _isAddDisabled: () => boolean;
  195. /**
  196. * Indicates if the Add button should be disabled.
  197. *
  198. * @private
  199. * @returns {boolean} - True to indicate that the Add button should
  200. * be disabled, false otherwise.
  201. */
  202. _isAddDisabled() {
  203. return !this.state.inviteItems.length
  204. || this.state.addToCallInProgress;
  205. }
  206. _onItemSelected: (Object) => Object;
  207. /**
  208. * Callback invoked when a selection has been made but before it has been
  209. * set as selected.
  210. *
  211. * @param {Object} item - The item that has just been selected.
  212. * @private
  213. * @returns {Object} The item to display as selected in the input.
  214. */
  215. _onItemSelected(item) {
  216. if (item.item.type === 'phone') {
  217. item.content = item.item.number;
  218. }
  219. return item;
  220. }
  221. _onSelectionChange: (Map<*, *>) => void;
  222. /**
  223. * Handles a selection change.
  224. *
  225. * @param {Map} selectedItems - The list of selected items.
  226. * @private
  227. * @returns {void}
  228. */
  229. _onSelectionChange(selectedItems) {
  230. this.setState({
  231. inviteItems: selectedItems
  232. });
  233. }
  234. _onSubmit: () => void;
  235. /**
  236. * Invite people and numbers to the conference. The logic works by inviting
  237. * numbers, people/rooms, and videosipgw in parallel. All invitees are
  238. * stored in an array. As each invite succeeds, the invitee is removed
  239. * from the array. After all invites finish, close the modal if there are
  240. * no invites left to send. If any are left, that means an invite failed
  241. * and an error state should display.
  242. *
  243. * @private
  244. * @returns {void}
  245. */
  246. _onSubmit() {
  247. const { inviteItems } = this.state;
  248. const items = inviteItems.map(item => item.item);
  249. const inviteTypeCounts = getInviteTypeCounts(items);
  250. sendAnalytics(createInviteDialogEvent(
  251. 'clicked', 'inviteButton', {
  252. ...inviteTypeCounts,
  253. inviteAllowed: this._isAddDisabled()
  254. }));
  255. if (this._isAddDisabled()) {
  256. return;
  257. }
  258. this.setState({
  259. addToCallInProgress: true
  260. });
  261. const { dispatch } = this.props;
  262. dispatch(sendInvitesForItems(items))
  263. .then(invitesLeftToSend => {
  264. // If any invites are left that means something failed to send
  265. // so treat it as an error.
  266. if (invitesLeftToSend.length) {
  267. const erroredInviteTypeCounts
  268. = getInviteTypeCounts(invitesLeftToSend);
  269. logger.error(`${invitesLeftToSend.length} invites failed`,
  270. erroredInviteTypeCounts);
  271. sendAnalytics(createInviteDialogEvent(
  272. 'error', 'invite', {
  273. ...erroredInviteTypeCounts
  274. }));
  275. this.setState({
  276. addToCallInProgress: false,
  277. addToCallError: true
  278. });
  279. const unsentInviteIDs = invitesLeftToSend.map(invite =>
  280. invite.id || invite.number
  281. );
  282. const itemsToSelect = inviteItems.filter(invite =>
  283. unsentInviteIDs.includes(
  284. invite.item.id || invite.item.number
  285. )
  286. );
  287. if (this._multiselect) {
  288. this._multiselect.setSelectedItems(itemsToSelect);
  289. }
  290. return;
  291. }
  292. this.setState({
  293. addToCallInProgress: false
  294. });
  295. dispatch(hideDialog());
  296. });
  297. }
  298. _parseQueryResults: (Array<Object>, string) => Array<Object>;
  299. /**
  300. * Processes results from requesting available numbers and people by munging
  301. * each result into a format {@code MultiSelectAutocomplete} can use for
  302. * display.
  303. *
  304. * @param {Array} response - The response object from the server for the
  305. * query.
  306. * @private
  307. * @returns {Object[]} Configuration objects for items to display in the
  308. * search autocomplete.
  309. */
  310. _parseQueryResults(response = []) {
  311. const { t } = this.props;
  312. const users = response.filter(item => item.type !== 'phone');
  313. const userDisplayItems = users.map(user => {
  314. return {
  315. content: user.name,
  316. elemBefore: <Avatar
  317. size = 'small'
  318. src = { user.avatar } />,
  319. item: user,
  320. tag: {
  321. elemBefore: <Avatar
  322. size = 'xsmall'
  323. src = { user.avatar } />
  324. },
  325. value: user.id
  326. };
  327. });
  328. const numbers = response.filter(item => item.type === 'phone');
  329. const telephoneIcon = this._renderTelephoneIcon();
  330. const numberDisplayItems = numbers.map(number => {
  331. const numberNotAllowedMessage
  332. = number.allowed ? '' : t('addPeople.countryNotSupported');
  333. const countryCodeReminder = number.showCountryCodeReminder
  334. ? t('addPeople.countryReminder') : '';
  335. const description
  336. = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
  337. return {
  338. filterValues: [
  339. number.originalEntry,
  340. number.number
  341. ],
  342. content: t('addPeople.telephone', { number: number.number }),
  343. description,
  344. isDisabled: !number.allowed,
  345. elemBefore: telephoneIcon,
  346. item: number,
  347. tag: {
  348. elemBefore: telephoneIcon
  349. },
  350. value: number.number
  351. };
  352. });
  353. return [
  354. ...userDisplayItems,
  355. ...numberDisplayItems
  356. ];
  357. }
  358. _query: (string) => Promise<Array<Object>>;
  359. /**
  360. * Performs a people and phone number search request.
  361. *
  362. * @param {string} query - The search text.
  363. * @private
  364. * @returns {Promise}
  365. */
  366. _query(query = '') {
  367. const {
  368. addPeopleEnabled,
  369. dialOutEnabled,
  370. _dialOutAuthUrl,
  371. _jwt,
  372. _peopleSearchQueryTypes,
  373. _peopleSearchUrl
  374. } = this.props;
  375. const options = {
  376. dialOutAuthUrl: _dialOutAuthUrl,
  377. addPeopleEnabled,
  378. dialOutEnabled,
  379. jwt: _jwt,
  380. peopleSearchQueryTypes: _peopleSearchQueryTypes,
  381. peopleSearchUrl: _peopleSearchUrl
  382. };
  383. return getInviteResultsForQuery(query, options);
  384. }
  385. /**
  386. * Renders the error message if the add doesn't succeed.
  387. *
  388. * @private
  389. * @returns {ReactElement|null}
  390. */
  391. _renderErrorMessage() {
  392. if (!this.state.addToCallError) {
  393. return null;
  394. }
  395. const { t } = this.props;
  396. const supportString = t('inlineDialogFailure.supportMsg');
  397. const supportLink = interfaceConfig.SUPPORT_URL;
  398. const supportLinkContent
  399. = ( // eslint-disable-line no-extra-parens
  400. <span>
  401. <span>
  402. { supportString.padEnd(supportString.length + 1) }
  403. </span>
  404. <span>
  405. <a
  406. href = { supportLink }
  407. rel = 'noopener noreferrer'
  408. target = '_blank'>
  409. { t('inlineDialogFailure.support') }
  410. </a>
  411. </span>
  412. <span>.</span>
  413. </span>
  414. );
  415. return (
  416. <div className = 'modal-dialog-form-error'>
  417. <InlineMessage
  418. title = { t('addPeople.failedToAdd') }
  419. type = 'error'>
  420. { supportLinkContent }
  421. </InlineMessage>
  422. </div>
  423. );
  424. }
  425. /**
  426. * Renders a telephone icon.
  427. *
  428. * @private
  429. * @returns {ReactElement}
  430. */
  431. _renderTelephoneIcon() {
  432. return (
  433. <span className = 'add-telephone-icon'>
  434. <i className = 'icon-telephone' />
  435. </span>
  436. );
  437. }
  438. _setMultiSelectElement: (React$ElementRef<*> | null) => mixed;
  439. /**
  440. * Sets the instance variable for the multi select component
  441. * element so it can be accessed directly.
  442. *
  443. * @param {Object} element - The DOM element for the component's dialog.
  444. * @private
  445. * @returns {void}
  446. */
  447. _setMultiSelectElement(element) {
  448. this._multiselect = element;
  449. }
  450. }
  451. /**
  452. * Maps (parts of) the Redux state to the associated
  453. * {@code AddPeopleDialog}'s props.
  454. *
  455. * @param {Object} state - The Redux state.
  456. * @private
  457. * @returns {{
  458. * _dialOutAuthUrl: string,
  459. * _jwt: string,
  460. * _peopleSearchQueryTypes: Array<string>,
  461. * _peopleSearchUrl: string
  462. * }}
  463. */
  464. function _mapStateToProps(state) {
  465. const {
  466. dialOutAuthUrl,
  467. peopleSearchQueryTypes,
  468. peopleSearchUrl
  469. } = state['features/base/config'];
  470. return {
  471. _dialOutAuthUrl: dialOutAuthUrl,
  472. _jwt: state['features/base/jwt'].jwt,
  473. _peopleSearchQueryTypes: peopleSearchQueryTypes,
  474. _peopleSearchUrl: peopleSearchUrl
  475. };
  476. }
  477. export default translate(connect(_mapStateToProps)(AddPeopleDialog));